[
  {
    "path": ".editorconfig",
    "content": "# EditorConfig (https://editorconfig.org)\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_size = 4\nindent_style = space\nmax_line_length = 100\ntrim_trailing_whitespace = true\n\n[{*.cmake,CMakeLists.txt}]\n# TODO: indent_size = 2\nindent_size = 4\n\n[*.css]\nindent_size = 2\n\n[*.json]\nindent_size = 2\n\n[*.{yaml,yml}]\nindent_size = 2\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Autodetect text files\n* text=auto\n\n# Enforce text mode\n*.c text\n*.cmake text\n*.conf text\n*.cpp text\n*.css text\n*.desktop text\n*.h text\n*.html text\n*.in text\n*.json text\n*.md text\n*.qrc text\n*.ui text\n*.yaml text\n*.yml text\n\n.editorconfig text\n.gitignore text\nCMakeLists.txt text\nCOPYING text\n\n# Binary files\n*.icns binary\n*.ico binary\n*.png binary\n*.woff binary\n*.woff2 binary\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# This file allows automatic assignment of pull requests.\n# See https://help.github.com/articles/about-codeowners/\n\n*       @trollixx\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n    commit-message:\n      prefix: ci(github)\n    labels: []\n"
  },
  {
    "path": ".github/workflows/analyze-codeql.yaml",
    "content": "name: CodeQL Scan\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    # The branches below must be a subset of the branches above.\n    branches: [main]\n  schedule:\n  - cron: '0 8 * * 6'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  analyze-codeql:\n    name: Analyze\n    runs-on: ubuntu-24.04\n\n    permissions:\n      security-events: write\n\n    steps:\n    - name: Checkout Repository\n      uses: actions/checkout@v6\n\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v4\n\n    - name: Install Dependencies\n      run: |\n        sudo apt-get -y -qq update\n        sudo apt-get -y -qq --no-install-recommends install \\\n          cmake \\\n          extra-cmake-modules \\\n          libarchive-dev \\\n          libgl1-mesa-dev \\\n          libqt6opengl6-dev \\\n          libsqlite3-dev \\\n          libvulkan-dev \\\n          libxcb-keysyms1-dev \\\n          ninja-build \\\n          qt6-base-private-dev \\\n          qt6-webengine-dev \\\n          qt6-webengine-dev-tools\n\n    - name: Configure & Build\n      uses: lukka/run-cmake@v10\n      with:\n        configurePreset: ninja-multi\n        buildPreset: ninja-multi-release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v4\n"
  },
  {
    "path": ".github/workflows/analyze-coverity.yaml",
    "content": "name: Coverity Scan\n\non:\n  push:\n    branches: [main]\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  analyze-coverity:\n    name: Analyze\n    if: github.repository == 'zealdocs/zeal'\n    runs-on: ubuntu-24.04\n\n    steps:\n    - name: Checkout Repository\n      uses: actions/checkout@v6\n      with:\n        fetch-depth: 0\n\n    - name: Install Dependencies\n      run: |\n        sudo apt-get -y -qq update\n        sudo apt-get -y -qq --no-install-recommends install \\\n            cmake \\\n            extra-cmake-modules \\\n            git \\\n            libarchive-dev \\\n            libgl1-mesa-dev \\\n            libqt6opengl6-dev \\\n            libsqlite3-dev \\\n            libvulkan-dev \\\n            libxcb-keysyms1-dev \\\n            ninja-build \\\n            qt6-base-private-dev \\\n            qt6-webengine-dev \\\n            qt6-webengine-dev-tools\n\n    - name: Configure\n      run: cmake -B build -G Ninja\n\n    - name: Retrieve Application Version\n      run: |\n        zeal_version=$(<build/zeal_version)\n        echo \"Zeal Version: ${zeal_version}\"\n        echo \"ZEAL_VERSION=${zeal_version}\" >> $GITHUB_ENV\n\n    - name: Coverity Scan\n      uses: vapier/coverity-scan-action@v1\n      with:\n        command: ninja -C build\n        version: ${{ env.ZEAL_VERSION }}\n        email: ${{ secrets.COVERITY_SCAN_EMAIL }}\n        token: ${{ secrets.COVERITY_SCAN_TOKEN }}\n\n    - name: Upload Build Log\n      uses: actions/upload-artifact@v7\n      with:\n        name: build-log\n        path: cov-int/build-log.txt\n        if-no-files-found: ignore\n"
  },
  {
    "path": ".github/workflows/appimage/Dockerfile",
    "content": "FROM ubuntu:jammy\n\n# Force older pipx to use global location.\nENV PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin PIPX_MAN_DIR=/usr/local/share/man\n\nRUN apt-get update -q -y \\\n    # Install appimage-builder and appimagetool dependencies.\n    && DEBIAN_FRONTEND=\"noninteractive\" apt-get install -q -y --no-install-recommends \\\n       appstream curl desktop-file-utils fakeroot file git gnupg patchelf pipx squashfs-tools zsync \\\n    # Install appimagetool, it has to be extracted because FUSE doesn't work in containers without extra fiddling.\n    && cd /tmp \\\n    && curl -sLO https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage \\\n    && chmod +x appimagetool-x86_64.AppImage \\\n    && ./appimagetool-x86_64.AppImage --appimage-extract \\\n    && mv squashfs-root/ /opt/appimagetool.AppDir \\\n    && ln -s /opt/appimagetool.AppDir/AppRun /usr/local/bin/appimagetool \\\n    && rm appimagetool-x86_64.AppImage \\\n    && cd - \\\n    # Install appimage-builder.\n    && pipx install git+https://github.com/AppImageCrafters/appimage-builder.git@d96cf01e131e01b9f9e713db8efa7d20c57f6a09 \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Set entrypoint.\nCOPY entrypoint.sh /entrypoint.sh\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": ".github/workflows/appimage/action.yaml",
    "content": "name: 'AppImage Builder'\ndescription: 'Create an AppImage with appimage-builder.'\ninputs:\n  recipe:\n    description: 'Path to the appimage-builder recipe.'\n    required: true\n    default: 'AppImageBuilder.yml'\n  apt_dependencies:\n    description: 'List of packages to install with apt-get.'\n    required: false\nruns:\n  using: 'docker'\n  image: 'Dockerfile'\n  args:\n    - ${{ inputs.recipe }}\n    - ${{ inputs.apt_dependencies }}\n"
  },
  {
    "path": ".github/workflows/appimage/entrypoint.sh",
    "content": "#!/bin/bash\n\n# Should be in .gitignore.\nexport APPIMAGE_BUILD_DIR=build.appimage\n\n# Install dependencies\nif [ ! -z ${INPUT_APT_DEPENDENCIES+x} ]; then\n    apt-get update -q -y\n    apt-get install -q -y --no-install-recommends ${INPUT_APT_DEPENDENCIES}\nfi\n\n# Run appimage-builder\nappimage-builder --skip-test --build-dir ${APPIMAGE_BUILD_DIR} --appdir ${APPIMAGE_BUILD_DIR}/AppDir --recipe ${INPUT_RECIPE}\n"
  },
  {
    "path": ".github/workflows/build-check.yaml",
    "content": "name: Build Check\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build-ubuntu:\n    name: ${{ matrix.config.name }}\n    runs-on: ${{ matrix.config.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        config:\n          - name: Ubuntu 24.04 / Qt 5\n            os: ubuntu-24.04\n            qt_packages: >-\n              libqt5x11extras5-dev\n              qtwebengine5-dev\n            configurePreset: ninja-multi\n            buildPreset: ninja-multi-release\n          - name: Ubuntu 24.04 / Qt 5 / Portable\n            os: ubuntu-24.04\n            qt_packages: >-\n              libqt5x11extras5-dev\n              qtwebengine5-dev\n            configurePreset: ninja-multi-portable\n            buildPreset: ninja-multi-portable-release\n          - name: Ubuntu 24.04 / Qt 6\n            os: ubuntu-24.04\n            qt_packages: >-\n              libgl1-mesa-dev\n              libqt6opengl6-dev\n              qt6-base-private-dev\n              qt6-webengine-dev\n              qt6-webengine-dev-tools\n            configurePreset: ninja-multi\n            buildPreset: ninja-multi-release\n          - name: Ubuntu 24.04 / Qt 6 / Portable\n            os: ubuntu-24.04\n            qt_packages: >-\n              libgl1-mesa-dev\n              libqt6opengl6-dev\n              qt6-base-private-dev\n              qt6-webengine-dev\n              qt6-webengine-dev-tools\n            configurePreset: ninja-multi-portable\n            buildPreset: ninja-multi-portable-release\n          - name: Ubuntu 24.04 ARM64 / Qt 6\n            os: ubuntu-24.04-arm\n            qt_packages: >-\n              libgl1-mesa-dev\n              libqt6opengl6-dev\n              qt6-base-private-dev\n              qt6-webengine-dev\n              qt6-webengine-dev-tools\n            configurePreset: ninja-multi\n            buildPreset: ninja-multi-release\n          - name: Ubuntu 24.04 ARM64 / Qt 6 / Portable\n            os: ubuntu-24.04-arm\n            qt_packages: >-\n              libgl1-mesa-dev\n              libqt6opengl6-dev\n              qt6-base-private-dev\n              qt6-webengine-dev\n              qt6-webengine-dev-tools\n            configurePreset: ninja-multi-portable\n            buildPreset: ninja-multi-portable-release\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Install Dependencies\n        run: |\n          sudo apt-get -y -qq update\n          sudo apt-get -y -qq --no-install-recommends install \\\n            cmake \\\n            extra-cmake-modules \\\n            libarchive-dev \\\n            libsqlite3-dev \\\n            libvulkan-dev \\\n            libxcb-keysyms1-dev \\\n            ninja-build \\\n            ${{ matrix.config.qt_packages }}\n\n      - name: Configure & Build\n        uses: lukka/run-cmake@v10\n        with:\n          configurePreset: ${{ matrix.config.configurePreset }}\n          buildPreset: ${{ matrix.config.buildPreset }}\n\n  build-windows:\n    name: ${{ matrix.config.name }}\n    runs-on: ${{ matrix.config.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        config:\n          - name: Windows Server 2022 / Qt 5\n            os: windows-2022\n            arch: win64_msvc2019_64\n            qt_modules: >-\n              qtwebengine\n            qt_version: \"5.15.2\"\n            configurePreset: ninja-multi-vcpkg\n            buildPreset: ninja-multi-vcpkg-release\n            publishArtifacts: false\n          - name: Windows Server 2022 / Qt 5 / Portable\n            os: windows-2022\n            arch: win64_msvc2019_64\n            qt_modules: >-\n              qtwebengine\n            qt_version: \"5.15.2\"\n            configurePreset: ninja-multi-vcpkg-portable\n            buildPreset: ninja-multi-vcpkg-portable-release\n            publishArtifacts: false\n          - name: Windows Server 2022 / Qt 6\n            os: windows-2022\n            arch: win64_msvc2022_64\n            qt_modules: >-\n              qtpositioning\n              qtwebchannel\n              qtwebengine\n            qt_version: \"6.10.2\"\n            configurePreset: ninja-multi-vcpkg\n            buildPreset: ninja-multi-vcpkg-release\n            publishArtifacts: true\n          - name: Windows Server 2022 / Qt 6 / Portable\n            os: windows-2022\n            arch: win64_msvc2022_64\n            qt_modules: >-\n              qtpositioning\n              qtwebchannel\n              qtwebengine\n            qt_version: \"6.10.2\"\n            configurePreset: ninja-multi-vcpkg-portable\n            buildPreset: ninja-multi-vcpkg-portable-release\n            publishArtifacts: true\n\n    env:\n      VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/.vcpkg-cache,readwrite\n      VCPKG_BUILD_TYPE: release\n      VCPKG_DEFAULT_TRIPLET: x64-windows\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      # Workaround for https://github.com/lukka/run-vcpkg/issues/251.\n      - name: Prepare vcpkg binary cache\n        uses: actions/cache@v5\n        with:\n          path: ${{ github.workspace }}/.vcpkg-cache\n          key: vcpkg-${{ matrix.config.os }}-${{ matrix.config.buildPreset }}-${{ hashFiles(format('build/{0}/vcpkg_installed/vcpkg/status', matrix.config.configurePreset)) }}\n          restore-keys: vcpkg-${{ matrix.config.os }}-${{ matrix.config.buildPreset }}-\n\n      - name: Prepare vcpkg\n        uses: lukka/run-vcpkg@v11\n        with:\n          vcpkgDirectory: ${{ github.workspace }}/.vcpkg\n\n      - name: Install Qt\n        uses: jurplel/install-qt-action@v4\n        with:\n          arch: ${{ matrix.config.arch }}\n          modules: ${{ matrix.config.qt_modules }}\n          version: ${{ matrix.config.qt_version }}\n          cache: true\n\n      - name: Configure & Build\n        uses: lukka/run-cmake@v10\n        with:\n          configurePreset: ${{ matrix.config.configurePreset }}\n          buildPreset: ${{ matrix.config.buildPreset }}\n          configurePresetAdditionalArgs: \"['-DVCPKG_DISABLE_COMPILER_TRACKING=ON']\"\n\n      - name: Retrieve Application Version\n        run: |\n          $zeal_version = Get-Content build/${{ matrix.config.configurePreset }}/zeal_version\n          Write-Output \"Zeal Version: $zeal_version\"\n          \"ZEAL_VERSION=$zeal_version\" >> $env:GITHUB_ENV\n\n      - name: Package\n        if: matrix.config.publishArtifacts\n        run: cmake --build build --preset ${{ matrix.config.buildPreset }} --target package\n        env:\n          CODESIGN_CERTIFICATE_BASE64: ${{ secrets.CODESIGN_CERTIFICATE_BASE64 }}\n          CODESIGN_PASSWORD: ${{ secrets.CODESIGN_PASSWORD }}\n\n      - name: Upload ZIP Artifacts\n        if: matrix.config.publishArtifacts\n        uses: actions/upload-artifact@v7\n        with:\n          name: zeal-${{ env.ZEAL_VERSION }}${{ matrix.config.configurePreset == 'ninja-multi-vcpkg-portable' &&  '-portable' || '' }}-windows-x64.zip\n          path: |\n            build/${{ matrix.config.configurePreset }}/zeal-*.zip\n            build/${{ matrix.config.configurePreset }}/zeal-*.zip.sha256\n\n      - name: Upload MSI Artifacts\n        if: matrix.config.publishArtifacts && matrix.config.configurePreset == 'ninja-multi-vcpkg'\n        uses: actions/upload-artifact@v7\n        with:\n          name: zeal-${{ env.ZEAL_VERSION }}-windows-x64.msi\n          path: |\n            build/${{ matrix.config.configurePreset }}/zeal-*.msi\n            build/${{ matrix.config.configurePreset }}/zeal-*.msi.sha256\n\n  build-appimage:\n    name: AppImage\n    runs-on: ubuntu-24.04\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Build AppImage\n        uses: ./.github/workflows/appimage/\n        with:\n          recipe: pkg/appimage/appimage-amd64.yaml\n          apt_dependencies: >-\n            appstream\n            build-essential\n            cmake extra-cmake-modules\n            libarchive-dev\n            libayatana-appindicator3-dev\n            libqt5x11extras5-dev\n            libsqlite3-dev\n            libxcb-keysyms1-dev\n            ninja-build\n            qtbase5-dev\n            qtwebengine5-dev\n\n      - name: Upload AppImage\n        uses: actions/upload-artifact@v7\n        with:\n          name: zeal-dev-x86_64.AppImage # TODO: Provide real version.\n          path: zeal-*.AppImage\n"
  },
  {
    "path": ".github/workflows/lock.yaml",
    "content": "name: Lock Issues\n\non:\n  schedule:\n    - cron: \"0 0 * * *\"\n  workflow_dispatch:\n\npermissions:\n  issues: write\n  pull-requests: write\n\nconcurrency:\n  group: lock\n\njobs:\n  lock-issues:\n    name: Lock Old Issues\n    if: github.repository == 'zealdocs/zeal'\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: dessant/lock-threads@v6\n        with:\n          issue-inactive-days: 180\n          issue-lock-reason: \"\"\n          process-only: issues\n"
  },
  {
    "path": ".github/workflows/release/cliff.toml",
    "content": "# git-cliff ~ configuration file\n# https://git-cliff.org/docs/configuration\n\n# Set via GITHUB_REPO and GITHUB_TOKEN environment variables.\n#[remote.github]\n#owner = \"zealdocs\"\n#repo = \"zeal\"\n#token = \"\"\n\n[changelog]\n# Template docs: https://keats.github.io/tera/docs/.\nbody = \"\"\"\\\n<!-- TODO: Update milestone link, review commits. -->\n{% if version and previous.version %}\n  [Full Changelog]({{ self::diff_url() }}) | [Resolved Issues]({{ self::remote_url() }}/milestone/TBD?closed=1)\n{% endif %}\\\n\n{% for group, commits in commits | group_by(attribute=\"group\") %}\n  ### {{ group | striptags | trim }}\n  {% for commit in commits %}\n    {% if commit.github.pr_title -%}\n      {%- set commit_message = commit.github.pr_title -%}\n    {%- else -%}\n      {%- set commit_message = commit.message -%}\n    {%- endif -%}\n    - {% if commit.scope %}**{{ commit.scope }}:** {% endif %}\\\n        {{ commit.message | split(pat=\"\\n\") | first | trim }} \\\n        ({{ self::commit_link(id=commit.id) }})\\\n  {% endfor %}\n{% endfor %}\n---\n\n{%- if github -%}\\\n{% if github.contributors | filter(attribute=\"is_first_time\", value=true) | length != 0 %}\n  {% raw %}\\n{% endraw -%}\n  #### New Contributors\n{%- endif %}\n{% for contributor in github.contributors | filter(attribute=\"is_first_time\", value=true) %}\n  - @{{ contributor.username }} made their first contribution\n    {%- if contributor.pr_number %} in \\\n      {{ self::pr_link(pr_number=contributor.pr_number) }}\\\n    {%- endif %}\n{%- endfor -%}\n{%- endif -%}\n\n{%- macro remote_url() -%}\n  https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\n{%- endmacro -%}\n\n{%- macro diff_url() -%}\n  {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}\n{%- endmacro -%}\n\n{%- macro pr_link(pr_number) -%}\n  [#{{ pr_number }}]({{ self::remote_url() }}/pull/{{ pr_number }})\n{%- endmacro -%}\n\n{%- macro commit_link(id) -%}\n  [`{{ id | truncate(length=7, end=\"\") }}`]({{ self::remote_url() }}/commit/{{ id }})\n{%- endmacro -%}\n\"\"\"\n# Remove the leading and trailing whitespace from the template.\ntrim = true\n# Template for the changelog footer.\nfooter = \"\"\"\n\"\"\"\n# Postprocessors.\npostprocessors = []\n\n[git]\n# Parse the commits based on https://www.conventionalcommits.org.\nconventional_commits = true\n# Filter out the commits that are not conventional.\nfilter_unconventional = true\n# Process each line of a commit as an individual commit.\nsplit_commits = false\n# Regex for preprocessing the commit messages.\ncommit_preprocessors = [\n  # Remove issue numbers from commits.\n  { pattern = '\\((\\w+\\s)?#([0-9]+)\\)', replace = \"\" },\n]\n# Regex for parsing and grouping commits.\ncommit_parsers = [\n  { message = \"^feat\", group = \"<!-- 0 -->Features\" },\n  { message = \"^fix\", group = \"<!-- 1 -->Bug Fixes\" },\n  { message = \"^perf\", group = \"<!-- 2 -->Performance\" },\n  { message = \"^doc\", group = \"<!-- 3 -->Documentation\" },\n  { message = \"^build\", group = \"<!-- 4 -->Build System\" },\n  { message = \"^ci\", group = \"<!-- 5 -->CI/CD\" },\n  # Skipped groups:\n  { message = \"^chore\", skip = true },\n  { message = \"^refactor\", skip = true },\n]\n# Filter out the commits that are not matched by commit parsers.\nfilter_commits = false\n# Sort the tags topologically.\ntopo_order = false\n# Sort the commits inside sections by oldest/newest order.\nsort_commits = \"oldest\"\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - \"v*.*.*\"\n\n# Required for creating GitHub release.\npermissions:\n  contents: write\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build-windows:\n    name: ${{ matrix.config.name }}\n    runs-on: ${{ matrix.config.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        config:\n          - name: Windows Server 2022 / Qt 6\n            os: windows-2022\n            arch: win64_msvc2022_64\n            qt_modules: >-\n              qtpositioning\n              qtwebchannel\n              qtwebengine\n            qt_version: \"6.10.2\"\n            configurePreset: ninja-multi-vcpkg\n            buildPreset: ninja-multi-vcpkg-release\n          - name: Windows Server 2022 / Qt 6 / Portable\n            os: windows-2022\n            arch: win64_msvc2022_64\n            qt_modules: >-\n              qtpositioning\n              qtwebchannel\n              qtwebengine\n            qt_version: \"6.10.2\"\n            configurePreset: ninja-multi-vcpkg-portable\n            buildPreset: ninja-multi-vcpkg-portable-release\n\n    env:\n      VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/.vcpkg-cache,readwrite\n      VCPKG_BUILD_TYPE: release\n      VCPKG_DEFAULT_TRIPLET: x64-windows\n      ZEAL_RELEASE_BUILD: ON\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      # Workaround for https://github.com/lukka/run-vcpkg/issues/251.\n      - name: Prepare vcpkg binary cache\n        uses: actions/cache@v5\n        with:\n          path: ${{ github.workspace }}/.vcpkg-cache\n          key: vcpkg-${{ matrix.config.os }}-${{ matrix.config.buildPreset }}-${{ hashFiles(format('build/{0}/vcpkg_installed/vcpkg/status', matrix.config.configurePreset)) }}\n          restore-keys: vcpkg-${{ matrix.config.os }}-${{ matrix.config.buildPreset }}-\n\n      - name: Prepare vcpkg\n        uses: lukka/run-vcpkg@v11\n        with:\n          vcpkgDirectory: ${{ github.workspace }}/.vcpkg\n\n      - name: Install Qt\n        uses: jurplel/install-qt-action@v4\n        with:\n          arch: ${{ matrix.config.arch }}\n          modules: ${{ matrix.config.qt_modules }}\n          version: ${{ matrix.config.qt_version }}\n          cache: true\n\n      - name: Configure & Build\n        uses: lukka/run-cmake@v10\n        with:\n          configurePreset: ${{ matrix.config.configurePreset }}\n          buildPreset: ${{ matrix.config.buildPreset }}\n          configurePresetAdditionalArgs: \"['-DVCPKG_DISABLE_COMPILER_TRACKING=ON']\"\n\n      - name: Retrieve Application Version\n        run: |\n          $zeal_version = Get-Content build/${{ matrix.config.configurePreset }}/zeal_version\n          Write-Output \"Zeal Version: $zeal_version\"\n          \"ZEAL_VERSION=$zeal_version\" >> $env:GITHUB_ENV\n\n      - name: Package\n        run: cmake --build build --preset ${{ matrix.config.buildPreset }} --target package\n        env:\n          CODESIGN_CERTIFICATE_BASE64: ${{ secrets.CODESIGN_CERTIFICATE_BASE64 }}\n          CODESIGN_PASSWORD: ${{ secrets.CODESIGN_PASSWORD }}\n\n      - name: Update GitHub Release\n        uses: softprops/action-gh-release@v2\n        with:\n          draft: true\n          # Only upload the following artifacts:\n          # - Portable 7-Zip and ZIP packages.\n          # - Non-portable MSI package.\n          files: |\n            build/${{ matrix.config.configurePreset }}/zeal-${{ env.ZEAL_VERSION }}-portable-windows-x64.*\n            build/${{ matrix.config.configurePreset }}/zeal-${{ env.ZEAL_VERSION }}-windows-x64.msi*\n\n  build-appimage:\n    name: AppImage\n    runs-on: ubuntu-24.04\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Build AppImage\n        uses: ./.github/workflows/appimage/\n        with:\n          recipe: pkg/appimage/appimage-amd64.yaml\n          apt_dependencies: >-\n            appstream\n            build-essential\n            cmake extra-cmake-modules\n            libayatana-appindicator3-dev\n            libarchive-dev\n            libqt5x11extras5-dev\n            libsqlite3-dev\n            libxcb-keysyms1-dev\n            ninja-build\n            qtbase5-dev\n            qtwebengine5-dev\n\n      - name: Generate Digest Files\n        run: for file in zeal-*.AppImage*; do sha256sum $file > $file.sha256; done\n\n      - name: Update GitHub Release\n        uses: softprops/action-gh-release@v2\n        with:\n          draft: true\n          fail_on_unmatched_files: true\n          files: |\n            zeal-*.AppImage*\n\n  build-ubuntu:\n    name: ${{ matrix.config.name }}\n    runs-on: ${{ matrix.config.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        config:\n          - name: Ubuntu 24.04 / Source\n            os: ubuntu-24.04\n            qt_packages: >-\n              libgl1-mesa-dev\n              libqt6opengl6-dev\n              qt6-base-private-dev\n              qt6-webengine-dev\n              qt6-webengine-dev-tools\n            configurePreset: ninja-multi\n            buildPreset: ninja-multi-release\n\n    env:\n      ZEAL_RELEASE_BUILD: ON\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Install Dependencies\n        run: |\n          sudo apt-get -y -qq update\n          sudo apt-get -y -qq --no-install-recommends install \\\n            cmake \\\n            extra-cmake-modules \\\n            libarchive-dev \\\n            libsqlite3-dev \\\n            libvulkan-dev \\\n            libxcb-keysyms1-dev \\\n            ninja-build \\\n            ${{ matrix.config.qt_packages }}\n\n      - name: Configure & Package Source\n        uses: lukka/run-cmake@v10\n        with:\n          configurePreset: ${{ matrix.config.configurePreset }}\n          buildPreset: ${{ matrix.config.buildPreset }}\n          buildPresetAdditionalArgs: \"['--target package_source']\"\n\n      - name: Generate Changelog\n        uses: orhun/git-cliff-action@v4\n        id: git-cliff\n        with:\n          args: --latest\n          config: .github/workflows/release/cliff.toml\n        env:\n          GITHUB_REPO: ${{ github.repository }}\n\n      - name: Update GitHub Release\n        uses: softprops/action-gh-release@v2\n        with:\n          draft: true\n          fail_on_unmatched_files: true\n          body: ${{ steps.git-cliff.outputs.content }}\n          # Only upload the following artifacts:\n          # - Source packages.\n          files: |\n            build/${{ matrix.config.configurePreset }}/zeal-*.*\n"
  },
  {
    "path": ".gitignore",
    "content": "# C++ objects and libs\n*.a\n*.dll\n*.dylib\n*.la\n*.lai\n*.lo\n*.o\n*.slo\n*.so\n\n# CMake\nbuild.*/\nbuild/\nCMakeLists.txt.user\nCMakeUserPresets.json\n\n# AppImage Builder\n*.AppImage\n*.AppImage.zsync\nappimage-builder-cache/\nsquashfs-root/\n\n# WiX Toolset\n*.msi\n*.wixobj\n*.wixpdb\n\n# Qt Creator\n*.autosave\n\n# VS Code\n.vscode/\n\n# Linux appdata\n/assets/freedesktop/org.zealdocs.zeal.appdata.xml\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nThe version history is available on [GitHub Releases](https://github.com/zealdocs/zeal/releases).\n"
  },
  {
    "path": "CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.16.3)\n\n# CMake options.\nset(CMAKE_DISABLE_IN_SOURCE_BUILD ON)\nset(CMAKE_DISABLE_SOURCE_CHANGES  ON)\nset(CMAKE_ERROR_DEPRECATED TRUE)\nset(CMAKE_MODULE_PATH \"${CMAKE_SOURCE_DIR}/cmake\")\n\nproject(Zeal\n    VERSION 0.8.0\n    DESCRIPTION \"A simple documentation browser.\"\n    HOMEPAGE_URL \"https://zealdocs.org\"\n    LANGUAGES CXX\n)\n\n# Set to TRUE for a tagged release.\n# NOTE: Don't forget to add a new release entry in the AppStream metadata!\nif(NOT ZEAL_RELEASE_BUILD AND DEFINED ENV{ZEAL_RELEASE_BUILD})\n    set(ZEAL_RELEASE_BUILD $ENV{ZEAL_RELEASE_BUILD})\nendif()\n\n# Project information.\nset(PROJECT_COMPANY_NAME \"Oleg Shparber\")\nset(PROJECT_COPYRIGHT \"© 2013-2026 Oleg Shparber and other contributors\")\n\n# Find available major Qt version. It will be stored in QT_VERSION_MAJOR.\nif(NOT ZEAL_USE_QT5)\n    find_package(QT NAMES Qt6 COMPONENTS Core)\n    set(QT_MINIMUM_VERSION 6.2.0)\nendif()\n\nif(NOT QT_FOUND)\n    find_package(QT NAMES Qt5 REQUIRED COMPONENTS Core)\n    set(QT_MINIMUM_VERSION 5.15.2)\nendif()\n\nmessage(NOTICE \"Detected Qt version: ${QT_VERSION}\")\n\n# Determine version for dev builds.\nif(NOT ZEAL_RELEASE_BUILD)\n    message(NOTICE \"Building unreleased code. Proceed at your own risk!\")\n\n    # TODO: Add support for metadata passed from env, e.g. aur, appimage, etc.\n    include(GetVersionFromGit)\n    if(Zeal_GIT_VERSION_SHA)\n        # Extra check in case we forgot to bump version in project() directive.\n        if(NOT PROJECT_VERSION_PATCH EQUAL Zeal_GIT_VERSION_PATCH_NEXT)\n            message(WARNING \"Incorrect patch version! Forgot to bump?\")\n        endif()\n\n        set(ZEAL_VERSION_SUFFIX \"-dev.${Zeal_GIT_VERSION_AHEAD}+${Zeal_GIT_VERSION_SHA}\")\n    else()\n        set(ZEAL_VERSION_SUFFIX \"-dev\")\n    endif()\nendif()\n\nset(ZEAL_VERSION_FULL \"${Zeal_VERSION}${ZEAL_VERSION_SUFFIX}\")\nmessage(NOTICE \"Calculated Zeal version: ${ZEAL_VERSION_FULL}\")\n\nfile(WRITE \"${CMAKE_BINARY_DIR}/zeal_version\" ${ZEAL_VERSION_FULL})\n\n# A custom target to print the full version.\n# Usage: cmake --build build --preset ninja-multi-vcpkg-release --target zeal_version\nadd_custom_target(zeal_version\n    COMMAND ${CMAKE_COMMAND} -E echo \"Zeal version: ${ZEAL_VERSION_FULL}\"\n    VERBATIM\n)\n\nif(${CMAKE_VERSION} VERSION_GREATER_EQUAL \"3.24.0\")\n    set(CMAKE_COMPILE_WARNING_AS_ERROR ON)\nendif()\n\noption(BUILD_TESTING \"Build the testing suite\" ON)\nif(BUILD_TESTING)\n    enable_testing()\nendif()\n\nadd_subdirectory(assets)\nadd_subdirectory(src)\n"
  },
  {
    "path": "CMakePresets.json",
    "content": "{\n  \"version\": 3,\n  \"cmakeMinimumRequired\": {\n    \"major\": 3,\n    \"minor\": 21,\n    \"patch\": 0\n  },\n  \"configurePresets\": [\n    {\n      \"name\": \"ninja-multi\",\n      \"generator\": \"Ninja Multi-Config\",\n      \"binaryDir\": \"${sourceDir}/build/${presetName}\",\n      \"warnings\": {\n        \"deprecated\": true,\n        \"dev\": true,\n        \"uninitialized\": true,\n        \"unusedCli\": true\n      }\n    },\n    {\n      \"name\": \"ninja-multi-portable\",\n      \"inherits\": [\n        \"ninja-multi\",\n        \"config-portable\"\n      ]\n    },\n    {\n      \"name\": \"ninja-multi-vcpkg\",\n      \"condition\": {\n        \"type\": \"equals\",\n        \"lhs\": \"${hostSystemName}\",\n        \"rhs\": \"Windows\"\n      },\n      \"inherits\": [\n        \"ninja-multi\"\n      ],\n      \"cacheVariables\": {\n        \"CMAKE_TOOLCHAIN_FILE\": {\n          \"type\": \"FILEPATH\",\n          \"value\": \"$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake\"\n        },\n        \"X_VCPKG_APPLOCAL_DEPS_INSTALL\": \"ON\"\n      }\n    },\n    {\n      \"name\": \"ninja-multi-vcpkg-portable\",\n      \"inherits\": [\n        \"ninja-multi-vcpkg\",\n        \"config-portable\"\n      ]\n    },\n    {\n      \"name\": \"config-portable\",\n      \"hidden\": true,\n      \"cacheVariables\": {\n        \"ZEAL_PORTABLE_BUILD\": {\n          \"type\": \"BOOL\",\n          \"value\": \"ON\"\n        }\n      }\n    },\n    {\n      \"name\": \"config-release\",\n      \"hidden\": true,\n      \"cacheVariables\": {\n        \"ZEAL_RELEASE_BUILD\": {\n          \"type\": \"BOOL\",\n          \"value\": \"ON\"\n        }\n      }\n    },\n    {\n      \"name\": \"config-testing\",\n      \"hidden\": true,\n      \"cacheVariables\": {\n        \"BUILD_TESTING\": {\n          \"type\": \"BOOL\",\n          \"value\": \"ON\"\n        }\n      }\n    },\n    {\n      \"name\": \"ninja-multi-vcpkg-test\",\n      \"inherits\": [\n        \"ninja-multi-vcpkg\",\n        \"config-testing\"\n      ]\n    }\n  ],\n  \"buildPresets\": [\n    {\n      \"name\": \"ninja-multi-debug\",\n      \"configurePreset\": \"ninja-multi\",\n      \"configuration\": \"Debug\"\n    },\n    {\n      \"name\": \"ninja-multi-release\",\n      \"configurePreset\": \"ninja-multi\",\n      \"configuration\": \"RelWithDebInfo\"\n    },\n    {\n      \"name\": \"ninja-multi-debug-portable\",\n      \"configurePreset\": \"ninja-multi-portable\",\n      \"configuration\": \"Debug\"\n    },\n    {\n      \"name\": \"ninja-multi-portable-release\",\n      \"configurePreset\": \"ninja-multi-portable\",\n      \"configuration\": \"RelWithDebInfo\"\n    },\n    {\n      \"name\": \"ninja-multi-vcpkg-debug\",\n      \"configurePreset\": \"ninja-multi-vcpkg\",\n      \"configuration\": \"Debug\"\n    },\n    {\n      \"name\": \"ninja-multi-vcpkg-release\",\n      \"configurePreset\": \"ninja-multi-vcpkg\",\n      \"configuration\": \"RelWithDebInfo\"\n    },\n    {\n      \"name\": \"ninja-multi-vcpkg-portable-debug\",\n      \"configurePreset\": \"ninja-multi-vcpkg-portable\",\n      \"configuration\": \"Debug\"\n    },\n    {\n      \"name\": \"ninja-multi-vcpkg-portable-release\",\n      \"configurePreset\": \"ninja-multi-vcpkg-portable\",\n      \"configuration\": \"RelWithDebInfo\"\n    }\n  ],\n  \"testPresets\": [\n    {\n      \"name\": \"ninja-multi-vcpkg-test-debug\",\n      \"configurePreset\": \"ninja-multi-vcpkg-test\",\n      \"configuration\": \"Debug\",\n      \"output\": {\n        \"outputOnFailure\": true\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "COPYING",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n"
  },
  {
    "path": "LICENSES/GPL-3.0-or-later.txt",
    "content": "GNU GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007\n\nCopyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>\n\nEveryone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.\n\nPreamble\n\nThe GNU General Public License is a free, copyleft license for software and other kinds of works.\n\nThe licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.\n\nWhen we speak of free software, we are referring to freedom, 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 them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.\n\nTo protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.\n\nFor example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.\n\nDevelopers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.\n\nFor the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.\n\nSome devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.\n\nFinally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.\n\nThe precise terms and conditions for copying, distribution and modification follow.\n\nTERMS AND CONDITIONS\n\n0. Definitions.\n\n“This License” refers to version 3 of the GNU General Public License.\n\n“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.\n\n“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.\n\nTo “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.\n\nA “covered work” means either the unmodified Program or a work based on the Program.\n\nTo “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.\n\nTo “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.\n\nAn interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.\n\n1. Source Code.\nThe “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.\n\nA “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.\n\nThe “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.\n\nThe “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.\n\nThe Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.\n\nThe Corresponding Source for a work in source code form is that same work.\n\n2. Basic Permissions.\nAll rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.\n\nYou may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.\n\nConveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.\n\n3. Protecting Users' Legal Rights From Anti-Circumvention Law.\nNo covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.\n\nWhen you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.\n\n4. Conveying Verbatim Copies.\nYou may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.\n\nYou may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.\n\n5. Conveying Modified Source Versions.\nYou may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:\n\n     a) The work must carry prominent notices stating that you modified it, and giving a relevant date.\n\n     b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”.\n\n     c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.\n\n     d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.\n\nA compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.\n\n6. Conveying Non-Source Forms.\nYou may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:\n\n     a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.\n\n     b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.\n\n     c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.\n\n     d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.\n\n     e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.\n\nA separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.\n\nA “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.\n\n“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.\n\nIf you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).\n\nThe requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.\n\nCorresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.\n\n7. Additional Terms.\n“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.\n\nWhen you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.\n\nNotwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:\n\n     a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or\n\n     b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or\n\n     c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or\n\n     d) Limiting the use for publicity purposes of names of licensors or authors of the material; or\n\n     e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or\n\n     f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.\n\nAll other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.\n\nIf you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.\n\nAdditional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.\n\n8. Termination.\nYou may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).\n\nHowever, 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.\n\nMoreover, 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.\n\nTermination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.\n\n9. Acceptance Not Required for Having Copies.\nYou are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.\n\n10. Automatic Licensing of Downstream Recipients.\nEach time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.\n\nAn “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.\n\nYou may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.\n\n11. Patents.\nA “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.\n\nA contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.\n\nEach contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.\n\nIn the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.\n\nIf you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.\n\nIf, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.\n\nA patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.\n\nNothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.\n\n12. No Surrender of Others' Freedom.\nIf 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 convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.\n\n13. Use with the GNU Affero General Public License.\nNotwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.\n\n14. Revised Versions of this License.\nThe Free Software Foundation may publish revised and/or new versions of the GNU 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.\n\nEach version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.\n\nIf the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.\n\nLater license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.\n\n15. Disclaimer of Warranty.\nTHERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “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 PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n16. Limitation of Liability.\nIN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n17. Interpretation of Sections 15 and 16.\nIf the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.\n\nEND OF TERMS AND CONDITIONS\n\nHow to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.\n\n     <one line to give the program's name and a brief idea of what it does.>\n     Copyright (C) <year>  <name of author>\n\n     This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n\n     This program 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.\n\n     You should have received a copy of the GNU General Public License along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:\n\n     <program>  Copyright (C) <year>  <name of author>\n     This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n     This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.\n\nYou should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>.\n\nThe GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <https://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "LICENSES/LicenseRef-Kapeli.txt",
    "content": "These files are used under permission from Bogdan Popescu (https://github.com/Kapeli).\n\nSee https://github.com/Kapeli/Dash-X-Platform-Resources for more details.\n"
  },
  {
    "path": "LICENSES/MIT.txt",
    "content": "MIT License\n\nCopyright (c) Oleg Shparber, et al. <https://zealdocs.org>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and\nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the\nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial\nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT\nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO\nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "LICENSES/MS-RL.txt",
    "content": "Microsoft Reciprocal License (Ms-RL)\n\nThis license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software.\n\n1.  Definitions\nThe terms \"reproduce,\" \"reproduction,\" \"derivative works,\" and \"distribution\" have the same meaning here as under U.S. copyright law.\n\nA \"contribution\" is the original software, or any additions or changes to the software.\n\nA \"contributor\" is any person that distributes its contribution under this license.\n\n\"Licensed patents\" are a contributor's patent claims that read directly on its contribution.\n\n2.  Grant of Rights\n     (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.\n\n     (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.\n\n3.  Conditions and Limitations\n     (A) Reciprocal Grants- For any file you distribute that contains code from the software (in source code or binary format), you must provide recipients the source code to that file along with a copy of this license, which license will govern that file. You may license other files that are entirely your own work and do not contain code from the software under any terms you choose.\n\n     (B) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.\n\n     (C) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.\n\n     (D) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.\n\n     (E) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.\n\n     (F) The software is licensed \"as-is.\" You bear the risk of using it. The contributors give no express warranties, guarantees, or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement.\n"
  },
  {
    "path": "README.md",
    "content": "# Zeal\n\n[![Changelog](https://img.shields.io/github/release/zealdocs/zeal.svg?style=flat-square)](https://github.com/zealdocs/zeal/releases)\n[![Gitter](https://img.shields.io/gitter/room/zealdocs/zeal.svg?style=flat-square)](https://gitter.im/zealdocs/zeal)\n[![IRC](https://img.shields.io/badge/chat-on%20irc-blue.svg?style=flat-square)](https://web.libera.chat/#zealdocs)\n[![Telegram Channel](https://img.shields.io/badge/follow-on%20telegram-179cde.svg?style=flat-square)](https://telegram.me/zealdocsapp)\n[![Twitter](https://img.shields.io/badge/follow-on%20twitter-1da1f2.svg?style=flat-square)](https://twitter.com/zealdocs)\n\n[![Build Check](https://img.shields.io/github/actions/workflow/status/zealdocs/zeal/build-check.yaml?style=flat-square)](https://github.com/zealdocs/zeal/actions/workflows/build-check.yaml)\n[![Coverity Scan](https://img.shields.io/coverity/scan/4271.svg?style=flat-square)](https://scan.coverity.com/projects/4271)\n\nZeal is a simple offline documentation browser inspired by [Dash](https://kapeli.com/dash).\n\n![Screenshot](https://github.com/zealdocs/zeal/assets/714940/e8443bb4-ccb9-469b-89d6-b5b3bfc7e239)\n\n## Download\n\nGet binary builds for Windows and Linux from the [download page](https://zealdocs.org/download.html).\n\n## How to use\n\nAfter installing Zeal go to `Tools->Docsets`, select the ones you want, and click the `Download` button.\n\n## How to compile\n\n### Build dependencies\n\n* [CMake](https://cmake.org/).\n* [Qt](https://www.qt.io/) version 5.15.2 or above. Required module: Qt WebEngine Widgets.\n* [libarchive](https://libarchive.org/).\n* [SQLite](https://sqlite.org/).\n* X11 platforms only: Qt X11 Extras and `xcb-util-keysyms`.\n\n### Build instructions\n\n```sh\ncmake -B build\ncmake --build build\n```\n\nMore detailed instructions are available in the [wiki](https://github.com/zealdocs/zeal/wiki).\n\n## Query & Filter docsets\n\nYou can limit the search scope by using ':' to indicate the desired docsets:\n\n`java:BaseDAO`\n\nYou can also search multiple docsets separating them with a comma:\n\n`python,django:string`\n\n## Command line\n\nIf you prefer, you can start Zeal with a query from the command line:\n\n`zeal python:pprint`\n\n## Create your own docsets\n\nFollow instructions in the [Dash docset generation guide](https://kapeli.com/docsets).\n\n## Contact and Support\n\nWe want your feedback! Here's a list of different ways to contact developers and request help:\n\n* Report bugs and submit feature requests to [GitHub issues](https://github.com/zealdocs/zeal/issues).\n* Reach developers and other Zeal users in `#zealdocs` IRC channel on [Libera Chat](https://libera.chat) ([web client](https://web.libera.chat/#zealdocs)).\n* Ask any questions in our [GitHub discussions](https://github.com/zealdocs/zeal/discussions).\n* Do not forget to follow [@zealdocs](https://twitter.com/zealdocs) on Twitter!\n* Finally, for private communication shoot an email to <support@zealdocs.org>.\n\n## License\n\nThis software is licensed under the terms of the GNU General Public License version 3 (GPLv3) or later. Full text of the license is available in the [COPYING](COPYING) file and [online](https://www.gnu.org/licenses/gpl-3.0.html).\n"
  },
  {
    "path": "REUSE.toml",
    "content": "version = 1\n\n# GPL-3.0-or-later for the main source code and assets.\n[[annotations]]\npath = [\n    \"assets/**/*\",\n    \"cmake/*\",\n    \"pkg/**/*\",\n    \"README.md\",\n    \"src/**/*.ui\",\n    \"src/app/resources/browser/*.html\",\n    \"src/app/resources/browser/assets/css/highlight.css\",\n    \"src/app/resources/browser/assets/css/welcome.css\",\n    \"src/app/resources/icons/**/*\",\n    \"src/app/resources/zeal.*\",\n    \"src/app/zeal.qrc\",\n\n    # Windows CMake files.\n    # CMake and vcpkg files.\n    \"**/*.cmake\",\n    \"**/CMakeLists.txt\",\n    \"CMakePresets.json\",\n    \"vcpkg.json\",\n]\nSPDX-FileCopyrightText = \"Oleg Shparber, et al. <https://zealdocs.org>\"\nSPDX-License-Identifier = \"GPL-3.0-or-later\"\n\n# MIT for auxiliary files.\n[[annotations]]\npath = [\n    \".editorconfig\",\n    \".gitattributes\",\n    \".github/**/*\",\n    \".gitignore\",\n    \"CHANGELOG.md\",\n]\nSPDX-FileCopyrightText = \"Oleg Shparber, et al. <https://zealdocs.org>\"\nSPDX-License-Identifier = \"MIT\"\n\n[[annotations]]\npath = \"src/app/resources/browser/assets/css/oat.min.css\"\nSPDX-FileCopyrightText = \"Copyright (c) Kailash Nadh <https://github.com/knadh/oat>\"\nSPDX-License-Identifier = \"MIT\"\n\n[[annotations]]\npath = \"src/contrib/cpp-httplib/httplib.h\"\nSPDX-FileCopyrightText = \"Copyright (c) 2023 Yuji Hirose. All rights reserved.\"\nSPDX-License-Identifier = \"MIT\"\n\n[[annotations]]\npath = \"src/app/resources/icons/type/*.png\"\nSPDX-FileCopyrightText = \"Bogdan Popescu, et al. <https://github.com/Kapeli/Dash-X-Platform-Resources>\"\nSPDX-License-Identifier = \"LicenseRef-Kapeli\"\n"
  },
  {
    "path": "assets/CMakeLists.txt",
    "content": "add_subdirectory(freedesktop)\n"
  },
  {
    "path": "assets/freedesktop/CMakeLists.txt",
    "content": "if(UNIX AND NOT APPLE)\n    find_package(ECM 1.0.0 REQUIRED NO_MODULE)\n    set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})\n    include(ECMInstallIcons)\n\n    if(QT_VERSION_MAJOR EQUAL 5)\n        include(KDEInstallDirs)\n    else()\n        # Workaround until KDEInstallDirs6 is ready to use.\n        include(GNUInstallDirs)\n        set(KDE_INSTALL_APPDIR \"${CMAKE_INSTALL_DATAROOTDIR}/applications\")\n        set(KDE_INSTALL_ICONDIR \"${CMAKE_INSTALL_DATAROOTDIR}/icons\")\n        set(KDE_INSTALL_METAINFODIR \"${CMAKE_INSTALL_DATAROOTDIR}/metainfo\")\n    endif()\n\n    ecm_install_icons(ICONS \"16-apps-zeal.png\"\n                            \"24-apps-zeal.png\"\n                            \"32-apps-zeal.png\"\n                            \"64-apps-zeal.png\"\n                            \"128-apps-zeal.png\"\n                      DESTINATION ${KDE_INSTALL_ICONDIR}\n    )\n\n# For development builds insert an extra release in the AppStream metadata.\nif(NOT ZEAL_RELEASE_BUILD)\n    string(TIMESTAMP ZEAL_APPSTREAM_DEV_RELEASE \"\\n    <release date=\\\"%Y-%m-%d\\\" version=\\\"${ZEAL_VERSION_FULL}\\\" type=\\\"development\\\" />\")\nendif()\n\n    configure_file(\n        org.zealdocs.zeal.appdata.xml.in\n        org.zealdocs.zeal.appdata.xml\n    )\n\n    install(FILES ${CMAKE_BINARY_DIR}/assets/freedesktop/org.zealdocs.zeal.appdata.xml\n            DESTINATION ${KDE_INSTALL_METAINFODIR}\n    )\n\n    install(FILES \"org.zealdocs.zeal.desktop\"\n            DESTINATION ${KDE_INSTALL_APPDIR}\n    )\nendif()\n"
  },
  {
    "path": "assets/freedesktop/org.zealdocs.zeal.appdata.xml.in",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<component type=\"desktop\">\n  <id>org.zealdocs.zeal</id>\n  <launchable type=\"desktop-id\">org.zealdocs.zeal.desktop</launchable>\n  <name>Zeal</name>\n  <metadata_license>CC0-1.0</metadata_license>\n  <project_license>GPL-3.0-or-later</project_license>\n  <developer id=\"org.zealdocs\">\n    <name>Oleg Shparber</name>\n  </developer>\n  <summary>Documentation browser</summary>\n  <description>\n    <p>Zeal is a simple offline documentation browser inspired by Dash. It offers access to over 200 docsets covering various libraries and APIs.</p>\n  </description>\n  <categories>\n    <category>Development</category>\n  </categories>\n  <url type=\"homepage\">https://zealdocs.org/</url>\n  <url type=\"bugtracker\">https://github.com/zealdocs/zeal/issues</url>\n  <url type=\"help\">https://zealdocs.org/usage.html</url>\n  <url type=\"contact\">https://go.zealdocs.org/l/contact</url>\n  <screenshots>\n    <screenshot type=\"default\">\n      <caption>The main window</caption>\n      <image>https://i.imgur.com/FvGEguY.png</image>\n    </screenshot>\n  </screenshots>\n  <provides>\n    <id>zeal.desktop</id>\n  </provides>\n  <releases>@ZEAL_APPSTREAM_DEV_RELEASE@\n    <release date=\"2025-02-27\" version=\"0.8.0\" type=\"stable\">\n      <url>https://github.com/zealdocs/zeal/releases/tag/v0.8.0</url>\n    </release>\n    <release date=\"2024-09-08\" version=\"0.7.2\" type=\"stable\">\n      <url>https://github.com/zealdocs/zeal/releases/tag/v0.7.2</url>\n    </release>\n    <release date=\"2024-05-27\" version=\"0.7.1\" type=\"stable\">\n      <url>https://github.com/zealdocs/zeal/releases/tag/v0.7.1</url>\n    </release>\n    <release date=\"2023-09-20\" version=\"0.7.0\" type=\"stable\">\n      <url>https://github.com/zealdocs/zeal/releases/tag/v0.7.0</url>\n    </release>\n    <release date=\"2018-09-28\" version=\"0.6.1\" type=\"stable\">\n      <url>https://github.com/zealdocs/zeal/releases/tag/v0.6.1</url>\n    </release>\n  </releases>\n  <update_contact>support@zealdocs.org</update_contact>\n  <content_rating type=\"oars-1.1\" />\n</component>\n"
  },
  {
    "path": "assets/freedesktop/org.zealdocs.zeal.desktop",
    "content": "[Desktop Entry]\nVersion=1.0\nName=Zeal\nGenericName=Documentation Browser\nComment=Simple API documentation browser\nExec=zeal %u\nIcon=zeal\nTerminal=false\nType=Application\nCategories=Development;Documentation;\nMimeType=x-scheme-handler/dash;x-scheme-handler/dash-plugin;\nStartupWMClass=Zeal\n"
  },
  {
    "path": "cmake/CodeSign.cmake",
    "content": "#\n# CodeSign.cmake - CMake helper for signing Windows executables\n#\n# SPDX-FileCopyrightText: Oleg Shparber, et al. <https://zealdocs.org>\n# SPDX-License-Identifier: MIT\n#\n\ninclude_guard()\n\n# codesign(FILES <files>...\n#          [DESCRIPTION] <description-string>\n#          [URL] <url-string>\n#          [CERTIFICATE_FILE] <filename>\n#          [PASSWORD] <password-string>\n#          [TIMESTAMP_URL] <url-string>\n#          [QUIET]\n#          [VERBOSE]\n#          [DEBUG])\nfunction(codesign)\n    # Cleans up temporary files created during signing.\n    macro(_cleanup)\n        if(DEFINED _certificate_file)\n            file(REMOVE ${_certificate_file})\n        endif()\n    endmacro()\n\n    # Sets '_certificate_file' variable to a temporary file path.\n    macro(_set_temporary_certificate_file)\n        # Determine temporary file location. Try to keep it local to the build.\n        if(CMAKE_BINARY_DIR)\n            set(_temp_path ${CMAKE_BINARY_DIR})\n        elseif(CPACK_TEMPORARY_DIRECTORY)\n            set(_temp_path ${CPACK_TEMPORARY_DIRECTORY})\n        else()\n            set(_temp_path $ENV{TEMP})\n        endif()\n\n        set(_certificate_file \"${_temp_path}/codesign.tmp\")\n\n        # Remove file if left from previous run.\n        _cleanup()\n    endmacro()\n\n    if(NOT WIN32)\n        message(FATAL_ERROR \"Code signing is only supported on Windows.\")\n    endif()\n\n    cmake_parse_arguments(_ARG\n        \"QUIET;VERBOSE;DEBUG\"                                     # Options.\n        \"DESCRIPTION;URL;CERTIFICATE_FILE;PASSWORD;TIMESTAMP_URL\" # Single-value keywords.\n        \"FILES\"                                                   # Multi-value keywords.\n        ${ARGN}\n    )\n\n    if(NOT _ARG_FILES)\n        message(FATAL_ERROR \"FILES argument is required.\")\n    endif()\n\n    # Find signtool executable.\n    # TODO: Add option for path to signtool.exe.\n\n    # Add Windows 10 SDK paths.\n    get_filename_component(_w10sdk_root_path\n        \"[HKEY_LOCAL_MACHINE\\\\SOFTWARE\\\\Microsoft\\\\Windows Kits\\\\Installed Roots;KitsRoot10]\"\n        ABSOLUTE CACHE\n    )\n    if(_w10sdk_root_path)\n        file(GLOB _w10sdk_paths \"${_w10sdk_root_path}/bin/10.*\")\n        list(REVERSE _w10sdk_paths) # Newest version first.\n\n        # Detect target architecture.\n        # https://learn.microsoft.com/en-us/windows/win32/winprog64/wow64-implementation-details#environment-variables\n        if(CMAKE_SYSTEM_PROCESSOR STREQUAL \"AMD64\")\n            set(_w10sdk_arch \"x64\")\n        elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL \"X86\")\n            set(_w10sdk_arch \"x86\")\n        elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL \"ARM64\")\n            set(_w10sdk_arch \"arm64\")\n        else()\n            message(WARNING \"Unknown architecture: ${CMAKE_SYSTEM_PROCESSOR}.\")\n        endif()\n    endif()\n\n    # TODO: Add microsoft.windows.sdk.buildtools path.\n\n    find_program(_cmd\n        NAMES signtool\n        PATHS ${_w10sdk_paths}\n        PATH_SUFFIXES ${_w10sdk_arch}\n    )\n\n    if(NOT _cmd)\n        message(NOTICE \"signtool.exe was not found, no binaries will be signed.\")\n        return()\n    endif()\n\n    message(DEBUG \"Found signtool.exe: ${_cmd}\")\n\n    # Start constructing command.\n    set(_cmd_args \"sign\")\n    list(APPEND _cmd_args \"/fd\" \"sha256\")\n\n    # Set certificate file.\n    if(NOT _ARG_CERTIFICATE_FILE)\n        if(CODESIGN_CERTIFICATE_FILE)\n            if(NOT EXISTS ${CODESIGN_CERTIFICATE_FILE})\n                message(NOTICE \"Certificate file '${CODESIGN_CERTIFICATE_FILE}' does not exist.\")\n                return()\n            endif()\n\n            set(_ARG_CERTIFICATE_FILE ${CODESIGN_CERTIFICATE_FILE})\n        elseif(DEFINED ENV{CODESIGN_CERTIFICATE_FILE})\n            if(\"$ENV{CODESIGN_CERTIFICATE_FILE}\" STREQUAL \"\")\n                message(NOTICE \"CODESIGN_CERTIFICATE_FILE is set to an empty string.\")\n                return()\n            endif()\n\n            if(NOT EXISTS $ENV{CODESIGN_CERTIFICATE_FILE})\n                message(NOTICE \"Certificate file '$ENV{CODESIGN_CERTIFICATE_FILE}' (set in CODESIGN_CERTIFICATE_FILE) does not exist.\")\n                return()\n            endif()\n\n            set(_ARG_CERTIFICATE_FILE $ENV{CODESIGN_CERTIFICATE_FILE})\n        elseif(DEFINED ENV{CODESIGN_CERTIFICATE})\n            if(\"$ENV{CODESIGN_CERTIFICATE}\" STREQUAL \"\")\n                message(NOTICE \"CODESIGN_CERTIFICATE is set to an empty string.\")\n                return()\n            endif()\n\n            # Store certificate value in a temporary file for signtool to use.\n            _set_temporary_certificate_file()\n            file(WRITE ${_certificate_file} $ENV{CODESIGN_CERTIFICATE})\n            set(_ARG_CERTIFICATE_FILE ${_certificate_file})\n        elseif(DEFINED ENV{CODESIGN_CERTIFICATE_BASE64})\n            if(\"$ENV{CODESIGN_CERTIFICATE_BASE64}\" STREQUAL \"\")\n                message(NOTICE \"CODESIGN_CERTIFICATE_BASE64 is set to an empty string.\")\n                return()\n            endif()\n\n            # Read base64-encoded certificate from environment variable,\n            # decode with `certutil.exe`, and store in a temporary file\n            # for signtool to use.\n            #\n            # This is useful for GitHub Actions, which cannot handle unencoded\n            # multiline secrets.\n\n            _set_temporary_certificate_file()\n\n            # Save base64-encoded certificate to file.\n            set(_certificate_base64_file \"${_certificate_file}.base64\")\n            file(WRITE ${_certificate_base64_file} $ENV{CODESIGN_CERTIFICATE_BASE64})\n\n            # Decode certificate.\n            set(_cmd_certutil_args \"-decode\" ${_certificate_base64_file} ${_certificate_file})\n            execute_process(COMMAND \"certutil.exe\" ${_cmd_certutil_args}\n                RESULT_VARIABLE _rc\n                OUTPUT_VARIABLE _stdout\n                # For some reason certutil prints errors to stdout.\n                # ERROR_VARIABLE  _stderr\n            )\n\n            # Remove temporary file first.\n            file(REMOVE ${_certificate_base64_file})\n\n            if(NOT _rc EQUAL 0)\n                message(WARNING \"Failed to decode certificate: ${_stdout}\")\n                _cleanup()\n                return()\n            endif()\n\n            unset(_rc)\n            unset(_stdout)\n\n            set(_ARG_CERTIFICATE_FILE ${_certificate_file})\n        else()\n            message(NOTICE \"Certificate is not provided, no binaries will be signed.\")\n            return()\n        endif()\n    endif()\n\n    list(APPEND _cmd_args \"/f\" ${_ARG_CERTIFICATE_FILE})\n\n    # Set password.\n    if(NOT _ARG_PASSWORD)\n        if(CODESIGN_PASSWORD)\n            set(_ARG_PASSWORD ${CODESIGN_PASSWORD})\n        elseif(DEFINED ENV{CODESIGN_PASSWORD})\n            if(\"$ENV{CODESIGN_PASSWORD}\" STREQUAL \"\")\n                message(NOTICE \"CODESIGN_PASSWORD is set to an empty string. Unset if not used.\")\n                _cleanup()\n                return()\n            endif()\n\n            set(_ARG_PASSWORD $ENV{CODESIGN_PASSWORD})\n        endif()\n    endif()\n\n    if(_ARG_PASSWORD)\n        list(APPEND _cmd_args \"/p\" ${_ARG_PASSWORD})\n    endif()\n\n    # Set description.\n    if(NOT _ARG_DESCRIPTION AND PROJECT_DESCRIPTION)\n        set(_ARG_DESCRIPTION ${PROJECT_DESCRIPTION})\n    endif()\n\n    if(_ARG_DESCRIPTION)\n        list(APPEND _cmd_args \"/d\" ${_ARG_DESCRIPTION})\n    endif()\n\n    # Set project URL.\n    if(NOT _ARG_URL AND PROJECT_HOMEPAGE_URL)\n        set(_ARG_URL ${PROJECT_HOMEPAGE_URL})\n    endif()\n\n    if(_ARG_URL)\n        list(APPEND _cmd_args \"/du\" ${_ARG_URL})\n    endif()\n\n    # Set timestamp server.\n    if(NOT _ARG_TIMESTAMP_URL)\n        set(_ARG_TIMESTAMP_URL \"http://timestamp.digicert.com\")\n    endif()\n\n    if(_ARG_TIMESTAMP_URL)\n        list(APPEND _cmd_args \"/tr\" ${_ARG_TIMESTAMP_URL} \"/td\" \"sha256\")\n    endif()\n\n    # Set quiet, verbose, or debug options.\n    if(_ARG_QUIET)\n        list(APPEND _cmd_args \"/q\")\n    endif()\n\n    if(_ARG_VERBOSE)\n        list(APPEND _cmd_args \"/v\")\n    endif()\n\n    if(_ARG_DEBUG)\n        list(APPEND _cmd_args \"/debug\")\n    endif()\n\n    foreach(_file ${_ARG_FILES})\n        if(NOT EXISTS ${_file})\n            message(NOTICE \"Cannot find file to sign: ${_file}\")\n            continue()\n        endif()\n\n        message(STATUS \"Signing ${_file}...\")\n        execute_process(\n            COMMAND \"${_cmd}\" ${_cmd_args} \"${_file}\"\n            RESULT_VARIABLE _rc\n            OUTPUT_VARIABLE _stdout\n            ERROR_VARIABLE  _stderr\n        )\n\n        if(_rc EQUAL 0)\n            message(STATUS \"Successfully signed: ${_file}\")\n        else()\n            message(NOTICE \"Failed to sign: ${_stderr}\")\n\n            if(NOT _ARG_QUIET)\n                message(VERBOSE ${_stdout})\n            endif()\n        endif()\n    endforeach()\n\n    _cleanup()\nendfunction()\n"
  },
  {
    "path": "cmake/GetVersionFromGit.cmake",
    "content": "#\n# GetVersionFromGit.cmake - CMake helper for getting version information from Git\n#\n# SPDX-FileCopyrightText: Oleg Shparber, et al. <https://zealdocs.org>\n# SPDX-License-Identifier: MIT\n#\n# Based on https://github.com/fakenmc/cmake-git-semver by Nuno Fachada.\n# This module is public domain, use it as it fits you best.\n#\n# This cmake module sets the project version and partial version\n# variables by analysing the git tag and commit history. It expects git\n# tags defined with semantic versioning 2.0.0 (http://semver.org/).\n#\n# The module expects the PROJECT_NAME variable to be set, and recognizes\n# the GIT_FOUND, GIT_EXECUTABLE and VERSION_UPDATE_FROM_GIT variables.\n# If Git is found and VERSION_UPDATE_FROM_GIT is set to boolean TRUE,\n# the project version will be updated using information fetched from the\n# most recent git tag and commit. Otherwise, the module will try to read\n# a VERSION file containing the full and partial versions. The module\n# will update this file each time the project version is updated.\n#\n# Once done, this module will define the following variables:\n#\n# ${PROJECT_NAME}_GIT_VERSION_STRING - Version string without metadata\n# such as \"v2.0.0\" or \"v1.2.41-beta.1\". This should correspond to the\n# most recent git tag.\n# ${PROJECT_NAME}_GIT_VERSION_STRING_FULL - Version string with metadata\n# such as \"v2.0.0+3.a23fbc\" or \"v1.3.1-alpha.2+4.9c4fd1\"\n# ${PROJECT_NAME}_GIT_VERSION_MAJOR - Major version integer (e.g. 2 in v2.3.1-RC.2+21.ef12c8)\n# ${PROJECT_NAME}_GIT_VERSION_MINOR - Minor version integer (e.g. 3 in v2.3.1-RC.2+21.ef12c8)\n# ${PROJECT_NAME}_GIT_VERSION_PATCH - Patch version integer (e.g. 1 in v2.3.1-RC.2+21.ef12c8)\n# ${PROJECT_NAME}_GIT_VERSION_TWEAK - Tweak version string (e.g. \"RC.2\" in v2.3.1-RC.2+21.ef12c8)\n# ${PROJECT_NAME}_GIT_VERSION_AHEAD - How many commits ahead of last tag (e.g. 21 in v2.3.1-RC.2+21.ef12c8)\n# ${PROJECT_NAME}_GIT_VERSION_SHA - The git sha1 of the most recent commit (e.g. the \"ef12c8\" in v2.3.1-RC.2+21.ef12c8)\n# Only if VERSION_UPDATE_FROM_GIT is TRUE:\n# ${PROJECT_NAME}_VERSION - Same as ${PROJECT_NAME}_GIT_VERSION_STRING,\n# without the preceding 'v', e.g. \"2.0.0\" or \"1.2.41-beta.1\"\n\n# Check if .git directory is present.\nif(NOT IS_DIRECTORY \"${CMAKE_SOURCE_DIR}/.git\")\n    message(NOTICE \"Cannot find Git metadata, using static version string.\")\n    return()\nendif()\n\n# Check if Git executable is present.\nfind_package(Git)\nif(NOT GIT_FOUND)\n    message(NOTICE \"Cannot find Git executable, using static version string.\")\n    return()\nendif()\n\n# Check if Git executable version is >= 2.15. Required for --is-shallow-repository argument.\n# See https://stackoverflow.com/a/37533086.\nif(GIT_VERSION_STRING VERSION_LESS \"2.15\")\n    message(NOTICE \"Git executable is too old (< 2.15), using static version string.\")\n    return()\nendif()\n\n# Detect shallow clone.\nexecute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --is-shallow-repository\n    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}\n    RESULT_VARIABLE IS_SHALLOW_RESULT\n    OUTPUT_VARIABLE IS_SHALLOW_OUTPUT\n    OUTPUT_STRIP_TRAILING_WHITESPACE\n    ERROR_QUIET)\nif(IS_SHALLOW_RESULT AND NOT IS_SHALLOW_RESULT EQUAL 0)\n    message(NOTICE \"Cannot perform shallow clone detection, using static version string.\")\n    unset(IS_SHALLOW_RESULT)\n    unset(IS_SHALLOW_OUTPUT)\n    return()\nendif()\n\nunset(IS_SHALLOW_RESULT)\n\nif(NOT \"${IS_SHALLOW_OUTPUT}\" STREQUAL \"false\")\n    message(NOTICE \"Shallow clone detected, using static version string.\")\n    unset(IS_SHALLOW_OUTPUT)\n    return()\nendif()\n\nunset(IS_SHALLOW_OUTPUT)\n\n# Get last tag from git\nexecute_process(COMMAND ${GIT_EXECUTABLE} describe --abbrev=0 --tags\n    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}\n    OUTPUT_VARIABLE ${PROJECT_NAME}_GIT_VERSION_STRING\n    OUTPUT_STRIP_TRAILING_WHITESPACE)\n\n# How many commits since the last tag\nexecute_process(COMMAND ${GIT_EXECUTABLE} rev-list ${${PROJECT_NAME}_GIT_VERSION_STRING}^..HEAD --count\n    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}\n    OUTPUT_VARIABLE ${PROJECT_NAME}_GIT_VERSION_AHEAD\n    OUTPUT_STRIP_TRAILING_WHITESPACE)\n\n# Get current commit SHA from git\nexecute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD\n    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}\n    OUTPUT_VARIABLE ${PROJECT_NAME}_GIT_VERSION_SHA\n    OUTPUT_STRIP_TRAILING_WHITESPACE)\n\n# Get partial versions into a list\nstring(REGEX MATCHALL \"-.*$|[0-9]+\" ${PROJECT_NAME}_PARTIAL_VERSION_LIST\n    ${${PROJECT_NAME}_GIT_VERSION_STRING})\n\n# Set the version numbers\nlist(GET ${PROJECT_NAME}_PARTIAL_VERSION_LIST\n    0 ${PROJECT_NAME}_GIT_VERSION_MAJOR)\nlist(GET ${PROJECT_NAME}_PARTIAL_VERSION_LIST\n    1 ${PROJECT_NAME}_GIT_VERSION_MINOR)\nlist(GET ${PROJECT_NAME}_PARTIAL_VERSION_LIST\n    2 ${PROJECT_NAME}_GIT_VERSION_PATCH)\n\n# Calculate next patch version.\nmath(EXPR ${PROJECT_NAME}_GIT_VERSION_PATCH_NEXT ${${PROJECT_NAME}_GIT_VERSION_PATCH}+1)\n\n# The tweak part is optional, so check if the list contains it\nlist(LENGTH ${PROJECT_NAME}_PARTIAL_VERSION_LIST\n    ${PROJECT_NAME}_PARTIAL_VERSION_LIST_LEN)\nif (${PROJECT_NAME}_PARTIAL_VERSION_LIST_LEN GREATER 3)\n    list(GET ${PROJECT_NAME}_PARTIAL_VERSION_LIST 3 ${PROJECT_NAME}_GIT_VERSION_TWEAK)\n    string(SUBSTRING ${${PROJECT_NAME}_GIT_VERSION_TWEAK} 1 -1 ${PROJECT_NAME}_GIT_VERSION_TWEAK)\nendif()\n\n# Unset the list\nunset(${PROJECT_NAME}_PARTIAL_VERSION_LIST)\n\n# Set full project version string\nset(${PROJECT_NAME}_GIT_VERSION_STRING_FULL\n    ${${PROJECT_NAME}_GIT_VERSION_STRING}+${${PROJECT_NAME}_GIT_VERSION_AHEAD}.${${PROJECT_NAME}_GIT_VERSION_SHA})\n\nif(VERSION_UPDATE_FROM_GIT)\n    # Set project version (without the preceding 'v')\n    set(${PROJECT_NAME}_VERSION ${${PROJECT_NAME}_GIT_VERSION_MAJOR}.${${PROJECT_NAME}_GIT_VERSION_MINOR}.${${PROJECT_NAME}_GIT_VERSION_PATCH})\n    if (${PROJECT_NAME}_GIT_VERSION_TWEAK)\n        set(${PROJECT_NAME}_VERSION ${${PROJECT_NAME}_VERSION}-${${PROJECT_NAME}_GIT_VERSION_TWEAK})\n    endif()\nendif()\n"
  },
  {
    "path": "cmake/MacOSXBundleInfo.plist.in",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>CFBundleDevelopmentRegion</key>\n    <string>English</string>\n    <key>CFBundleExecutable</key>\n    <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>\n    <key>CFBundleIconFile</key>\n    <string>${MACOSX_BUNDLE_ICON_FILE}</string>\n    <key>CFBundleIdentifier</key>\n    <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>\n    <key>CFBundleInfoDictionaryVersion</key>\n    <string>6.0</string>\n    <key>CFBundleLongVersionString</key>\n    <string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>\n    <key>CFBundleName</key>\n    <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>\n    <key>CFBundlePackageType</key>\n    <string>APPL</string>\n    <key>CFBundleShortVersionString</key>\n    <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>\n    <key>CFBundleSignature</key>\n    <string>????</string>\n    <key>CFBundleVersion</key>\n    <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>\n    <key>CSResourcesFileMapped</key>\n    <true/>\n    <key>NSHumanReadableCopyright</key>\n    <string>${MACOSX_BUNDLE_COPYRIGHT}</string>\n    <key>NSPrincipalClass</key>\n    <string>NSApplication</string>\n    <key>NSHighResolutionCapable</key>\n    <true/>\n    <key>NSSupportsAutomaticGraphicsSwitching</key>\n    <true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "pkg/appimage/README.md",
    "content": "# AppImage Package\n\n## Local Testing\n\nRun Docker container:\n\n```shell\ndocker run -it --rm -v $(pwd):/src --entrypoint /bin/bash ubuntu:jammy\n```\n\nInstall `appimage-builder` and `appimagetool` dependencies:\n\n```shell\napt-get update -q -y\nDEBIAN_FRONTEND=\"noninteractive\" apt-get install -q -y --no-install-recommends appstream curl desktop-file-utils fakeroot file git gnupg patchelf squashfs-tools zsync python3-pip python3-setuptools python3-wheel\n```\n\nInstall appimagetool, it has to be extracted because FUSE doesn't work in containers without extra fiddling.\n\n```shell\ncd /tmp\ncurl -sLO https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage\nchmod +x appimagetool-x86_64.AppImage\n./appimagetool-x86_64.AppImage --appimage-extract\nmv squashfs-root/ /opt/appimagetool.AppDir\nln -s /opt/appimagetool.AppDir/AppRun /usr/local/bin/appimagetool\ncd -\n```\n\nInstall appimage-builder.\n\n```shell\npip3 install git+https://github.com/AppImageCrafters/appimage-builder.git@669213cb730e007d5b316ed19b39691fbdcd41c4\n```\n\nInstall build dependencies:\n\n```shell\napt-get install -q -y --no-install-recommends build-essential cmake extra-cmake-modules libappindicator-dev libarchive-dev libqt5x11extras5-dev libsqlite3-dev libxcb-keysyms1-dev ninja-build qtbase5-dev qtwebengine5-dev\n```\n\nRun `appimage-builder`:\n\n```shell\ncd /src\nappimage-builder --skip-test --build-dir build.appimage --appdir build.appimage/AppDir --recipe pkg/appimage/appimage-amd64.yaml\n```\n"
  },
  {
    "path": "pkg/appimage/appimage-amd64.yaml",
    "content": "version: 1\n\nscript:\n  - cmake -B $BUILD_DIR/cmake-build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo\n  - cmake --build $BUILD_DIR/cmake-build\n  - cmake --install $BUILD_DIR/cmake-build --prefix $TARGET_APPDIR/usr\n  - appstreamcli validate $TARGET_APPDIR/usr/share/metainfo/org.zealdocs.zeal.appdata.xml\n\nAppDir:\n  app_info:\n    id: org.zealdocs.zeal\n    name: zeal\n    icon: zeal\n    version: 0.8.0 # TODO: Use version from CMake.\n    exec: usr/bin/zeal\n    exec_args: $@\n\n  runtime:\n    env:\n      APPDIR_LIBRARY_PATH: $APPDIR/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu\n      QTWEBENGINE_DISABLE_SANDBOX: 1\n\n  apt:\n    arch: amd64\n    sources:\n      - sourceline: deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted universe multiverse\n        key_url: https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920d1991bc93c\n      - sourceline: deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted universe multiverse\n      - sourceline: deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security main restricted universe multiverse\n      - sourceline: deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse\n    include:\n      # Required Qt packages.\n      - libqt5concurrent5\n      - libqt5gui5\n      - libqt5network5\n      - libqt5webchannel5\n      - libqt5webengine5\n      - libqt5webenginewidgets5\n      - libqt5widgets5\n      - libqt5x11extras5\n      - qt5-gtk-platformtheme\n      - qtwayland5\n      # Other dependencies.\n      - libsqlite3-0\n      - libarchive13\n      - libfontconfig1\n      - libfreetype6\n    exclude:\n      - \"*dbgsym*\"\n      - adwaita-icon-theme\n      - dconf-service\n      - gcc-*\n      - gnupg\n      - humanity-icon-theme\n      - libsystemd0\n      - libwacom*\n      - perl\n      - perl-*\n      - sound-theme-freedesktop\n      - systemd\n      - systemd-*\n\n  files:\n    exclude:\n      - etc/systemd\n      - lib/systemd\n      - usr/bin/*-linux-gnu-*\n      - usr/bin/dpkg*\n      - usr/bin/systemd*\n      - usr/include\n      - usr/lib/x86_64-linux-gnu/gconv\n      - usr/share/doc\n      - usr/share/man\n\nAppImage:\n  arch: x86_64\n  comp: zstd\n  sign-key: None\n  update-information: gh-releases-zsync|zealdocs|zeal|latest|zeal-*x86_64.AppImage.zsync\n"
  },
  {
    "path": "pkg/wix/cpack_post_build.cmake",
    "content": "if(CPACK_SOURCE_INSTALLED_DIRECTORIES)\n    message(DEBUG \"Skipping package signing for source package generator.\")\n    return()\nendif()\n\nif(NOT CPACK_GENERATOR STREQUAL \"WIX\")\n    message(DEBUG \"Skipping package signing for ${CPACK_GENERATOR} generator.\")\n    return()\nendif()\n\ninclude(CodeSign)\ncodesign(FILES ${CPACK_PACKAGE_FILES} QUIET)\n"
  },
  {
    "path": "pkg/wix/cpack_pre_build.cmake",
    "content": "if(CPACK_SOURCE_INSTALLED_DIRECTORIES)\n    message(DEBUG \"Skipping package signing for source package generator.\")\n    return()\nendif()\n\n# TODO: Automatically generate list.\nset(_file_list\n    \"zeal.exe\"\n    \"archive.dll\"\n    \"zlib1.dll\"\n    \"sqlite3.dll\"\n)\n\ninclude(CodeSign)\n\nforeach(_file ${_file_list})\n    codesign(FILES \"${CPACK_TEMPORARY_DIRECTORY}/${_file}\" QUIET)\nendforeach()\n"
  },
  {
    "path": "pkg/wix/exitdialog.wxs",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- SPDX-FileCopyrightText: Oleg Shparber, et al. <https://zealdocs.org>. -->\n<!-- SPDX-FileCopyrightText: .NET Foundation and contributors. All rights reserved. -->\n<!-- SPDX-License-Identifier: MS-RL -->\n<!-- Upstream: https://github.com/wixtoolset/wix3/blob/develop/src/ext/UIExtension/wixlib/ExitDialog.wxs -->\n\n<Wix xmlns=\"http://schemas.microsoft.com/wix/2006/wi\">\n    <Fragment>\n        <UI>\n            <Dialog Id=\"Zeal_ExitDialog\" Width=\"370\" Height=\"270\" Title=\"!(loc.ExitDialog_Title)\">\n                <Control Id=\"Finish\" Type=\"PushButton\" X=\"236\" Y=\"243\" Width=\"56\" Height=\"17\" Default=\"yes\" Cancel=\"yes\" Text=\"!(loc.WixUIFinish)\" />\n                <Control Id=\"Cancel\" Type=\"PushButton\" X=\"304\" Y=\"243\" Width=\"56\" Height=\"17\" Disabled=\"yes\" Text=\"!(loc.WixUICancel)\" />\n                <Control Id=\"Bitmap\" Type=\"Bitmap\" X=\"0\" Y=\"0\" Width=\"370\" Height=\"234\" TabSkip=\"no\" Text=\"!(loc.ExitDialogBitmap)\" />\n                <Control Id=\"Back\" Type=\"PushButton\" X=\"180\" Y=\"243\" Width=\"56\" Height=\"17\" Disabled=\"yes\" Text=\"!(loc.WixUIBack)\" />\n                <Control Id=\"BottomLine\" Type=\"Line\" X=\"0\" Y=\"234\" Width=\"370\" Height=\"0\" />\n                <Control Id=\"Description\" Type=\"Text\" X=\"135\" Y=\"70\" Width=\"220\" Height=\"40\" Transparent=\"yes\" NoPrefix=\"yes\" Text=\"!(loc.ExitDialogDescription)\" />\n                <Control Id=\"Title\" Type=\"Text\" X=\"135\" Y=\"20\" Width=\"220\" Height=\"60\" Transparent=\"yes\" NoPrefix=\"yes\" Text=\"!(loc.ExitDialogTitle)\" />\n\n                <!-- Customization for Zeal installer. -->\n                <Control Id=\"LaunchCheckBox\" Type=\"CheckBox\" X=\"20\" Y=\"243\" Width=\"100\" Height=\"17\" Property=\"LAUNCHAPPONEXIT\" Hidden=\"yes\" CheckBoxValue=\"1\" Text=\"Launch [ProductName]\">\n                    <Condition Action=\"show\">NOT Installed OR WIX_UPGRADE_DETECTED</Condition>\n                </Control>\n            </Dialog>\n\n            <InstallUISequence>\n                <Show Dialog=\"Zeal_ExitDialog\" OnExit=\"success\" Overridable=\"yes\" />\n            </InstallUISequence>\n\n            <AdminUISequence>\n                <Show Dialog=\"Zeal_ExitDialog\" OnExit=\"success\" Overridable=\"yes\" />\n            </AdminUISequence>\n        </UI>\n    </Fragment>\n</Wix>\n"
  },
  {
    "path": "pkg/wix/patch.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- SPDX-FileCopyrightText: Oleg Shparber, et al. <https://zealdocs.org>. -->\n<!-- SPDX-License-Identifier: GPL-3.0-or-later -->\n\n<CPackWiXPatch>\n    <CPackWiXFragment Id=\"#PRODUCT\">\n        <Condition Message=\"This package can only be installed on 64-bit versions of Windows.\"><![CDATA[VersionNT64]]></Condition>\n    </CPackWiXFragment>\n</CPackWiXPatch>\n"
  },
  {
    "path": "pkg/wix/template.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- SPDX-FileCopyrightText: Oleg Shparber, et al. <https://zealdocs.org>. -->\n<!-- SPDX-License-Identifier: GPL-3.0-or-later -->\n<!-- Based on: https://gitlab.kitware.com/cmake/cmake/-/blob/master/Utilities/Release/WiX/WIX.template.in -->\n\n<?include \"cpack_variables.wxi\"?>\n\n<?define AppExeName=\"zeal.exe\" ?>\n\n<Wix xmlns=\"http://schemas.microsoft.com/wix/2006/wi\"\n    xmlns:util=\"http://schemas.microsoft.com/wix/UtilExtension\"\n    RequiredVersion=\"3.6.3303.0\">\n\n    <Product Id=\"$(var.CPACK_WIX_PRODUCT_GUID)\"\n        Name=\"$(var.CPACK_PACKAGE_NAME)\"\n        Language=\"1033\"\n        Version=\"$(var.CPACK_PACKAGE_VERSION)\"\n        Manufacturer=\"$(var.CPACK_PACKAGE_VENDOR)\"\n        UpgradeCode=\"$(var.CPACK_WIX_UPGRADE_GUID)\">\n\n        <Package InstallScope=\"perMachine\" InstallerVersion=\"301\" Compressed=\"yes\"/>\n\n        <Media Id=\"1\" Cabinet=\"media1.cab\" EmbedCab=\"yes\"/>\n\n        <MajorUpgrade\n            Schedule=\"afterInstallInitialize\"\n            DowngradeErrorMessage=\"A later version of [ProductName] is already installed. Setup will now exit.\"/>\n\n        <WixVariable Id=\"WixUILicenseRtf\" Value=\"$(var.CPACK_WIX_LICENSE_RTF)\"/>\n        <Property Id=\"WIXUI_INSTALLDIR\" Value=\"INSTALL_ROOT\"/>\n\n        <?ifdef CPACK_WIX_PRODUCT_ICON?>\n        <Property Id=\"ARPPRODUCTICON\">ProductIcon.ico</Property>\n        <Icon Id=\"ProductIcon.ico\" SourceFile=\"$(var.CPACK_WIX_PRODUCT_ICON)\"/>\n        <?endif?>\n\n        <?ifdef CPACK_WIX_UI_BANNER?>\n        <WixVariable Id=\"WixUIBannerBmp\" Value=\"$(var.CPACK_WIX_UI_BANNER)\"/>\n        <?endif?>\n\n        <?ifdef CPACK_WIX_UI_DIALOG?>\n        <WixVariable Id=\"WixUIDialogBmp\" Value=\"$(var.CPACK_WIX_UI_DIALOG)\"/>\n        <?endif?>\n\n        <UI>\n            <UIRef Id=\"$(var.CPACK_WIX_UI_REF)\"/>\n            <Publish Dialog=\"Zeal_ExitDialog\" Control=\"Finish\" Order=\"1\" Event=\"DoAction\" Value=\"LaunchApplication\">LAUNCHAPPONEXIT</Publish>\n        </UI>\n\n        <?include \"properties.wxi\"?>\n        <?include \"product_fragment.wxi\"?>\n\n        <!-- Dash protocol handlers. -->\n        <Component Id=\"ProtocolHandlers\" Directory=\"INSTALL_ROOT\">\n            <?foreach UrlScheme in dash;dash-plugin ?>\n            <!-- Remove broken HKCU keys. -->\n            <RemoveRegistryKey Action=\"removeOnInstall\" Root=\"HKCU\" Key=\"Software\\Classes\\$(var.UrlScheme)\" />\n\n            <RegistryKey Root=\"HKLM\" Key=\"Software\\Classes\\$(var.UrlScheme)\">\n                <RegistryValue Type=\"string\" Value=\"URL:Dash Plugin Protocol (Zeal)\" />\n                <RegistryValue Name=\"URL Protocol\" Type=\"string\" Value=\"\" />\n\n                <RegistryKey Key=\"DefaultIcon\">\n                    <RegistryValue Type=\"string\" Value=\"&quot;[#CM_FP_zeal.exe],1&quot;\" />\n                </RegistryKey>\n\n                <RegistryKey Key=\"shell\\open\\command\">\n                    <RegistryValue Type=\"string\" Value=\"&quot;[#CM_FP_zeal.exe]&quot; &quot;%1&quot;\" />\n                </RegistryKey>\n            </RegistryKey>\n            <?endforeach ?>\n        </Component>\n\n        <!-- Custom properties to control installation options -->\n        <Property Id=\"LAUNCHAPPONEXIT\" Value=\"1\" Secure=\"yes\"/>\n\n        <!-- Set properties based on existing conditions, prevents changing state on upgrade. -->\n        <SetProperty Id=\"LicenseAccepted\" After=\"AppSearch\" Value=\"1\">WIX_UPGRADE_DETECTED</SetProperty>\n\n        <FeatureRef Id=\"ProductFeature\">\n            <ComponentRef Id=\"ProtocolHandlers\" />\n        </FeatureRef>\n\n        <!-- Action to launch application after installer exits. -->\n        <Property Id=\"WixShellExecTarget\" Value=\"[#CM_FP_zeal.exe]\"/>\n        <CustomAction Id=\"LaunchApplication\" BinaryKey=\"WixCA\" DllEntry=\"WixShellExec\" Impersonate=\"yes\"/>\n\n        <!-- Close running Zeal processes. -->\n        <util:CloseApplication CloseMessage=\"yes\" Target=\"$(var.AppExeName)\" ElevatedCloseMessage=\"yes\" RebootPrompt=\"no\" TerminateProcess=\"0\" />\n\n        <InstallExecuteSequence>\n            <Custom Action=\"WixCloseApplications\" Before=\"RemoveFiles\" />\n        </InstallExecuteSequence>\n    </Product>\n</Wix>\n"
  },
  {
    "path": "pkg/wix/ui.wxs",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- SPDX-FileCopyrightText: Oleg Shparber, et al. <https://zealdocs.org>. -->\n<!-- SPDX-FileCopyrightText: .NET Foundation and contributors. All rights reserved. -->\n<!-- SPDX-License-Identifier: MS-RL -->\n<!-- Upstream: https://github.com/wixtoolset/wix3/blob/develop/src/ext/UIExtension/wixlib/WixUI_InstallDir.wxs -->\n\n<!--\nFirst-time install dialog sequence:\n - WixUI_WelcomeDlg\n - WixUI_LicenseAgreementDlg\n - WixUI_InstallDirDlg\n - WixUI_VerifyReadyDlg\n - WixUI_DiskCostDlg\n\nMaintenance dialog sequence:\n - WixUI_MaintenanceWelcomeDlg\n - WixUI_MaintenanceTypeDlg\n - WixUI_InstallDirDlg\n - WixUI_VerifyReadyDlg\n\nPatch dialog sequence:\n - WixUI_WelcomeDlg\n - WixUI_VerifyReadyDlg\n\n-->\n\n<Wix xmlns=\"http://schemas.microsoft.com/wix/2006/wi\">\n    <Fragment>\n        <UI Id=\"Zeal_InstallDir\">\n            <TextStyle Id=\"WixUI_Font_Normal\" FaceName=\"Tahoma\" Size=\"8\" />\n            <TextStyle Id=\"WixUI_Font_Bigger\" FaceName=\"Tahoma\" Size=\"12\" />\n            <TextStyle Id=\"WixUI_Font_Title\" FaceName=\"Tahoma\" Size=\"9\" Bold=\"yes\" />\n\n            <Property Id=\"DefaultUIFont\" Value=\"WixUI_Font_Normal\" />\n            <Property Id=\"WixUI_Mode\" Value=\"InstallDir\" />\n\n            <DialogRef Id=\"BrowseDlg\" />\n            <DialogRef Id=\"DiskCostDlg\" />\n            <DialogRef Id=\"ErrorDlg\" />\n            <DialogRef Id=\"FatalError\" />\n            <DialogRef Id=\"FilesInUse\" />\n            <DialogRef Id=\"MsiRMFilesInUse\" />\n            <DialogRef Id=\"PrepareDlg\" />\n            <DialogRef Id=\"ProgressDlg\" />\n            <DialogRef Id=\"ResumeDlg\" />\n            <DialogRef Id=\"UserExit\" />\n\n            <Publish Dialog=\"BrowseDlg\" Control=\"OK\" Event=\"DoAction\" Value=\"WixUIValidatePath\" Order=\"3\">1</Publish>\n            <Publish Dialog=\"BrowseDlg\" Control=\"OK\" Event=\"SpawnDialog\" Value=\"InvalidDirDlg\" Order=\"4\"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>\"1\"]]></Publish>\n\n            <Publish Dialog=\"Zeal_ExitDialog\" Control=\"Finish\" Event=\"EndDialog\" Value=\"Return\" Order=\"999\">1</Publish>\n\n            <Publish Dialog=\"WelcomeDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"LicenseAgreementDlg\">NOT Installed</Publish>\n            <Publish Dialog=\"WelcomeDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"VerifyReadyDlg\">Installed AND PATCH</Publish>\n\n            <Publish Dialog=\"LicenseAgreementDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"WelcomeDlg\">1</Publish>\n            <Publish Dialog=\"LicenseAgreementDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"InstallDirDlg\">LicenseAccepted = \"1\"</Publish>\n\n            <Publish Dialog=\"InstallDirDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"LicenseAgreementDlg\">1</Publish>\n            <Publish Dialog=\"InstallDirDlg\" Control=\"Next\" Event=\"SetTargetPath\" Value=\"[WIXUI_INSTALLDIR]\" Order=\"1\">1</Publish>\n            <Publish Dialog=\"InstallDirDlg\" Control=\"Next\" Event=\"DoAction\" Value=\"WixUIValidatePath\" Order=\"2\">NOT WIXUI_DONTVALIDATEPATH</Publish>\n            <Publish Dialog=\"InstallDirDlg\" Control=\"Next\" Event=\"SpawnDialog\" Value=\"InvalidDirDlg\" Order=\"3\"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>\"1\"]]></Publish>\n            <Publish Dialog=\"InstallDirDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"VerifyReadyDlg\" Order=\"4\">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID=\"1\"</Publish>\n            <Publish Dialog=\"InstallDirDlg\" Control=\"ChangeFolder\" Property=\"_BrowseProperty\" Value=\"[WIXUI_INSTALLDIR]\" Order=\"1\">1</Publish>\n            <Publish Dialog=\"InstallDirDlg\" Control=\"ChangeFolder\" Event=\"SpawnDialog\" Value=\"BrowseDlg\" Order=\"2\">1</Publish>\n\n            <Publish Dialog=\"VerifyReadyDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"InstallDirDlg\" Order=\"1\">NOT Installed</Publish>\n            <Publish Dialog=\"VerifyReadyDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"MaintenanceTypeDlg\" Order=\"2\">Installed AND NOT PATCH</Publish>\n            <Publish Dialog=\"VerifyReadyDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"WelcomeDlg\" Order=\"2\">Installed AND PATCH</Publish>\n\n            <Publish Dialog=\"MaintenanceWelcomeDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"MaintenanceTypeDlg\">1</Publish>\n\n            <Publish Dialog=\"MaintenanceTypeDlg\" Control=\"RepairButton\" Event=\"NewDialog\" Value=\"VerifyReadyDlg\">1</Publish>\n            <Publish Dialog=\"MaintenanceTypeDlg\" Control=\"RemoveButton\" Event=\"NewDialog\" Value=\"VerifyReadyDlg\">1</Publish>\n            <Publish Dialog=\"MaintenanceTypeDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"MaintenanceWelcomeDlg\">1</Publish>\n\n            <Property Id=\"ARPNOMODIFY\" Value=\"1\" />\n        </UI>\n\n        <UIRef Id=\"WixUI_Common\" />\n    </Fragment>\n</Wix>\n"
  },
  {
    "path": "src/CMakeLists.txt",
    "content": "set(CMAKE_CXX_STANDARD 17)\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\n\n# Find includes in corresponding build directories.\nset(CMAKE_INCLUDE_CURRENT_DIR ON)\n\n## Build options\noption(ZEAL_PORTABLE_BUILD \"Build portable version\")\nif(ZEAL_PORTABLE_BUILD)\n    add_definitions(-DPORTABLE_BUILD)\nendif()\n\n## Macros.\nadd_definitions(-DZEAL_VERSION=\"${ZEAL_VERSION_FULL}\")\n\n# QString options\nadd_definitions(-DQT_USE_QSTRINGBUILDER)\nadd_definitions(-DQT_RESTRICTED_CAST_FROM_ASCII)\nadd_definitions(-DQT_NO_CAST_TO_ASCII)\nadd_definitions(-DQT_NO_URL_CAST_FROM_STRING)\n\n## Handle moc, uic, and rcc files.\nset(CMAKE_AUTOMOC ON)\nset(CMAKE_AUTOUIC ON)\nset(CMAKE_AUTORCC ON)\n\ninclude_directories(libs)\nadd_subdirectory(libs)\n\nadd_subdirectory(app)\n"
  },
  {
    "path": "src/app/CMakeLists.txt",
    "content": "find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Concurrent WebEngineWidgets Widgets REQUIRED)\nif (Qt${QT_VERSION_MAJOR}Widgets_VERSION VERSION_LESS QT_MINIMUM_VERSION)\n    message(FATAL_ERROR \"Qt version >= ${QT_MINIMUM_VERSION} is required.\")\nendif()\n\n# Define output binary name.\nif(APPLE)\n    set(_project_output_name ${CMAKE_PROJECT_NAME})\nelse()\n    string(TOLOWER ${CMAKE_PROJECT_NAME} _project_output_name)\nendif()\n\nset(PROJECT_EXECUTABLE_NAME \"${_project_output_name}${CMAKE_EXECUTABLE_SUFFIX}\")\nmessage(STATUS \"Project executable name: ${PROJECT_EXECUTABLE_NAME}\")\n\n# Only support installing runtime dependencies with Qt >=6.5.1 (see QTBUG-111741).\nif(Qt${QT_VERSION_MAJOR}Widgets_VERSION VERSION_GREATER_EQUAL \"6.5.1\")\n    set(_use_qt_cmake_commands TRUE)\n\n    qt_standard_project_setup()\nendif()\n\nif(APPLE)\n    list(APPEND App_RESOURCES resources/zeal.icns)\nelseif(WIN32)\n    configure_file(versioninfo.rc.in ${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc)\n    list(APPEND App_RESOURCES ${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc)\nelse()\n    set(App_RESOURCES) # Silence CMake warning.\nendif()\n\nif(QT_VERSION_MAJOR EQUAL 6)\n    qt_add_executable(App WIN32\n        main.cpp\n        zeal.qrc\n        ${App_RESOURCES}\n    )\nelse()\n    add_executable(App WIN32\n        main.cpp\n        zeal.qrc\n        ${App_RESOURCES}\n    )\nendif()\n\ntarget_link_libraries(App PRIVATE Core Util Qt${QT_VERSION_MAJOR}::Widgets)\n\nset_target_properties(App PROPERTIES\n    OUTPUT_NAME ${_project_output_name}\n    RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}\n)\n\n# Install Qt runtime dependencies on Windows.\nif(WIN32 AND _use_qt_cmake_commands)\n    qt_generate_deploy_script(\n        TARGET App\n        OUTPUT_SCRIPT _qt_deploy_script\n        CONTENT \"\n# TODO: Run windeployqt after build.\n# Override deployment script's working directory.\n# set(QT_DEPLOY_PREFIX \\\"$<TARGET_FILE_DIR:App>\\\")\n\nqt_deploy_runtime_dependencies(\n    EXECUTABLE \\\"$<TARGET_FILE:App>\\\"\n    BIN_DIR .\n    NO_TRANSLATIONS\n    NO_COMPILER_RUNTIME\n)\")\nendif()\n\nif(APPLE)\n    set_target_properties(App PROPERTIES\n        MACOSX_BUNDLE TRUE\n        MACOSX_BUNDLE_BUNDLE_NAME ${CMAKE_PROJECT_NAME}\n        MACOSX_BUNDLE_GUI_IDENTIFIER \"org.zealdocs.zeal\"\n        MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}\n        MACOSX_BUNDLE_LONG_VERSION_STRING ${PROJECT_VERSION}\n        MACOSX_BUNDLE_SHORT_VERSION_STRING \"${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}\"\n        MACOSX_BUNDLE_ICON_FILE \"zeal.icns\"\n        MACOSX_BUNDLE_COPYRIGHT ${PROJECT_COPYRIGHT}\n        RESOURCE \"resources/zeal.icns\"\n    )\nelseif(WIN32)\n    install(TARGETS App RUNTIME DESTINATION .)\n\n    if(_use_qt_cmake_commands)\n        # Install Qt runtime dependencies.\n        install(SCRIPT ${_qt_deploy_script})\n\n        unset(_qt_deploy_script)\n        unset(_use_qt_cmake_commands)\n    endif()\nelseif(UNIX)\n    include(GNUInstallDirs)\n    install(TARGETS App DESTINATION ${CMAKE_INSTALL_BINDIR})\nendif()\n\n#\n# CPack configuration.\n#\nset(CPACK_GENERATOR \"7Z;ZIP\")\n\nset(CPACK_VERBATIM_VARIABLES YES)\n\n# Usage: cmake --build --preset <preset-name> --target package\n# E.g. cmake --build build --preset ninja-multi-vcpkg-release --target package\nset(CPACK_PACKAGE_NAME ${CMAKE_PROJECT_NAME})\nset(CPACK_PACKAGE_DESCRIPTION ${CMAKE_PROJECT_DESCRIPTION})\nset(CPACK_PACKAGE_VENDOR ${PROJECT_COMPANY_NAME})\nset(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})\nset(CPACK_PACKAGE_ICON \"${CMAKE_SOURCE_DIR}/src/app/resources/zeal.ico\")\n\n# Set binary package file name.\nif(WIN32)\n    if(CMAKE_SYSTEM_PROCESSOR STREQUAL \"AMD64\")\n        set(_package_file_name_suffix \"-windows-x64\")\n    elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL \"X86\")\n        set(_package_file_name_suffix \"-windows-x86\")\n    elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL \"ARM64\")\n        set(_package_file_name_suffix \"-windows-arm64\")\n    else()\n        set(_package_file_name_suffix \"-windows-unknown\")\n    endif()\nelse()\n    set(_package_file_name_suffix \"\")\nendif()\n\nif(ZEAL_PORTABLE_BUILD)\n    string(PREPEND _package_file_name_suffix \"-portable\")\nendif()\n\nset(CPACK_PACKAGE_FILE_NAME \"${_project_output_name}-${ZEAL_VERSION_FULL}${_package_file_name_suffix}\")\n\nset(CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME})\nset(CPACK_PACKAGE_EXECUTABLES ${_project_output_name} ${CPACK_PACKAGE_NAME})\nset(CPACK_CREATE_DESKTOP_LINKS ${_project_output_name} ${CPACK_PACKAGE_NAME})\n\n# Allow CPack to do text to RTF conversion.\nconfigure_file(\"${CMAKE_SOURCE_DIR}/COPYING\" \"${CMAKE_CURRENT_BINARY_DIR}/license.txt\" COPYONLY)\nset(CPACK_RESOURCE_FILE_LICENSE \"${CMAKE_CURRENT_BINARY_DIR}/license.txt\")\nset(CPACK_RESOURCE_FILE_README \"${CMAKE_SOURCE_DIR}/README.md\")\n\nset(CPACK_PACKAGE_CHECKSUM SHA256)\n\nif(WIN32)\n    # CPack WiX configuration.\n    set(CPACK_WIX_UPGRADE_GUID \"5C4B6030-A1B4-4EFE-A5AF-28F6FA2E7978\")\n    set(CPACK_WIX_PROPERTY_ARPURLINFOABOUT ${CMAKE_PROJECT_HOMEPAGE_URL})\n    set(CPACK_WIX_PRODUCT_ICON \"${CMAKE_SOURCE_DIR}/src/app/resources/zeal.ico\")\n    #set(CPACK_WIX_UI_BANNER \"${CMAKE_SOURCE_DIR}/pkg/wix/banner.png\")\n    #set(CPACK_WIX_UI_DIALOG \"${CMAKE_SOURCE_DIR}/pkg/wix/dialog.png\")\n    set(CPACK_WIX_EXTENSIONS \"WixUtilExtension\")\n    set(CPACK_WIX_UI_REF \"Zeal_InstallDir\")\n    set(CPACK_WIX_TEMPLATE \"${CMAKE_SOURCE_DIR}/pkg/wix/template.xml\")\n    set(CPACK_WIX_PATCH_FILE \"${CMAKE_SOURCE_DIR}/pkg/wix/patch.xml\")\n    set(CPACK_WIX_EXTRA_SOURCES\n        \"${CMAKE_SOURCE_DIR}/pkg/wix/ui.wxs\"\n        \"${CMAKE_SOURCE_DIR}/pkg/wix/exitdialog.wxs\"\n    )\n\n    set(CPACK_PRE_BUILD_SCRIPTS \"${CMAKE_SOURCE_DIR}/pkg/wix/cpack_pre_build.cmake\")\n\n    if(NOT ZEAL_PORTABLE_BUILD)\n        list(APPEND CPACK_GENERATOR \"WIX\")\n        set(CPACK_POST_BUILD_SCRIPTS \"${CMAKE_SOURCE_DIR}/pkg/wix/cpack_post_build.cmake\")\n    endif()\nendif()\n\n# Set options for the source package.\n# Usage: cmake --build <build> --target package_source\nset(CPACK_SOURCE_GENERATOR \"TGZ;TXZ;ZIP\")\nset(CPACK_SOURCE_PACKAGE_FILE_NAME \"${_project_output_name}-${ZEAL_VERSION_FULL}\")\nset(CPACK_SOURCE_IGNORE_FILES\n    # Directories.\n    \".git/\"\n    \".github/\"\n    \".vscode/\"\n    \"build/\"\n    # Files.\n    \".editorconfig\"\n    \".gitattributes\"\n    \".gitignore\"\n)\n\ninclude(CPack)\n"
  },
  {
    "path": "src/app/main.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include <core/application.h>\n#include <core/applicationsingleton.h>\n#include <registry/searchquery.h>\n\n#include <QApplication>\n#include <QCommandLineParser>\n#include <QDataStream>\n#include <QDesktopServices>\n#include <QDir>\n#include <QIcon>\n#include <QMessageBox>\n#include <QTextStream>\n#include <QTimer>\n#include <QUrlQuery>\n\n#ifdef Q_OS_WINDOWS\n#include <QSettings>\n\n#include <Windows.h>\n\n#include <utility> // for std::ignore\n#endif\n\n#include <cstdlib>\n\nusing namespace Zeal;\n\nstruct CommandLineParameters\n{\n    bool forceMinimized;\n    bool preventActivation;\n\n#ifdef Q_OS_WINDOWS\n    bool attachConsole;\n    bool registerProtocolHandlers;\n    bool unregisterProtocolHandlers;\n#endif\n\n    Registry::SearchQuery query;\n};\n\nQString stripParameterUrl(const QString &url, const QString &scheme)\n{\n    QString str = url.mid(scheme.length() + 1);\n\n    if (str.startsWith(QLatin1String(\"//\"))) {\n        str = str.mid(2);\n    }\n\n    if (str.endsWith(QLatin1Char('/'))) {\n        str = str.left(str.length() - 1);\n    }\n\n    return str;\n}\n\nCommandLineParameters parseCommandLine(const QStringList &arguments)\n{\n    QCommandLineParser parser;\n    parser.setApplicationDescription(QObject::tr(\"Zeal - Offline documentation browser.\"));\n    parser.addHelpOption();\n    parser.addVersionOption();\n\n    parser.addOptions({\n        {QStringLiteral(\"minimized\"), QObject::tr(\"Start minimized regardless of settings.\")}\n    });\n\n#ifdef Q_OS_WINDOWS\n    parser.addOptions({\n        {QStringLiteral(\"attach-console\"), QObject::tr(\"Attach console for logging.\")},\n        {QStringLiteral(\"register\"), QObject::tr(\"Register protocol handlers.\")},\n        {QStringLiteral(\"unregister\"), QObject::tr(\"Unregister protocol handlers.\")}\n    });\n#endif\n\n    parser.addPositionalArgument(QStringLiteral(\"url\"), QObject::tr(\"dash[-plugin]:// URL\"));\n    parser.process(arguments);\n\n    CommandLineParameters clParams;\n    clParams.forceMinimized = parser.isSet(QStringLiteral(\"minimized\"));\n    clParams.preventActivation = false;\n\n#ifdef Q_OS_WINDOWS\n    clParams.attachConsole = parser.isSet(QStringLiteral(\"attach-console\"));\n    clParams.registerProtocolHandlers = parser.isSet(QStringLiteral(\"register\"));\n    clParams.unregisterProtocolHandlers = parser.isSet(QStringLiteral(\"unregister\"));\n\n    if (clParams.registerProtocolHandlers && clParams.unregisterProtocolHandlers) {\n        QTextStream(stderr) << QObject::tr(\"Parameter conflict: --register and --unregister.\\n\");\n        ::exit(EXIT_FAILURE);\n    }\n#endif\n\n    // TODO: Support dash-feed:// protocol\n    const QString arg\n            = QUrl::fromPercentEncoding(parser.positionalArguments().value(0).toUtf8());\n\n    if (arg.startsWith(QLatin1String(\"dash:\"))) {\n        clParams.query.setQuery(stripParameterUrl(arg, QStringLiteral(\"dash\")));\n    } else if (arg.startsWith(QLatin1String(\"dash-plugin:\"))) {\n        const QUrlQuery urlQuery(stripParameterUrl(arg, QStringLiteral(\"dash-plugin\")));\n\n        const QString keys = urlQuery.queryItemValue(QStringLiteral(\"keys\"));\n        if (!keys.isEmpty())\n            clParams.query.setKeywords(keys.split(QLatin1Char(',')));\n\n        clParams.query.setQuery(urlQuery.queryItemValue(QStringLiteral(\"query\")));\n\n        const QString preventActivation\n                = urlQuery.queryItemValue(QStringLiteral(\"prevent_activation\"));\n        clParams.preventActivation = preventActivation == QLatin1String(\"true\");\n    } else {\n        clParams.query.setQuery(arg);\n    }\n\n    return clParams;\n}\n\n#ifdef Q_OS_WINDOWS\nvoid registerProtocolHandler(const QString &scheme, const QString &description)\n{\n    const QString appPath = QDir::toNativeSeparators(QCoreApplication::applicationFilePath());\n    const QString regPath = QStringLiteral(\"HKEY_CURRENT_USER\\\\Software\\\\Classes\\\\\") + scheme;\n\n    QScopedPointer<QSettings> reg(new QSettings(regPath, QSettings::NativeFormat));\n\n    reg->setValue(QStringLiteral(\"Default\"), description);\n    reg->setValue(QStringLiteral(\"URL Protocol\"), QString());\n\n    reg->beginGroup(QStringLiteral(\"DefaultIcon\"));\n    reg->setValue(QStringLiteral(\"Default\"), QString(\"%1,1\").arg(appPath));\n    reg->endGroup();\n\n    reg->beginGroup(QStringLiteral(\"shell\"));\n    reg->beginGroup(QStringLiteral(\"open\"));\n    reg->beginGroup(QStringLiteral(\"command\"));\n    reg->setValue(QStringLiteral(\"Default\"), QVariant(appPath + QLatin1String(\" %1\")));\n}\n\nvoid registerProtocolHandlers(const QHash<QString, QString> &protocols, bool force = false)\n{\n    const QString regPath = QStringLiteral(\"HKEY_CURRENT_USER\\\\Software\\\\Classes\");\n    QScopedPointer<QSettings> reg(new QSettings(regPath, QSettings::NativeFormat));\n\n    const QStringList groups = reg->childGroups();\n    for (auto it = protocols.cbegin(); it != protocols.cend(); ++it) {\n        if (force || !groups.contains(it.key()))\n            registerProtocolHandler(it.key(), it.value());\n    }\n}\n\nvoid unregisterProtocolHandlers(const QHash<QString, QString> &protocols)\n{\n    const QString regPath = QStringLiteral(\"HKEY_CURRENT_USER\\\\Software\\\\Classes\");\n    QScopedPointer<QSettings> reg(new QSettings(regPath, QSettings::NativeFormat));\n\n    for (auto it = protocols.cbegin(); it != protocols.cend(); ++it) {\n        reg->remove(it.key());\n    }\n}\n#endif\n\nint main(int argc, char *argv[])\n{\n    // Do not allow Qt version lower than the app was compiled with.\n    QT_REQUIRE_VERSION(argc, argv, QT_VERSION_STR)\n\n    QCoreApplication::setApplicationName(QStringLiteral(\"Zeal\"));\n    QCoreApplication::setApplicationVersion(ZEAL_VERSION);\n    QCoreApplication::setOrganizationDomain(QStringLiteral(\"zealdocs.org\"));\n    QCoreApplication::setOrganizationName(QStringLiteral(\"Zeal\"));\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);\n    QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);\n#endif\n\n// Use Fusion style on Windows 10 & 11. This enables proper dark mode support.\n// See https://www.qt.io/blog/dark-mode-on-windows-11-with-qt-6.5.\n// TODO: Make style configurable, detect -style argument.\n#if defined(Q_OS_WINDOWS) && (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0))\n    const auto osName = QSysInfo::prettyProductName();\n    if (osName.startsWith(\"Windows 10\") || osName.startsWith(\"Windows 11\")) {\n        QApplication::setStyle(\"fusion\");\n    }\n#endif\n\n    QScopedPointer<QApplication> qapp(new QApplication(argc, argv));\n\n    const CommandLineParameters clParams = parseCommandLine(qapp->arguments());\n\n#ifdef Q_OS_WINDOWS\n    const static QHash<QString, QString> protocols = {\n        {QStringLiteral(\"dash\"), QStringLiteral(\"URL:Dash Protocol (Zeal)\")},\n        {QStringLiteral(\"dash-plugin\"), QStringLiteral(\"URL:Dash Plugin Protocol (Zeal)\")}\n    };\n\n    if (clParams.registerProtocolHandlers) {\n        registerProtocolHandlers(protocols, clParams.registerProtocolHandlers);\n        return EXIT_SUCCESS;\n    }\n\n    if (clParams.unregisterProtocolHandlers) {\n        unregisterProtocolHandlers(protocols);\n        return EXIT_SUCCESS;\n    }\n#endif\n\n#ifdef Q_OS_WINDOWS\n    if (clParams.attachConsole && AttachConsole(ATTACH_PARENT_PROCESS)) {\n        FILE *fp = nullptr;\n        std::ignore = freopen_s(&fp, \"CONOUT$\", \"w\", stdout);\n        std::ignore = freopen_s(&fp, \"CONOUT$\", \"w\", stderr);\n        std::ignore = freopen_s(&fp, \"CONIN$\", \"r\", stdin);\n    }\n#endif // Q_OS_WINDOWS\n\n    QScopedPointer<Core::ApplicationSingleton> appSingleton(new Core::ApplicationSingleton());\n    if (appSingleton->isSecondary()) {\n#ifdef Q_OS_WINDOWS\n        ::AllowSetForegroundWindow(appSingleton->primaryPid());\n#endif\n        QByteArray ba;\n        QDataStream out(&ba, QIODevice::WriteOnly);\n        out << clParams.query << clParams.preventActivation;\n        if (!appSingleton->sendMessage(ba)) {\n            QTextStream(stderr) << \"Failed to send query to the primary instance.\" << '\\n';\n            return EXIT_FAILURE;\n        }\n        return EXIT_SUCCESS;\n    }\n\n    // Set application-wide window icon. All message boxes and other windows will use it by default.\n    qapp->setDesktopFileName(QStringLiteral(\"org.zealdocs.zeal\"));\n    qapp->setWindowIcon(QIcon::fromTheme(QStringLiteral(\"zeal\"),\n                                         QIcon(QStringLiteral(\":/zeal.ico\"))));\n\n    QDir::setSearchPaths(QStringLiteral(\"typeIcon\"), {QStringLiteral(\":/icons/type\")});\n\n    QScopedPointer<Core::Application> app(new Core::Application());\n\n    QObject::connect(appSingleton.data(), &Core::ApplicationSingleton::messageReceived,\n                     [&app](const QByteArray &data) {\n        Registry::SearchQuery query;\n        bool preventActivation;\n\n        QDataStream in(data);\n        in >> query >> preventActivation;\n\n        app->executeQuery(query, preventActivation);\n    });\n\n    app->showMainWindow(clParams.forceMinimized);\n\n    if (!clParams.query.isEmpty()) {\n        QTimer::singleShot(0, app.data(), [&app, clParams] {\n            app->executeQuery(clParams.query, clParams.preventActivation);\n        });\n    }\n\n    return qapp->exec();\n}\n"
  },
  {
    "path": "src/app/resources/browser/404.html",
    "content": "<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>File not found</title>\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"assets/css/oat.min.css\">\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"assets/css/welcome.css\">\n</head>\n\n<body>\n  <div class=\"page\">\n    <div class=\"brand\">\n      <h1>File not found</h1>\n      <p>Encountered a problem? Please <a onclick=\"zAppBridge.openShortUrl('report-bug')\">report</a>!</p>\n    </div>\n\n    <div class=\"actions\">\n      <h2>Get in touch</h2>\n      <div class=\"command-block\" onclick=\"zAppBridge.openShortUrl('gitter')\">\n        <h3>Matrix</h3>\n        <p>Chat with developers and other users</p>\n      </div>\n      <div class=\"command-block\" onclick=\"zAppBridge.openShortUrl('github')\">\n        <h3>GitHub</h3>\n        <p>Contribute to the project</p>\n      </div>\n    </div>\n  </div>\n\n  <footer class=\"footer\">\n      <div class=\"icon-links\">\n        <!-- Simple Icons: GitHub (CC0) -->\n        <a class=\"icon-link\" onclick=\"zAppBridge.openShortUrl('github')\" title=\"GitHub\">\n          <svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\">\n            <path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"/>\n          </svg>\n        </a>\n        <!-- Simple Icons: Matrix (CC0) -->\n        <a class=\"icon-link\" onclick=\"zAppBridge.openShortUrl('gitter')\" title=\"Matrix\">\n          <svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\">\n            <path d=\"M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033c.309-.443.683-.784 1.117-1.024.433-.245.936-.365 1.5-.365.54 0 1.033.107 1.481.314.448.208.785.582 1.02 1.108.254-.374.6-.706 1.034-.992.434-.287.95-.43 1.546-.43.453 0 .872.056 1.26.167.388.11.716.286.993.53.276.245.489.559.646.951.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66 1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499 1.946 1.946 0 0 0-.231.696 5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688 1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19-.111 0-.259.024-.439.074-.18.051-.36.143-.53.282-.171.138-.319.337-.439.595-.12.259-.18.6-.18 1.02v4.966H5.46V7.81zm15.693 15.64V.55H21.72V0H24v24h-2.28v-.55z\"/>\n          </svg>\n        </a>\n        <!-- Simple Icons: X / Twitter (CC0) -->\n        <a class=\"icon-link\" onclick=\"zAppBridge.openShortUrl('twitter')\" title=\"X\">\n          <svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\">\n            <path d=\"M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z\"/>\n          </svg>\n        </a>\n      </div>\n      <!-- Lucide: Bug (ISC) -->\n      <a class=\"report-link\" onclick=\"zAppBridge.openShortUrl('report-bug')\">\n        <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n          <path d=\"M12 20v-9\"/>\n          <path d=\"M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z\"/>\n          <path d=\"M14.12 3.88 16 2\"/>\n          <path d=\"M21 21a4 4 0 0 0-3.81-4\"/>\n          <path d=\"M21 5a4 4 0 0 1-3.55 3.97\"/>\n          <path d=\"M22 13h-4\"/>\n          <path d=\"M3 21a4 4 0 0 1 3.81-4\"/>\n          <path d=\"M3 5a4 4 0 0 0 3.55 3.97\"/>\n          <path d=\"M6 13H2\"/>\n          <path d=\"m8 2 1.88 1.88\"/>\n          <path d=\"M9 7.13V6a3 3 0 1 1 6 0v1.13\"/>\n        </svg>\n        <span>Report a problem</span>\n      </a>\n  </footer>\n\n  <script type=\"text/javascript\" src=\"qrc:///qtwebchannel/qwebchannel.js\"></script>\n  <script>\n    new QWebChannel(qt.webChannelTransport, (channel) => {\n      window.zAppBridge = channel.objects.zAppBridge;\n    });\n  </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/app/resources/browser/assets/css/highlight.css",
    "content": "/* Highlight on navigation to an anchor. */\n@keyframes targetNavigatedAnimation {\n    from { background: #fff; }\n    50% { background: #ff0; }\n    to { background: #fff; }\n}\n\n*:target {\n    animation: targetNavigatedAnimation .5s linear;\n}\n"
  },
  {
    "path": "src/app/resources/browser/assets/css/welcome.css",
    "content": "/* Custom styles for Zeal's built-in browser pages. Depends on oat.min.css. */\n\nbody {\n\tuser-select: none;\n\tmin-height: 100dvh;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n}\n\n.page {\n\tflex: 1;\n\tdisplay: grid;\n\tgrid-template-columns: 1fr 1.5fr;\n\talign-content: center;\n\tgap: 1.5rem 3rem;\n\tmax-width: 720px;\n\twidth: 100%;\n\tpadding: 2rem;\n}\n\n.brand {\n\tdisplay: flex;\n\tflex-direction: column;\n\tjustify-content: center;\n\tgap: 0.25rem;\n}\n\n.brand h1 {\n\tfont-size: 2rem;\n\tline-height: 1.1;\n}\n\n.brand .version {\n\tfont-weight: 300;\n\topacity: 0.5;\n\tmargin-left: 0.3rem;\n\tfont-size: 1rem;\n}\n\n.brand > p {\n\topacity: 0.6;\n}\n\n.actions {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 0.5rem;\n}\n\n.actions > h2 {\n\tfont-size: 0.7rem;\n\ttext-transform: uppercase;\n\tletter-spacing: 0.07em;\n\topacity: 0.45;\n\tmargin-top: 0.5rem;\n\tmargin-bottom: 0;\n}\n\n.command-block {\n\tcursor: pointer;\n\tpadding: 0.625rem 0.875rem;\n\tborder-radius: 6px;\n\tborder: 1px solid #e2e8f0;\n\ttransition:\n\t\tborder-color 0.15s,\n\t\tbackground 0.15s;\n}\n\n.command-block:hover {\n\tborder-color: #94a3b8;\n\tbackground: #f8fafc;\n}\n\n.command-block h3 {\n\tfont-size: 0.875rem;\n\tfont-weight: 600;\n\tmargin-bottom: 0.125rem;\n}\n\n.command-block p {\n\tfont-size: 0.775rem;\n\topacity: 0.55;\n\tmargin: 0;\n}\n\n.footer {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tgap: 0.625rem;\n\twidth: 100%;\n\tmax-width: 720px;\n\tpadding: 1rem 2rem;\n\tborder-top: 1px solid #e2e8f0;\n}\n\n.icon-links {\n\tdisplay: flex;\n\tgap: 1.25rem;\n\talign-items: center;\n}\n\n.icon-link {\n\tcursor: pointer;\n\tdisplay: inline-flex;\n\tcolor: inherit;\n\ttext-decoration: none;\n\topacity: 0.45;\n\ttransition: opacity 0.15s;\n}\n\n.icon-link:hover {\n\topacity: 0.85;\n\ttext-decoration: none;\n}\n\n.icon-link svg {\n\twidth: 1.2rem;\n\theight: 1.2rem;\n}\n\n.report-link {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 0.3rem;\n\tfont-size: 0.75rem;\n\tcursor: pointer;\n\topacity: 0.4;\n\ttransition: opacity 0.15s;\n\ttext-decoration: none;\n\tcolor: inherit;\n}\n\n.report-link:hover {\n\topacity: 0.7;\n\ttext-decoration: none;\n}\n\n.report-link svg {\n\twidth: 0.85rem;\n\theight: 0.85rem;\n}\n\n@media (prefers-color-scheme: dark) {\n\t.command-block {\n\t\tborder-color: #334155;\n\t}\n\n\t.command-block:hover {\n\t\tborder-color: #475569;\n\t\tbackground: #1e293b;\n\t}\n\n\t.footer {\n\t\tborder-color: #334155;\n\t}\n}\n"
  },
  {
    "path": "src/app/resources/browser/welcome.html",
    "content": "<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>Welcome</title>\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"assets/css/oat.min.css\">\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"assets/css/welcome.css\">\n</head>\n\n<body>\n  <div class=\"page\">\n    <div class=\"brand\">\n      <h1>Zeal<span class=\"version\" id=\"version\"></span></h1>\n      <p>Docs for everyone</p>\n    </div>\n\n    <div class=\"actions\">\n      <h2>Customize</h2>\n      <div class=\"command-block\" onclick=\"zAppBridge.triggerAction('openDocsetManager')\">\n        <h3>Docsets</h3>\n        <p>Install and update docsets</p>\n      </div>\n      <div class=\"command-block\" onclick=\"zAppBridge.triggerAction('openPreferences')\">\n        <h3>Preferences</h3>\n        <p>Adjust application settings</p>\n      </div>\n      <h2>Get in touch</h2>\n      <div class=\"command-block\" onclick=\"zAppBridge.openShortUrl('gitter')\">\n        <h3>Matrix</h3>\n        <p>Chat with developers and other users</p>\n      </div>\n      <div class=\"command-block\" onclick=\"zAppBridge.openShortUrl('github')\">\n        <h3>GitHub</h3>\n        <p>Contribute to the project</p>\n      </div>\n    </div>\n  </div>\n\n  <footer class=\"footer\">\n      <div class=\"icon-links\">\n        <!-- Simple Icons: GitHub (CC0) -->\n        <a class=\"icon-link\" onclick=\"zAppBridge.openShortUrl('github')\" title=\"GitHub\">\n          <svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\">\n            <path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"/>\n          </svg>\n        </a>\n        <!-- Simple Icons: Matrix (CC0) -->\n        <a class=\"icon-link\" onclick=\"zAppBridge.openShortUrl('gitter')\" title=\"Matrix\">\n          <svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\">\n            <path d=\"M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033c.309-.443.683-.784 1.117-1.024.433-.245.936-.365 1.5-.365.54 0 1.033.107 1.481.314.448.208.785.582 1.02 1.108.254-.374.6-.706 1.034-.992.434-.287.95-.43 1.546-.43.453 0 .872.056 1.26.167.388.11.716.286.993.53.276.245.489.559.646.951.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66 1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499 1.946 1.946 0 0 0-.231.696 5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688 1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19-.111 0-.259.024-.439.074-.18.051-.36.143-.53.282-.171.138-.319.337-.439.595-.12.259-.18.6-.18 1.02v4.966H5.46V7.81zm15.693 15.64V.55H21.72V0H24v24h-2.28v-.55z\"/>\n          </svg>\n        </a>\n        <!-- Simple Icons: X / Twitter (CC0) -->\n        <a class=\"icon-link\" onclick=\"zAppBridge.openShortUrl('twitter')\" title=\"X\">\n          <svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\">\n            <path d=\"M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z\"/>\n          </svg>\n        </a>\n      </div>\n      <!-- Lucide: Bug (ISC) -->\n      <a class=\"report-link\" onclick=\"zAppBridge.openShortUrl('report-bug')\">\n        <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n          <path d=\"M12 20v-9\"/>\n          <path d=\"M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z\"/>\n          <path d=\"M14.12 3.88 16 2\"/>\n          <path d=\"M21 21a4 4 0 0 0-3.81-4\"/>\n          <path d=\"M21 5a4 4 0 0 1-3.55 3.97\"/>\n          <path d=\"M22 13h-4\"/>\n          <path d=\"M3 21a4 4 0 0 1 3.81-4\"/>\n          <path d=\"M3 5a4 4 0 0 0 3.55 3.97\"/>\n          <path d=\"M6 13H2\"/>\n          <path d=\"m8 2 1.88 1.88\"/>\n          <path d=\"M9 7.13V6a3 3 0 1 1 6 0v1.13\"/>\n        </svg>\n        <span>Report a problem</span>\n      </a>\n  </footer>\n\n  <script type=\"text/javascript\" src=\"qrc:///qtwebchannel/qwebchannel.js\"></script>\n  <script>\n    new QWebChannel(qt.webChannelTransport, (channel) => {\n      window.zAppBridge = channel.objects.zAppBridge;\n      document.getElementById(\"version\").textContent = zAppBridge.AppVersion;\n    });\n  </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/app/resources/icons/README.md",
    "content": "# Resources\n\n## Dash Type Icons (`type`)\n\nUpstream repository: <https://github.com/Kapeli/Dash-X-Platform-Resources>\n\nOptimized with:\n\n```shell\nfind . -type f -iname '*.png' -exec pngcrush -ow -rem allb -reduce {} \\;\nfind . -type f -name \"*.png\" -exec convert {} -strip {} \\;\noptipng *.png\n```\n\nThe following icons are renamed:\n\n* `Enum` to `Enumeration`\n* `Struct` to `Structure`\n"
  },
  {
    "path": "src/app/versioninfo.rc.in",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include <winver.h>\n\n#pragma code_page(65001)\n\nIDI_ICON1 ICON DISCARDABLE \"resources/zeal.ico\"\n\n#define VER_COMPANYNAME_STR \"${PROJECT_COMPANY_NAME}\"\n#define VER_FILEDESCRIPTION_STR \"${CMAKE_PROJECT_DESCRIPTION}\"\n#define VER_FILEVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH}\n#define VER_FILEVERSION_STR \"${PROJECT_VERSION}\"\n#define VER_INTERNALNAME_STR \"${CMAKE_PROJECT_NAME}\"\n#define VER_LEGALCOPYRIGHT_STR \"${PROJECT_COPYRIGHT}\"\n#define VER_LEGALTRADEMARKS1_STR \"\"\n#define VER_LEGALTRADEMARKS2_STR \"\"\n#define VER_ORIGINALFILENAME_STR \"${PROJECT_EXECUTABLE_NAME}\"\n#define VER_PRODUCTNAME_STR \"${CMAKE_PROJECT_NAME}\"\n#define VER_PRODUCTVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH}\n#define VER_PRODUCTVERSION_STR \"${PROJECT_VERSION}\"\n\n#ifndef DEBUG\n#define VER_DEBUG 0\n#else\n#define VER_DEBUG VS_FF_DEBUG\n#endif\n\nVS_VERSION_INFO VERSIONINFO\nFILEVERSION    \tVER_FILEVERSION\nPRODUCTVERSION \tVER_PRODUCTVERSION\nFILEFLAGSMASK  \tVS_FFI_FILEFLAGSMASK\n//TODO: Set file flags.\n//FILEFLAGS     (VER_PRIVATEBUILD|VER_PRERELEASE|VER_DEBUG)\nFILEFLAGS       0\nFILEOS         \tVOS__WINDOWS32\nFILETYPE       \tVFT_DLL\nFILESUBTYPE    \tVFT2_UNKNOWN\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904B0\"\n        BEGIN\n            VALUE \"CompanyName\",      VER_COMPANYNAME_STR\n            VALUE \"FileDescription\",  VER_FILEDESCRIPTION_STR\n            VALUE \"FileVersion\",      VER_FILEVERSION_STR\n            VALUE \"InternalName\",     VER_INTERNALNAME_STR\n            VALUE \"LegalCopyright\",   VER_LEGALCOPYRIGHT_STR\n            VALUE \"LegalTrademarks1\", VER_LEGALTRADEMARKS1_STR\n            VALUE \"LegalTrademarks2\", VER_LEGALTRADEMARKS2_STR\n            VALUE \"OriginalFilename\", VER_ORIGINALFILENAME_STR\n            VALUE \"ProductName\",      VER_PRODUCTNAME_STR\n            VALUE \"ProductVersion\",   VER_PRODUCTVERSION_STR\n        END\n    END\n\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x0409, 1200\n    END\nEND\n"
  },
  {
    "path": "src/app/zeal.qrc",
    "content": "<RCC>\n    <qresource prefix=\"/\">\n        <file alias=\"zeal.ico\">resources/zeal.ico</file>\n    </qresource>\n    <qresource prefix=\"/browser\">\n        <file alias=\"404.html\">resources/browser/404.html</file>\n        <file alias=\"assets/css/highlight.css\">resources/browser/assets/css/highlight.css</file>\n        <file alias=\"assets/css/oat.min.css\">resources/browser/assets/css/oat.min.css</file>\n        <file alias=\"assets/css/welcome.css\">resources/browser/assets/css/welcome.css</file>\n        <file alias=\"welcome.html\">resources/browser/welcome.html</file>\n    </qresource>\n    <qresource prefix=\"/icons/type\">\n        <file alias=\"Abbreviation.png\">resources/icons/type/Abbreviation.png</file>\n        <file alias=\"Abbreviation@2x.png\">resources/icons/type/Abbreviation@2x.png</file>\n        <file alias=\"Alias.png\">resources/icons/type/Alias.png</file>\n        <file alias=\"Alias@2x.png\">resources/icons/type/Alias@2x.png</file>\n        <file alias=\"Annotation.png\">resources/icons/type/Annotation.png</file>\n        <file alias=\"Annotation@2x.png\">resources/icons/type/Annotation@2x.png</file>\n        <file alias=\"Attribute.png\">resources/icons/type/Attribute.png</file>\n        <file alias=\"Attribute@2x.png\">resources/icons/type/Attribute@2x.png</file>\n        <file alias=\"Axiom.png\">resources/icons/type/Axiom.png</file>\n        <file alias=\"Axiom@2x.png\">resources/icons/type/Axiom@2x.png</file>\n        <file alias=\"Binding.png\">resources/icons/type/Binding.png</file>\n        <file alias=\"Binding@2x.png\">resources/icons/type/Binding@2x.png</file>\n        <file alias=\"Block.png\">resources/icons/type/Block.png</file>\n        <file alias=\"Block@2x.png\">resources/icons/type/Block@2x.png</file>\n        <file alias=\"Bookmark.png\">resources/icons/type/Bookmark.png</file>\n        <file alias=\"Bookmark@2x.png\">resources/icons/type/Bookmark@2x.png</file>\n        <file alias=\"Builtin.png\">resources/icons/type/Builtin.png</file>\n        <file alias=\"Builtin@2x.png\">resources/icons/type/Builtin@2x.png</file>\n        <file alias=\"Callback.png\">resources/icons/type/Callback.png</file>\n        <file alias=\"Callback@2x.png\">resources/icons/type/Callback@2x.png</file>\n        <file alias=\"Category.png\">resources/icons/type/Category.png</file>\n        <file alias=\"Category@2x.png\">resources/icons/type/Category@2x.png</file>\n        <file alias=\"Class.png\">resources/icons/type/Class.png</file>\n        <file alias=\"Class@2x.png\">resources/icons/type/Class@2x.png</file>\n        <file alias=\"Collection.png\">resources/icons/type/Collection.png</file>\n        <file alias=\"Collection@2x.png\">resources/icons/type/Collection@2x.png</file>\n        <file alias=\"Column.png\">resources/icons/type/Column.png</file>\n        <file alias=\"Column@2x.png\">resources/icons/type/Column@2x.png</file>\n        <file alias=\"Command.png\">resources/icons/type/Command.png</file>\n        <file alias=\"Command@2x.png\">resources/icons/type/Command@2x.png</file>\n        <file alias=\"Component.png\">resources/icons/type/Component.png</file>\n        <file alias=\"Component@2x.png\">resources/icons/type/Component@2x.png</file>\n        <file alias=\"Constant.png\">resources/icons/type/Constant.png</file>\n        <file alias=\"Constant@2x.png\">resources/icons/type/Constant@2x.png</file>\n        <file alias=\"Constructor.png\">resources/icons/type/Constructor.png</file>\n        <file alias=\"Constructor@2x.png\">resources/icons/type/Constructor@2x.png</file>\n        <file alias=\"Conversion.png\">resources/icons/type/Conversion.png</file>\n        <file alias=\"Conversion@2x.png\">resources/icons/type/Conversion@2x.png</file>\n        <file alias=\"Data Source.png\">resources/icons/type/Data Source.png</file>\n        <file alias=\"Data Source@2x.png\">resources/icons/type/Data Source@2x.png</file>\n        <file alias=\"Database.png\">resources/icons/type/Database.png</file>\n        <file alias=\"Database@2x.png\">resources/icons/type/Database@2x.png</file>\n        <file alias=\"Decorator.png\">resources/icons/type/Decorator.png</file>\n        <file alias=\"Decorator@2x.png\">resources/icons/type/Decorator@2x.png</file>\n        <file alias=\"Define.png\">resources/icons/type/Define.png</file>\n        <file alias=\"Define@2x.png\">resources/icons/type/Define@2x.png</file>\n        <file alias=\"Delegate.png\">resources/icons/type/Delegate.png</file>\n        <file alias=\"Delegate@2x.png\">resources/icons/type/Delegate@2x.png</file>\n        <file alias=\"Device.png\">resources/icons/type/Device.png</file>\n        <file alias=\"Device@2x.png\">resources/icons/type/Device@2x.png</file>\n        <file alias=\"Diagram.png\">resources/icons/type/Diagram.png</file>\n        <file alias=\"Diagram@2x.png\">resources/icons/type/Diagram@2x.png</file>\n        <file alias=\"Directive.png\">resources/icons/type/Directive.png</file>\n        <file alias=\"Directive@2x.png\">resources/icons/type/Directive@2x.png</file>\n        <file alias=\"Element.png\">resources/icons/type/Element.png</file>\n        <file alias=\"Element@2x.png\">resources/icons/type/Element@2x.png</file>\n        <file alias=\"Entry.png\">resources/icons/type/Entry.png</file>\n        <file alias=\"Entry@2x.png\">resources/icons/type/Entry@2x.png</file>\n        <file alias=\"Enumeration.png\">resources/icons/type/Enumeration.png</file>\n        <file alias=\"Enumeration@2x.png\">resources/icons/type/Enumeration@2x.png</file>\n        <file alias=\"Environment.png\">resources/icons/type/Environment.png</file>\n        <file alias=\"Environment@2x.png\">resources/icons/type/Environment@2x.png</file>\n        <file alias=\"Error.png\">resources/icons/type/Error.png</file>\n        <file alias=\"Error@2x.png\">resources/icons/type/Error@2x.png</file>\n        <file alias=\"Event.png\">resources/icons/type/Event.png</file>\n        <file alias=\"Event@2x.png\">resources/icons/type/Event@2x.png</file>\n        <file alias=\"Exception.png\">resources/icons/type/Exception.png</file>\n        <file alias=\"Exception@2x.png\">resources/icons/type/Exception@2x.png</file>\n        <file alias=\"Expression.png\">resources/icons/type/Expression.png</file>\n        <file alias=\"Expression@2x.png\">resources/icons/type/Expression@2x.png</file>\n        <file alias=\"Extension.png\">resources/icons/type/Extension.png</file>\n        <file alias=\"Extension@2x.png\">resources/icons/type/Extension@2x.png</file>\n        <file alias=\"Field.png\">resources/icons/type/Field.png</file>\n        <file alias=\"Field@2x.png\">resources/icons/type/Field@2x.png</file>\n        <file alias=\"File.png\">resources/icons/type/File.png</file>\n        <file alias=\"File@2x.png\">resources/icons/type/File@2x.png</file>\n        <file alias=\"Filter.png\">resources/icons/type/Filter.png</file>\n        <file alias=\"Filter@2x.png\">resources/icons/type/Filter@2x.png</file>\n        <file alias=\"Flag.png\">resources/icons/type/Flag.png</file>\n        <file alias=\"Flag@2x.png\">resources/icons/type/Flag@2x.png</file>\n        <file alias=\"Foreign Key.png\">resources/icons/type/Foreign Key.png</file>\n        <file alias=\"Foreign Key@2x.png\">resources/icons/type/Foreign Key@2x.png</file>\n        <file alias=\"Framework.png\">resources/icons/type/Framework.png</file>\n        <file alias=\"Framework@2x.png\">resources/icons/type/Framework@2x.png</file>\n        <file alias=\"Function.png\">resources/icons/type/Function.png</file>\n        <file alias=\"Function@2x.png\">resources/icons/type/Function@2x.png</file>\n        <file alias=\"Global.png\">resources/icons/type/Global.png</file>\n        <file alias=\"Global@2x.png\">resources/icons/type/Global@2x.png</file>\n        <file alias=\"Glossary.png\">resources/icons/type/Glossary.png</file>\n        <file alias=\"Glossary@2x.png\">resources/icons/type/Glossary@2x.png</file>\n        <file alias=\"Guide.png\">resources/icons/type/Guide.png</file>\n        <file alias=\"Guide@2x.png\">resources/icons/type/Guide@2x.png</file>\n        <file alias=\"Handler.png\">resources/icons/type/Handler.png</file>\n        <file alias=\"Handler@2x.png\">resources/icons/type/Handler@2x.png</file>\n        <file alias=\"Helper.png\">resources/icons/type/Helper.png</file>\n        <file alias=\"Helper@2x.png\">resources/icons/type/Helper@2x.png</file>\n        <file alias=\"Hook.png\">resources/icons/type/Hook.png</file>\n        <file alias=\"Hook@2x.png\">resources/icons/type/Hook@2x.png</file>\n        <file alias=\"Index.png\">resources/icons/type/Index.png</file>\n        <file alias=\"Index@2x.png\">resources/icons/type/Index@2x.png</file>\n        <file alias=\"Indirection.png\">resources/icons/type/Indirection.png</file>\n        <file alias=\"Indirection@2x.png\">resources/icons/type/Indirection@2x.png</file>\n        <file alias=\"Inductive.png\">resources/icons/type/Inductive.png</file>\n        <file alias=\"Inductive@2x.png\">resources/icons/type/Inductive@2x.png</file>\n        <file alias=\"Instance.png\">resources/icons/type/Instance.png</file>\n        <file alias=\"Instance@2x.png\">resources/icons/type/Instance@2x.png</file>\n        <file alias=\"Instruction.png\">resources/icons/type/Instruction.png</file>\n        <file alias=\"Instruction@2x.png\">resources/icons/type/Instruction@2x.png</file>\n        <file alias=\"Interface.png\">resources/icons/type/Interface.png</file>\n        <file alias=\"Interface@2x.png\">resources/icons/type/Interface@2x.png</file>\n        <file alias=\"Iterator.png\">resources/icons/type/Iterator.png</file>\n        <file alias=\"Iterator@2x.png\">resources/icons/type/Iterator@2x.png</file>\n        <file alias=\"Keyword.png\">resources/icons/type/Keyword.png</file>\n        <file alias=\"Keyword@2x.png\">resources/icons/type/Keyword@2x.png</file>\n        <file alias=\"Kind.png\">resources/icons/type/Kind.png</file>\n        <file alias=\"Kind@2x.png\">resources/icons/type/Kind@2x.png</file>\n        <file alias=\"Lemma.png\">resources/icons/type/Lemma.png</file>\n        <file alias=\"Lemma@2x.png\">resources/icons/type/Lemma@2x.png</file>\n        <file alias=\"Library.png\">resources/icons/type/Library.png</file>\n        <file alias=\"Library@2x.png\">resources/icons/type/Library@2x.png</file>\n        <file alias=\"Literal.png\">resources/icons/type/Literal.png</file>\n        <file alias=\"Literal@2x.png\">resources/icons/type/Literal@2x.png</file>\n        <file alias=\"Macro.png\">resources/icons/type/Macro.png</file>\n        <file alias=\"Macro@2x.png\">resources/icons/type/Macro@2x.png</file>\n        <file alias=\"Member.png\">resources/icons/type/Member.png</file>\n        <file alias=\"Member@2x.png\">resources/icons/type/Member@2x.png</file>\n        <file alias=\"Message.png\">resources/icons/type/Message.png</file>\n        <file alias=\"Message@2x.png\">resources/icons/type/Message@2x.png</file>\n        <file alias=\"Method.png\">resources/icons/type/Method.png</file>\n        <file alias=\"Method@2x.png\">resources/icons/type/Method@2x.png</file>\n        <file alias=\"Mixin.png\">resources/icons/type/Mixin.png</file>\n        <file alias=\"Mixin@2x.png\">resources/icons/type/Mixin@2x.png</file>\n        <file alias=\"Modifier.png\">resources/icons/type/Modifier.png</file>\n        <file alias=\"Modifier@2x.png\">resources/icons/type/Modifier@2x.png</file>\n        <file alias=\"Module.png\">resources/icons/type/Module.png</file>\n        <file alias=\"Module@2x.png\">resources/icons/type/Module@2x.png</file>\n        <file alias=\"Namespace.png\">resources/icons/type/Namespace.png</file>\n        <file alias=\"Namespace@2x.png\">resources/icons/type/Namespace@2x.png</file>\n        <file alias=\"NewSnippet.png\">resources/icons/type/NewSnippet.png</file>\n        <file alias=\"NewSnippet@2x.png\">resources/icons/type/NewSnippet@2x.png</file>\n        <file alias=\"Node.png\">resources/icons/type/Node.png</file>\n        <file alias=\"Node@2x.png\">resources/icons/type/Node@2x.png</file>\n        <file alias=\"Notation.png\">resources/icons/type/Notation.png</file>\n        <file alias=\"Notation@2x.png\">resources/icons/type/Notation@2x.png</file>\n        <file alias=\"Object.png\">resources/icons/type/Object.png</file>\n        <file alias=\"Object@2x.png\">resources/icons/type/Object@2x.png</file>\n        <file alias=\"Operator.png\">resources/icons/type/Operator.png</file>\n        <file alias=\"Operator@2x.png\">resources/icons/type/Operator@2x.png</file>\n        <file alias=\"Option.png\">resources/icons/type/Option.png</file>\n        <file alias=\"Option@2x.png\">resources/icons/type/Option@2x.png</file>\n        <file alias=\"Package.png\">resources/icons/type/Package.png</file>\n        <file alias=\"Package@2x.png\">resources/icons/type/Package@2x.png</file>\n        <file alias=\"Parameter.png\">resources/icons/type/Parameter.png</file>\n        <file alias=\"Parameter@2x.png\">resources/icons/type/Parameter@2x.png</file>\n        <file alias=\"Pattern.png\">resources/icons/type/Pattern.png</file>\n        <file alias=\"Pattern@2x.png\">resources/icons/type/Pattern@2x.png</file>\n        <file alias=\"Pipe.png\">resources/icons/type/Pipe.png</file>\n        <file alias=\"Pipe@2x.png\">resources/icons/type/Pipe@2x.png</file>\n        <file alias=\"Plugin.png\">resources/icons/type/Plugin.png</file>\n        <file alias=\"Plugin@2x.png\">resources/icons/type/Plugin@2x.png</file>\n        <file alias=\"Procedure.png\">resources/icons/type/Procedure.png</file>\n        <file alias=\"Procedure@2x.png\">resources/icons/type/Procedure@2x.png</file>\n        <file alias=\"Projection.png\">resources/icons/type/Projection.png</file>\n        <file alias=\"Projection@2x.png\">resources/icons/type/Projection@2x.png</file>\n        <file alias=\"Property.png\">resources/icons/type/Property.png</file>\n        <file alias=\"Property@2x.png\">resources/icons/type/Property@2x.png</file>\n        <file alias=\"Protocol.png\">resources/icons/type/Protocol.png</file>\n        <file alias=\"Protocol@2x.png\">resources/icons/type/Protocol@2x.png</file>\n        <file alias=\"Provider.png\">resources/icons/type/Provider.png</file>\n        <file alias=\"Provider@2x.png\">resources/icons/type/Provider@2x.png</file>\n        <file alias=\"Provisioner.png\">resources/icons/type/Provisioner.png</file>\n        <file alias=\"Provisioner@2x.png\">resources/icons/type/Provisioner@2x.png</file>\n        <file alias=\"Query.png\">resources/icons/type/Query.png</file>\n        <file alias=\"Query@2x.png\">resources/icons/type/Query@2x.png</file>\n        <file alias=\"Reference.png\">resources/icons/type/Reference.png</file>\n        <file alias=\"Reference@2x.png\">resources/icons/type/Reference@2x.png</file>\n        <file alias=\"Register.png\">resources/icons/type/Register.png</file>\n        <file alias=\"Register@2x.png\">resources/icons/type/Register@2x.png</file>\n        <file alias=\"Record.png\">resources/icons/type/Record.png</file>\n        <file alias=\"Record@2x.png\">resources/icons/type/Record@2x.png</file>\n        <file alias=\"Relationship.png\">resources/icons/type/Relationship.png</file>\n        <file alias=\"Relationship@2x.png\">resources/icons/type/Relationship@2x.png</file>\n        <file alias=\"Report.png\">resources/icons/type/Report.png</file>\n        <file alias=\"Report@2x.png\">resources/icons/type/Report@2x.png</file>\n        <file alias=\"Request.png\">resources/icons/type/Request.png</file>\n        <file alias=\"Request@2x.png\">resources/icons/type/Request@2x.png</file>\n        <file alias=\"Resource.png\">resources/icons/type/Resource.png</file>\n        <file alias=\"Resource@2x.png\">resources/icons/type/Resource@2x.png</file>\n        <file alias=\"Role.png\">resources/icons/type/Role.png</file>\n        <file alias=\"Role@2x.png\">resources/icons/type/Role@2x.png</file>\n        <file alias=\"Sample.png\">resources/icons/type/Sample.png</file>\n        <file alias=\"Sample@2x.png\">resources/icons/type/Sample@2x.png</file>\n        <file alias=\"Schema.png\">resources/icons/type/Schema.png</file>\n        <file alias=\"Schema@2x.png\">resources/icons/type/Schema@2x.png</file>\n        <file alias=\"Script.png\">resources/icons/type/Script.png</file>\n        <file alias=\"Script@2x.png\">resources/icons/type/Script@2x.png</file>\n        <file alias=\"Section.png\">resources/icons/type/Section.png</file>\n        <file alias=\"Section@2x.png\">resources/icons/type/Section@2x.png</file>\n        <file alias=\"Sender.png\">resources/icons/type/Sender.png</file>\n        <file alias=\"Sender@2x.png\">resources/icons/type/Sender@2x.png</file>\n        <file alias=\"Service.png\">resources/icons/type/Service.png</file>\n        <file alias=\"Service@2x.png\">resources/icons/type/Service@2x.png</file>\n        <file alias=\"Setting.png\">resources/icons/type/Setting.png</file>\n        <file alias=\"Setting@2x.png\">resources/icons/type/Setting@2x.png</file>\n        <file alias=\"Shortcut.png\">resources/icons/type/Shortcut.png</file>\n        <file alias=\"Shortcut@2x.png\">resources/icons/type/Shortcut@2x.png</file>\n        <file alias=\"Signature.png\">resources/icons/type/Signature.png</file>\n        <file alias=\"Signature@2x.png\">resources/icons/type/Signature@2x.png</file>\n        <file alias=\"Special Form.png\">resources/icons/type/Special Form.png</file>\n        <file alias=\"Special Form@2x.png\">resources/icons/type/Special Form@2x.png</file>\n        <file alias=\"State.png\">resources/icons/type/State.png</file>\n        <file alias=\"State@2x.png\">resources/icons/type/State@2x.png</file>\n        <file alias=\"Statement.png\">resources/icons/type/Statement.png</file>\n        <file alias=\"Statement@2x.png\">resources/icons/type/Statement@2x.png</file>\n        <file alias=\"Structure.png\">resources/icons/type/Structure.png</file>\n        <file alias=\"Structure@2x.png\">resources/icons/type/Structure@2x.png</file>\n        <file alias=\"Style.png\">resources/icons/type/Style.png</file>\n        <file alias=\"Style@2x.png\">resources/icons/type/Style@2x.png</file>\n        <file alias=\"Syntax.png\">resources/icons/type/Syntax.png</file>\n        <file alias=\"Syntax@2x.png\">resources/icons/type/Syntax@2x.png</file>\n        <file alias=\"Subroutine.png\">resources/icons/type/Subroutine.png</file>\n        <file alias=\"Subroutine@2x.png\">resources/icons/type/Subroutine@2x.png</file>\n        <file alias=\"Table.png\">resources/icons/type/Table.png</file>\n        <file alias=\"Table@2x.png\">resources/icons/type/Table@2x.png</file>\n        <file alias=\"Tactic.png\">resources/icons/type/Tactic.png</file>\n        <file alias=\"Tactic@2x.png\">resources/icons/type/Tactic@2x.png</file>\n        <file alias=\"Tag.png\">resources/icons/type/Tag.png</file>\n        <file alias=\"Tag@2x.png\">resources/icons/type/Tag@2x.png</file>\n        <file alias=\"Template.png\">resources/icons/type/Template.png</file>\n        <file alias=\"Template@2x.png\">resources/icons/type/Template@2x.png</file>\n        <file alias=\"Test.png\">resources/icons/type/Test.png</file>\n        <file alias=\"Test@2x.png\">resources/icons/type/Test@2x.png</file>\n        <file alias=\"Trait.png\">resources/icons/type/Trait.png</file>\n        <file alias=\"Trait@2x.png\">resources/icons/type/Trait@2x.png</file>\n        <file alias=\"Trigger.png\">resources/icons/type/Trigger.png</file>\n        <file alias=\"Trigger@2x.png\">resources/icons/type/Trigger@2x.png</file>\n        <file alias=\"Type.png\">resources/icons/type/Type.png</file>\n        <file alias=\"Type@2x.png\">resources/icons/type/Type@2x.png</file>\n        <file alias=\"Union.png\">resources/icons/type/Union.png</file>\n        <file alias=\"Union@2x.png\">resources/icons/type/Union@2x.png</file>\n        <file alias=\"Unknown.png\">resources/icons/type/Unknown.png</file>\n        <file alias=\"Unknown@2x.png\">resources/icons/type/Unknown@2x.png</file>\n        <file alias=\"Value.png\">resources/icons/type/Value.png</file>\n        <file alias=\"Value@2x.png\">resources/icons/type/Value@2x.png</file>\n        <file alias=\"Variable.png\">resources/icons/type/Variable.png</file>\n        <file alias=\"Variable@2x.png\">resources/icons/type/Variable@2x.png</file>\n        <file alias=\"Variant.png\">resources/icons/type/Variant.png</file>\n        <file alias=\"Variant@2x.png\">resources/icons/type/Variant@2x.png</file>\n        <file alias=\"View.png\">resources/icons/type/View.png</file>\n        <file alias=\"View@2x.png\">resources/icons/type/View@2x.png</file>\n        <file alias=\"Widget.png\">resources/icons/type/Widget.png</file>\n        <file alias=\"Widget@2x.png\">resources/icons/type/Widget@2x.png</file>\n        <file alias=\"Word.png\">resources/icons/type/Word.png</file>\n        <file alias=\"Word@2x.png\">resources/icons/type/Word@2x.png</file>\n    </qresource>\n    <qresource prefix=\"/icons/logo\">\n        <file alias=\"64x64.png\">resources/icons/logo/64x64.png</file>\n        <file alias=\"128x128.png\">resources/icons/logo/128x128.png</file>\n        <file alias=\"icon.png\">resources/icons/logo/icon.png</file>\n        <file alias=\"icon@2x.png\">resources/icons/logo/icon@2x.png</file>\n    </qresource>\n</RCC>\n"
  },
  {
    "path": "src/contrib/cpp-httplib/httplib.h",
    "content": "//\n//  httplib.h\n//\n//  Copyright (c) 2026 Yuji Hirose. All rights reserved.\n//  MIT License\n//\n\n#ifndef CPPHTTPLIB_HTTPLIB_H\n#define CPPHTTPLIB_HTTPLIB_H\n\n#define CPPHTTPLIB_VERSION \"0.32.0\"\n#define CPPHTTPLIB_VERSION_NUM \"0x002000\"\n\n/*\n * Platform compatibility check\n */\n\n#if defined(_WIN32) && !defined(_WIN64)\n#if defined(_MSC_VER)\n#pragma message(                                                               \\\n    \"cpp-httplib doesn't support 32-bit Windows. Please use a 64-bit compiler.\")\n#else\n#warning                                                                       \\\n    \"cpp-httplib doesn't support 32-bit Windows. Please use a 64-bit compiler.\"\n#endif\n#elif defined(__SIZEOF_POINTER__) && __SIZEOF_POINTER__ < 8\n#warning                                                                       \\\n    \"cpp-httplib doesn't support 32-bit platforms. Please use a 64-bit compiler.\"\n#elif defined(__SIZEOF_SIZE_T__) && __SIZEOF_SIZE_T__ < 8\n#warning                                                                       \\\n    \"cpp-httplib doesn't support platforms where size_t is less than 64 bits.\"\n#endif\n\n#ifdef _WIN32\n#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00\n#error                                                                         \\\n    \"cpp-httplib doesn't support Windows 8 or lower. Please use Windows 10 or later.\"\n#endif\n#endif\n\n/*\n * Configuration\n */\n\n#ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND\n#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND 5\n#endif\n\n#ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND\n#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND 10000\n#endif\n\n#ifndef CPPHTTPLIB_KEEPALIVE_MAX_COUNT\n#define CPPHTTPLIB_KEEPALIVE_MAX_COUNT 100\n#endif\n\n#ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND\n#define CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND 300\n#endif\n\n#ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND\n#define CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND 0\n#endif\n\n#ifndef CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND\n#define CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND 5\n#endif\n\n#ifndef CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND\n#define CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND 0\n#endif\n\n#ifndef CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND\n#define CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND 5\n#endif\n\n#ifndef CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND\n#define CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND 0\n#endif\n\n#ifndef CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND\n#define CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND 300\n#endif\n\n#ifndef CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND\n#define CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND 0\n#endif\n\n#ifndef CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND\n#define CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND 5\n#endif\n\n#ifndef CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND\n#define CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND 0\n#endif\n\n#ifndef CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND\n#define CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND 0\n#endif\n\n#ifndef CPPHTTPLIB_EXPECT_100_THRESHOLD\n#define CPPHTTPLIB_EXPECT_100_THRESHOLD 1024\n#endif\n\n#ifndef CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND\n#define CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND 1000\n#endif\n\n#ifndef CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD\n#define CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD (1024 * 1024)\n#endif\n\n#ifndef CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND\n#define CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND 50\n#endif\n\n#ifndef CPPHTTPLIB_IDLE_INTERVAL_SECOND\n#define CPPHTTPLIB_IDLE_INTERVAL_SECOND 0\n#endif\n\n#ifndef CPPHTTPLIB_IDLE_INTERVAL_USECOND\n#ifdef _WIN32\n#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 1000\n#else\n#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 0\n#endif\n#endif\n\n#ifndef CPPHTTPLIB_REQUEST_URI_MAX_LENGTH\n#define CPPHTTPLIB_REQUEST_URI_MAX_LENGTH 8192\n#endif\n\n#ifndef CPPHTTPLIB_HEADER_MAX_LENGTH\n#define CPPHTTPLIB_HEADER_MAX_LENGTH 8192\n#endif\n\n#ifndef CPPHTTPLIB_HEADER_MAX_COUNT\n#define CPPHTTPLIB_HEADER_MAX_COUNT 100\n#endif\n\n#ifndef CPPHTTPLIB_REDIRECT_MAX_COUNT\n#define CPPHTTPLIB_REDIRECT_MAX_COUNT 20\n#endif\n\n#ifndef CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT\n#define CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT 1024\n#endif\n\n#ifndef CPPHTTPLIB_PAYLOAD_MAX_LENGTH\n#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH (100 * 1024 * 1024) // 100MB\n#endif\n\n#ifndef CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH\n#define CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH 8192\n#endif\n\n#ifndef CPPHTTPLIB_RANGE_MAX_COUNT\n#define CPPHTTPLIB_RANGE_MAX_COUNT 1024\n#endif\n\n#ifndef CPPHTTPLIB_TCP_NODELAY\n#define CPPHTTPLIB_TCP_NODELAY false\n#endif\n\n#ifndef CPPHTTPLIB_IPV6_V6ONLY\n#define CPPHTTPLIB_IPV6_V6ONLY false\n#endif\n\n#ifndef CPPHTTPLIB_RECV_BUFSIZ\n#define CPPHTTPLIB_RECV_BUFSIZ size_t(16384u)\n#endif\n\n#ifndef CPPHTTPLIB_SEND_BUFSIZ\n#define CPPHTTPLIB_SEND_BUFSIZ size_t(16384u)\n#endif\n\n#ifndef CPPHTTPLIB_COMPRESSION_BUFSIZ\n#define CPPHTTPLIB_COMPRESSION_BUFSIZ size_t(16384u)\n#endif\n\n#ifndef CPPHTTPLIB_THREAD_POOL_COUNT\n#define CPPHTTPLIB_THREAD_POOL_COUNT                                           \\\n  ((std::max)(8u, std::thread::hardware_concurrency() > 0                      \\\n                      ? std::thread::hardware_concurrency() - 1                \\\n                      : 0))\n#endif\n\n#ifndef CPPHTTPLIB_RECV_FLAGS\n#define CPPHTTPLIB_RECV_FLAGS 0\n#endif\n\n#ifndef CPPHTTPLIB_SEND_FLAGS\n#define CPPHTTPLIB_SEND_FLAGS 0\n#endif\n\n#ifndef CPPHTTPLIB_LISTEN_BACKLOG\n#define CPPHTTPLIB_LISTEN_BACKLOG 5\n#endif\n\n#ifndef CPPHTTPLIB_MAX_LINE_LENGTH\n#define CPPHTTPLIB_MAX_LINE_LENGTH 32768\n#endif\n\n/*\n * Headers\n */\n\n#ifdef _WIN32\n#ifndef _CRT_SECURE_NO_WARNINGS\n#define _CRT_SECURE_NO_WARNINGS\n#endif //_CRT_SECURE_NO_WARNINGS\n\n#ifndef _CRT_NONSTDC_NO_DEPRECATE\n#define _CRT_NONSTDC_NO_DEPRECATE\n#endif //_CRT_NONSTDC_NO_DEPRECATE\n\n#if defined(_MSC_VER)\n#if _MSC_VER < 1900\n#error Sorry, Visual Studio versions prior to 2015 are not supported\n#endif\n\n#pragma comment(lib, \"ws2_32.lib\")\n\n#ifndef _SSIZE_T_DEFINED\nusing ssize_t = __int64;\n#define _SSIZE_T_DEFINED\n#endif\n#endif // _MSC_VER\n\n#ifndef S_ISREG\n#define S_ISREG(m) (((m) & S_IFREG) == S_IFREG)\n#endif // S_ISREG\n\n#ifndef S_ISDIR\n#define S_ISDIR(m) (((m) & S_IFDIR) == S_IFDIR)\n#endif // S_ISDIR\n\n#ifndef NOMINMAX\n#define NOMINMAX\n#endif // NOMINMAX\n\n#include <io.h>\n#include <winsock2.h>\n#include <ws2tcpip.h>\n\n#if defined(__has_include)\n#if __has_include(<afunix.h>)\n// afunix.h uses types declared in winsock2.h, so has to be included after it.\n#include <afunix.h>\n#define CPPHTTPLIB_HAVE_AFUNIX_H 1\n#endif\n#endif\n\n#ifndef WSA_FLAG_NO_HANDLE_INHERIT\n#define WSA_FLAG_NO_HANDLE_INHERIT 0x80\n#endif\n\nusing nfds_t = unsigned long;\nusing socket_t = SOCKET;\nusing socklen_t = int;\n\n#else // not _WIN32\n\n#include <arpa/inet.h>\n#if !defined(_AIX) && !defined(__MVS__)\n#include <ifaddrs.h>\n#endif\n#ifdef __MVS__\n#include <strings.h>\n#ifndef NI_MAXHOST\n#define NI_MAXHOST 1025\n#endif\n#endif\n#include <net/if.h>\n#include <netdb.h>\n#include <netinet/in.h>\n#ifdef __linux__\n#include <resolv.h>\n#undef _res // Undefine _res macro to avoid conflicts with user code (#2278)\n#endif\n#include <csignal>\n#include <netinet/tcp.h>\n#include <poll.h>\n#include <pthread.h>\n#include <sys/mman.h>\n#include <sys/socket.h>\n#include <sys/un.h>\n#include <unistd.h>\n\nusing socket_t = int;\n#ifndef INVALID_SOCKET\n#define INVALID_SOCKET (-1)\n#endif\n#endif //_WIN32\n\n#if defined(__APPLE__)\n#include <TargetConditionals.h>\n#endif\n\n#include <algorithm>\n#include <array>\n#include <atomic>\n#include <cassert>\n#include <cctype>\n#include <chrono>\n#include <climits>\n#include <condition_variable>\n#include <cstdlib>\n#include <cstring>\n#include <errno.h>\n#include <exception>\n#include <fcntl.h>\n#include <functional>\n#include <iomanip>\n#include <iostream>\n#include <list>\n#include <map>\n#include <memory>\n#include <mutex>\n#include <random>\n#include <regex>\n#include <set>\n#include <sstream>\n#include <string>\n#include <sys/stat.h>\n#include <system_error>\n#include <thread>\n#include <unordered_map>\n#include <unordered_set>\n#include <utility>\n\n#if defined(CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO) ||                        \\\n    defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)\n#if TARGET_OS_MAC\n#include <CFNetwork/CFHost.h>\n#include <CoreFoundation/CoreFoundation.h>\n#endif\n#endif // CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO or\n       // CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN\n\n#ifdef CPPHTTPLIB_OPENSSL_SUPPORT\n#ifdef _WIN32\n#include <wincrypt.h>\n\n// these are defined in wincrypt.h and it breaks compilation if BoringSSL is\n// used\n#undef X509_NAME\n#undef X509_CERT_PAIR\n#undef X509_EXTENSIONS\n#undef PKCS7_SIGNER_INFO\n\n#ifdef _MSC_VER\n#pragma comment(lib, \"crypt32.lib\")\n#endif\n#endif // _WIN32\n\n#if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)\n#if TARGET_OS_MAC\n#include <Security/Security.h>\n#endif\n#endif // CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO\n\n#include <openssl/err.h>\n#include <openssl/evp.h>\n#include <openssl/ssl.h>\n#include <openssl/x509v3.h>\n\n#if defined(_WIN32) && defined(OPENSSL_USE_APPLINK)\n#include <openssl/applink.c>\n#endif\n\n#include <iostream>\n#include <sstream>\n\n#if defined(OPENSSL_IS_BORINGSSL) || defined(LIBRESSL_VERSION_NUMBER)\n#if OPENSSL_VERSION_NUMBER < 0x1010107f\n#error Please use OpenSSL or a current version of BoringSSL\n#endif\n#define SSL_get1_peer_certificate SSL_get_peer_certificate\n#elif OPENSSL_VERSION_NUMBER < 0x30000000L\n#error Sorry, OpenSSL versions prior to 3.0.0 are not supported\n#endif\n\n#endif // CPPHTTPLIB_OPENSSL_SUPPORT\n\n#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT\n#include <mbedtls/ctr_drbg.h>\n#include <mbedtls/entropy.h>\n#include <mbedtls/error.h>\n#include <mbedtls/md5.h>\n#include <mbedtls/net_sockets.h>\n#include <mbedtls/oid.h>\n#include <mbedtls/pk.h>\n#include <mbedtls/sha1.h>\n#include <mbedtls/sha256.h>\n#include <mbedtls/sha512.h>\n#include <mbedtls/ssl.h>\n#include <mbedtls/x509_crt.h>\n#ifdef _WIN32\n#include <wincrypt.h>\n#ifdef _MSC_VER\n#pragma comment(lib, \"crypt32.lib\")\n#endif\n#endif // _WIN32\n#if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)\n#if TARGET_OS_MAC\n#include <Security/Security.h>\n#endif\n#endif // CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN\n\n// Mbed TLS 3.x API compatibility\n#if MBEDTLS_VERSION_MAJOR >= 3\n#define CPPHTTPLIB_MBEDTLS_V3\n#endif\n\n#endif // CPPHTTPLIB_MBEDTLS_SUPPORT\n\n// Define CPPHTTPLIB_SSL_ENABLED if any SSL backend is available\n// This simplifies conditional compilation when adding new backends (e.g.,\n// wolfSSL)\n#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) || defined(CPPHTTPLIB_MBEDTLS_SUPPORT)\n#define CPPHTTPLIB_SSL_ENABLED\n#endif\n\n#ifdef CPPHTTPLIB_ZLIB_SUPPORT\n#include <zlib.h>\n#endif\n\n#ifdef CPPHTTPLIB_BROTLI_SUPPORT\n#include <brotli/decode.h>\n#include <brotli/encode.h>\n#endif\n\n#ifdef CPPHTTPLIB_ZSTD_SUPPORT\n#include <zstd.h>\n#endif\n\n/*\n * Declaration\n */\nnamespace httplib {\n\nnamespace detail {\n\n/*\n * Backport std::make_unique from C++14.\n *\n * NOTE: This code came up with the following stackoverflow post:\n * https://stackoverflow.com/questions/10149840/c-arrays-and-make-unique\n *\n */\n\ntemplate <class T, class... Args>\ntypename std::enable_if<!std::is_array<T>::value, std::unique_ptr<T>>::type\nmake_unique(Args &&...args) {\n  return std::unique_ptr<T>(new T(std::forward<Args>(args)...));\n}\n\ntemplate <class T>\ntypename std::enable_if<std::is_array<T>::value, std::unique_ptr<T>>::type\nmake_unique(std::size_t n) {\n  typedef typename std::remove_extent<T>::type RT;\n  return std::unique_ptr<T>(new RT[n]);\n}\n\nnamespace case_ignore {\n\ninline unsigned char to_lower(int c) {\n  const static unsigned char table[256] = {\n      0,   1,   2,   3,   4,   5,   6,   7,   8,   9,   10,  11,  12,  13,  14,\n      15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25,  26,  27,  28,  29,\n      30,  31,  32,  33,  34,  35,  36,  37,  38,  39,  40,  41,  42,  43,  44,\n      45,  46,  47,  48,  49,  50,  51,  52,  53,  54,  55,  56,  57,  58,  59,\n      60,  61,  62,  63,  64,  97,  98,  99,  100, 101, 102, 103, 104, 105, 106,\n      107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121,\n      122, 91,  92,  93,  94,  95,  96,  97,  98,  99,  100, 101, 102, 103, 104,\n      105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,\n      120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134,\n      135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149,\n      150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164,\n      165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179,\n      180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 224, 225, 226,\n      227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241,\n      242, 243, 244, 245, 246, 215, 248, 249, 250, 251, 252, 253, 254, 223, 224,\n      225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239,\n      240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254,\n      255,\n  };\n  return table[(unsigned char)(char)c];\n}\n\ninline bool equal(const std::string &a, const std::string &b) {\n  return a.size() == b.size() &&\n         std::equal(a.begin(), a.end(), b.begin(), [](char ca, char cb) {\n           return to_lower(ca) == to_lower(cb);\n         });\n}\n\nstruct equal_to {\n  bool operator()(const std::string &a, const std::string &b) const {\n    return equal(a, b);\n  }\n};\n\nstruct hash {\n  size_t operator()(const std::string &key) const {\n    return hash_core(key.data(), key.size(), 0);\n  }\n\n  size_t hash_core(const char *s, size_t l, size_t h) const {\n    return (l == 0) ? h\n                    : hash_core(s + 1, l - 1,\n                                // Unsets the 6 high bits of h, therefore no\n                                // overflow happens\n                                (((std::numeric_limits<size_t>::max)() >> 6) &\n                                 h * 33) ^\n                                    static_cast<unsigned char>(to_lower(*s)));\n  }\n};\n\ntemplate <typename T>\nusing unordered_set = std::unordered_set<T, detail::case_ignore::hash,\n                                         detail::case_ignore::equal_to>;\n\n} // namespace case_ignore\n\n// This is based on\n// \"http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189\".\n\nstruct scope_exit {\n  explicit scope_exit(std::function<void(void)> &&f)\n      : exit_function(std::move(f)), execute_on_destruction{true} {}\n\n  scope_exit(scope_exit &&rhs) noexcept\n      : exit_function(std::move(rhs.exit_function)),\n        execute_on_destruction{rhs.execute_on_destruction} {\n    rhs.release();\n  }\n\n  ~scope_exit() {\n    if (execute_on_destruction) { this->exit_function(); }\n  }\n\n  void release() { this->execute_on_destruction = false; }\n\nprivate:\n  scope_exit(const scope_exit &) = delete;\n  void operator=(const scope_exit &) = delete;\n  scope_exit &operator=(scope_exit &&) = delete;\n\n  std::function<void(void)> exit_function;\n  bool execute_on_destruction;\n};\n\n// Simple from_chars implementation for integer and double types (C++17\n// substitute)\ntemplate <typename T> struct from_chars_result {\n  const char *ptr;\n  std::errc ec;\n};\n\ntemplate <typename T>\ninline from_chars_result<T> from_chars(const char *first, const char *last,\n                                       T &value, int base = 10) {\n  value = 0;\n  const char *p = first;\n  bool negative = false;\n\n  if (p != last && *p == '-') {\n    negative = true;\n    ++p;\n  }\n  if (p == last) { return {first, std::errc::invalid_argument}; }\n\n  T result = 0;\n  for (; p != last; ++p) {\n    char c = *p;\n    int digit = -1;\n    if ('0' <= c && c <= '9') {\n      digit = c - '0';\n    } else if ('a' <= c && c <= 'z') {\n      digit = c - 'a' + 10;\n    } else if ('A' <= c && c <= 'Z') {\n      digit = c - 'A' + 10;\n    } else {\n      break;\n    }\n\n    if (digit < 0 || digit >= base) { break; }\n    if (result > ((std::numeric_limits<T>::max)() - digit) / base) {\n      return {p, std::errc::result_out_of_range};\n    }\n    result = result * base + digit;\n  }\n\n  if (p == first || (negative && p == first + 1)) {\n    return {first, std::errc::invalid_argument};\n  }\n\n  value = negative ? -result : result;\n  return {p, std::errc{}};\n}\n\n// from_chars for double (simple wrapper for strtod)\ninline from_chars_result<double> from_chars(const char *first, const char *last,\n                                            double &value) {\n  std::string s(first, last);\n  char *endptr = nullptr;\n  errno = 0;\n  value = std::strtod(s.c_str(), &endptr);\n  if (endptr == s.c_str()) { return {first, std::errc::invalid_argument}; }\n  if (errno == ERANGE) {\n    return {first + (endptr - s.c_str()), std::errc::result_out_of_range};\n  }\n  return {first + (endptr - s.c_str()), std::errc{}};\n}\n\n} // namespace detail\n\nenum SSLVerifierResponse {\n  // no decision has been made, use the built-in certificate verifier\n  NoDecisionMade,\n  // connection certificate is verified and accepted\n  CertificateAccepted,\n  // connection certificate was processed but is rejected\n  CertificateRejected\n};\n\nenum StatusCode {\n  // Information responses\n  Continue_100 = 100,\n  SwitchingProtocol_101 = 101,\n  Processing_102 = 102,\n  EarlyHints_103 = 103,\n\n  // Successful responses\n  OK_200 = 200,\n  Created_201 = 201,\n  Accepted_202 = 202,\n  NonAuthoritativeInformation_203 = 203,\n  NoContent_204 = 204,\n  ResetContent_205 = 205,\n  PartialContent_206 = 206,\n  MultiStatus_207 = 207,\n  AlreadyReported_208 = 208,\n  IMUsed_226 = 226,\n\n  // Redirection messages\n  MultipleChoices_300 = 300,\n  MovedPermanently_301 = 301,\n  Found_302 = 302,\n  SeeOther_303 = 303,\n  NotModified_304 = 304,\n  UseProxy_305 = 305,\n  unused_306 = 306,\n  TemporaryRedirect_307 = 307,\n  PermanentRedirect_308 = 308,\n\n  // Client error responses\n  BadRequest_400 = 400,\n  Unauthorized_401 = 401,\n  PaymentRequired_402 = 402,\n  Forbidden_403 = 403,\n  NotFound_404 = 404,\n  MethodNotAllowed_405 = 405,\n  NotAcceptable_406 = 406,\n  ProxyAuthenticationRequired_407 = 407,\n  RequestTimeout_408 = 408,\n  Conflict_409 = 409,\n  Gone_410 = 410,\n  LengthRequired_411 = 411,\n  PreconditionFailed_412 = 412,\n  PayloadTooLarge_413 = 413,\n  UriTooLong_414 = 414,\n  UnsupportedMediaType_415 = 415,\n  RangeNotSatisfiable_416 = 416,\n  ExpectationFailed_417 = 417,\n  ImATeapot_418 = 418,\n  MisdirectedRequest_421 = 421,\n  UnprocessableContent_422 = 422,\n  Locked_423 = 423,\n  FailedDependency_424 = 424,\n  TooEarly_425 = 425,\n  UpgradeRequired_426 = 426,\n  PreconditionRequired_428 = 428,\n  TooManyRequests_429 = 429,\n  RequestHeaderFieldsTooLarge_431 = 431,\n  UnavailableForLegalReasons_451 = 451,\n\n  // Server error responses\n  InternalServerError_500 = 500,\n  NotImplemented_501 = 501,\n  BadGateway_502 = 502,\n  ServiceUnavailable_503 = 503,\n  GatewayTimeout_504 = 504,\n  HttpVersionNotSupported_505 = 505,\n  VariantAlsoNegotiates_506 = 506,\n  InsufficientStorage_507 = 507,\n  LoopDetected_508 = 508,\n  NotExtended_510 = 510,\n  NetworkAuthenticationRequired_511 = 511,\n};\n\nusing Headers =\n    std::unordered_multimap<std::string, std::string, detail::case_ignore::hash,\n                            detail::case_ignore::equal_to>;\n\nusing Params = std::multimap<std::string, std::string>;\nusing Match = std::smatch;\n\nusing DownloadProgress = std::function<bool(size_t current, size_t total)>;\nusing UploadProgress = std::function<bool(size_t current, size_t total)>;\n\nstruct Response;\nusing ResponseHandler = std::function<bool(const Response &response)>;\n\nstruct FormData {\n  std::string name;\n  std::string content;\n  std::string filename;\n  std::string content_type;\n  Headers headers;\n};\n\nstruct FormField {\n  std::string name;\n  std::string content;\n  Headers headers;\n};\nusing FormFields = std::multimap<std::string, FormField>;\n\nusing FormFiles = std::multimap<std::string, FormData>;\n\nstruct MultipartFormData {\n  FormFields fields; // Text fields from multipart\n  FormFiles files;   // Files from multipart\n\n  // Text field access\n  std::string get_field(const std::string &key, size_t id = 0) const;\n  std::vector<std::string> get_fields(const std::string &key) const;\n  bool has_field(const std::string &key) const;\n  size_t get_field_count(const std::string &key) const;\n\n  // File access\n  FormData get_file(const std::string &key, size_t id = 0) const;\n  std::vector<FormData> get_files(const std::string &key) const;\n  bool has_file(const std::string &key) const;\n  size_t get_file_count(const std::string &key) const;\n};\n\nstruct UploadFormData {\n  std::string name;\n  std::string content;\n  std::string filename;\n  std::string content_type;\n};\nusing UploadFormDataItems = std::vector<UploadFormData>;\n\nclass DataSink {\npublic:\n  DataSink() : os(&sb_), sb_(*this) {}\n\n  DataSink(const DataSink &) = delete;\n  DataSink &operator=(const DataSink &) = delete;\n  DataSink(DataSink &&) = delete;\n  DataSink &operator=(DataSink &&) = delete;\n\n  std::function<bool(const char *data, size_t data_len)> write;\n  std::function<bool()> is_writable;\n  std::function<void()> done;\n  std::function<void(const Headers &trailer)> done_with_trailer;\n  std::ostream os;\n\nprivate:\n  class data_sink_streambuf final : public std::streambuf {\n  public:\n    explicit data_sink_streambuf(DataSink &sink) : sink_(sink) {}\n\n  protected:\n    std::streamsize xsputn(const char *s, std::streamsize n) override {\n      sink_.write(s, static_cast<size_t>(n));\n      return n;\n    }\n\n  private:\n    DataSink &sink_;\n  };\n\n  data_sink_streambuf sb_;\n};\n\nusing ContentProvider =\n    std::function<bool(size_t offset, size_t length, DataSink &sink)>;\n\nusing ContentProviderWithoutLength =\n    std::function<bool(size_t offset, DataSink &sink)>;\n\nusing ContentProviderResourceReleaser = std::function<void(bool success)>;\n\nstruct FormDataProvider {\n  std::string name;\n  ContentProviderWithoutLength provider;\n  std::string filename;\n  std::string content_type;\n};\nusing FormDataProviderItems = std::vector<FormDataProvider>;\n\nusing ContentReceiverWithProgress = std::function<bool(\n    const char *data, size_t data_length, size_t offset, size_t total_length)>;\n\nusing ContentReceiver =\n    std::function<bool(const char *data, size_t data_length)>;\n\nusing FormDataHeader = std::function<bool(const FormData &file)>;\n\nclass ContentReader {\npublic:\n  using Reader = std::function<bool(ContentReceiver receiver)>;\n  using FormDataReader =\n      std::function<bool(FormDataHeader header, ContentReceiver receiver)>;\n\n  ContentReader(Reader reader, FormDataReader multipart_reader)\n      : reader_(std::move(reader)),\n        formdata_reader_(std::move(multipart_reader)) {}\n\n  bool operator()(FormDataHeader header, ContentReceiver receiver) const {\n    return formdata_reader_(std::move(header), std::move(receiver));\n  }\n\n  bool operator()(ContentReceiver receiver) const {\n    return reader_(std::move(receiver));\n  }\n\n  Reader reader_;\n  FormDataReader formdata_reader_;\n};\n\nusing Range = std::pair<ssize_t, ssize_t>;\nusing Ranges = std::vector<Range>;\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n// TLS abstraction layer - public type definitions and API\nnamespace tls {\n\n// Opaque handles (defined as void* for abstraction)\nusing ctx_t = void *;\nusing session_t = void *;\nusing const_session_t = const void *; // For read-only session access\nusing cert_t = void *;\nusing ca_store_t = void *;\n\n// TLS versions\nenum class Version {\n  TLS1_2 = 0x0303,\n  TLS1_3 = 0x0304,\n};\n\n// Subject Alternative Names (SAN) entry types\nenum class SanType { DNS, IP, EMAIL, URI, OTHER };\n\n// SAN entry structure\nstruct SanEntry {\n  SanType type;\n  std::string value;\n};\n\n// Verification context for certificate verification callback\nstruct VerifyContext {\n  session_t session;        // TLS session handle\n  cert_t cert;              // Current certificate being verified\n  int depth;                // Certificate chain depth (0 = leaf)\n  bool preverify_ok;        // OpenSSL/Mbed TLS pre-verification result\n  long error_code;          // Backend-specific error code (0 = no error)\n  const char *error_string; // Human-readable error description\n\n  // Certificate introspection methods\n  std::string subject_cn() const;\n  std::string issuer_name() const;\n  bool check_hostname(const char *hostname) const;\n  std::vector<SanEntry> sans() const;\n  bool validity(time_t &not_before, time_t &not_after) const;\n  std::string serial() const;\n};\n\nusing VerifyCallback = std::function<bool(const VerifyContext &ctx)>;\n\n// TlsError codes for TLS operations (backend-independent)\nenum class ErrorCode : int {\n  Success = 0,\n  WantRead,         // Non-blocking: need to wait for read\n  WantWrite,        // Non-blocking: need to wait for write\n  PeerClosed,       // Peer closed the connection\n  Fatal,            // Unrecoverable error\n  SyscallError,     // System call error (check sys_errno)\n  CertVerifyFailed, // Certificate verification failed\n  HostnameMismatch, // Hostname verification failed\n};\n\n// TLS error information\nstruct TlsError {\n  ErrorCode code = ErrorCode::Fatal;\n  uint64_t backend_code = 0; // OpenSSL: ERR_get_error(), mbedTLS: return value\n  int sys_errno = 0;         // errno when SyscallError\n\n  // Convert verification error code to human-readable string\n  static std::string verify_error_to_string(long error_code);\n};\n\n// RAII wrapper for peer certificate\nclass PeerCert {\npublic:\n  PeerCert();\n  PeerCert(PeerCert &&other) noexcept;\n  PeerCert &operator=(PeerCert &&other) noexcept;\n  ~PeerCert();\n\n  PeerCert(const PeerCert &) = delete;\n  PeerCert &operator=(const PeerCert &) = delete;\n\n  explicit operator bool() const;\n  std::string subject_cn() const;\n  std::string issuer_name() const;\n  bool check_hostname(const char *hostname) const;\n  std::vector<SanEntry> sans() const;\n  bool validity(time_t &not_before, time_t &not_after) const;\n  std::string serial() const;\n\nprivate:\n  explicit PeerCert(cert_t cert);\n  cert_t cert_ = nullptr;\n  friend PeerCert get_peer_cert_from_session(const_session_t session);\n};\n\n// Callback for TLS context setup (used by SSLServer constructor)\nusing ContextSetupCallback = std::function<bool(ctx_t ctx)>;\n\n} // namespace tls\n#endif\n\nstruct Request {\n  std::string method;\n  std::string path;\n  std::string matched_route;\n  Params params;\n  Headers headers;\n  Headers trailers;\n  std::string body;\n\n  std::string remote_addr;\n  int remote_port = -1;\n  std::string local_addr;\n  int local_port = -1;\n\n  // for server\n  std::string version;\n  std::string target;\n  MultipartFormData form;\n  Ranges ranges;\n  Match matches;\n  std::unordered_map<std::string, std::string> path_params;\n  std::function<bool()> is_connection_closed = []() { return true; };\n\n  // for client\n  std::vector<std::string> accept_content_types;\n  ResponseHandler response_handler;\n  ContentReceiverWithProgress content_receiver;\n  DownloadProgress download_progress;\n  UploadProgress upload_progress;\n\n  bool has_header(const std::string &key) const;\n  std::string get_header_value(const std::string &key, const char *def = \"\",\n                               size_t id = 0) const;\n  size_t get_header_value_u64(const std::string &key, size_t def = 0,\n                              size_t id = 0) const;\n  size_t get_header_value_count(const std::string &key) const;\n  void set_header(const std::string &key, const std::string &val);\n\n  bool has_trailer(const std::string &key) const;\n  std::string get_trailer_value(const std::string &key, size_t id = 0) const;\n  size_t get_trailer_value_count(const std::string &key) const;\n\n  bool has_param(const std::string &key) const;\n  std::string get_param_value(const std::string &key, size_t id = 0) const;\n  size_t get_param_value_count(const std::string &key) const;\n\n  bool is_multipart_form_data() const;\n\n  // private members...\n  size_t redirect_count_ = CPPHTTPLIB_REDIRECT_MAX_COUNT;\n  size_t content_length_ = 0;\n  ContentProvider content_provider_;\n  bool is_chunked_content_provider_ = false;\n  size_t authorization_count_ = 0;\n  std::chrono::time_point<std::chrono::steady_clock> start_time_ =\n      (std::chrono::steady_clock::time_point::min)();\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n  tls::const_session_t ssl = nullptr;\n  tls::PeerCert peer_cert() const;\n  std::string sni() const;\n#endif\n};\n\nstruct Response {\n  std::string version;\n  int status = -1;\n  std::string reason;\n  Headers headers;\n  Headers trailers;\n  std::string body;\n  std::string location; // Redirect location\n\n  bool has_header(const std::string &key) const;\n  std::string get_header_value(const std::string &key, const char *def = \"\",\n                               size_t id = 0) const;\n  size_t get_header_value_u64(const std::string &key, size_t def = 0,\n                              size_t id = 0) const;\n  size_t get_header_value_count(const std::string &key) const;\n  void set_header(const std::string &key, const std::string &val);\n\n  bool has_trailer(const std::string &key) const;\n  std::string get_trailer_value(const std::string &key, size_t id = 0) const;\n  size_t get_trailer_value_count(const std::string &key) const;\n\n  void set_redirect(const std::string &url, int status = StatusCode::Found_302);\n  void set_content(const char *s, size_t n, const std::string &content_type);\n  void set_content(const std::string &s, const std::string &content_type);\n  void set_content(std::string &&s, const std::string &content_type);\n\n  void set_content_provider(\n      size_t length, const std::string &content_type, ContentProvider provider,\n      ContentProviderResourceReleaser resource_releaser = nullptr);\n\n  void set_content_provider(\n      const std::string &content_type, ContentProviderWithoutLength provider,\n      ContentProviderResourceReleaser resource_releaser = nullptr);\n\n  void set_chunked_content_provider(\n      const std::string &content_type, ContentProviderWithoutLength provider,\n      ContentProviderResourceReleaser resource_releaser = nullptr);\n\n  void set_file_content(const std::string &path,\n                        const std::string &content_type);\n  void set_file_content(const std::string &path);\n\n  Response() = default;\n  Response(const Response &) = default;\n  Response &operator=(const Response &) = default;\n  Response(Response &&) = default;\n  Response &operator=(Response &&) = default;\n  ~Response() {\n    if (content_provider_resource_releaser_) {\n      content_provider_resource_releaser_(content_provider_success_);\n    }\n  }\n\n  // private members...\n  size_t content_length_ = 0;\n  ContentProvider content_provider_;\n  ContentProviderResourceReleaser content_provider_resource_releaser_;\n  bool is_chunked_content_provider_ = false;\n  bool content_provider_success_ = false;\n  std::string file_content_path_;\n  std::string file_content_content_type_;\n};\n\nenum class Error {\n  Success = 0,\n  Unknown,\n  Connection,\n  BindIPAddress,\n  Read,\n  Write,\n  ExceedRedirectCount,\n  Canceled,\n  SSLConnection,\n  SSLLoadingCerts,\n  SSLServerVerification,\n  SSLServerHostnameVerification,\n  UnsupportedMultipartBoundaryChars,\n  Compression,\n  ConnectionTimeout,\n  ProxyConnection,\n  ConnectionClosed,\n  Timeout,\n  ResourceExhaustion,\n  TooManyFormDataFiles,\n  ExceedMaxPayloadSize,\n  ExceedUriMaxLength,\n  ExceedMaxSocketDescriptorCount,\n  InvalidRequestLine,\n  InvalidHTTPMethod,\n  InvalidHTTPVersion,\n  InvalidHeaders,\n  MultipartParsing,\n  OpenFile,\n  Listen,\n  GetSockName,\n  UnsupportedAddressFamily,\n  HTTPParsing,\n  InvalidRangeHeader,\n\n  // For internal use only\n  SSLPeerCouldBeClosed_,\n};\n\nstd::string to_string(Error error);\n\nstd::ostream &operator<<(std::ostream &os, const Error &obj);\n\nclass Stream {\npublic:\n  virtual ~Stream() = default;\n\n  virtual bool is_readable() const = 0;\n  virtual bool wait_readable() const = 0;\n  virtual bool wait_writable() const = 0;\n\n  virtual ssize_t read(char *ptr, size_t size) = 0;\n  virtual ssize_t write(const char *ptr, size_t size) = 0;\n  virtual void get_remote_ip_and_port(std::string &ip, int &port) const = 0;\n  virtual void get_local_ip_and_port(std::string &ip, int &port) const = 0;\n  virtual socket_t socket() const = 0;\n\n  virtual time_t duration() const = 0;\n\n  ssize_t write(const char *ptr);\n  ssize_t write(const std::string &s);\n\n  Error get_error() const { return error_; }\n\nprotected:\n  Error error_ = Error::Success;\n};\n\nclass TaskQueue {\npublic:\n  TaskQueue() = default;\n  virtual ~TaskQueue() = default;\n\n  virtual bool enqueue(std::function<void()> fn) = 0;\n  virtual void shutdown() = 0;\n\n  virtual void on_idle() {}\n};\n\nclass ThreadPool final : public TaskQueue {\npublic:\n  explicit ThreadPool(size_t n, size_t mqr = 0);\n  ThreadPool(const ThreadPool &) = delete;\n  ~ThreadPool() override = default;\n\n  bool enqueue(std::function<void()> fn) override;\n  void shutdown() override;\n\nprivate:\n  struct worker {\n    explicit worker(ThreadPool &pool);\n\n    void operator()();\n\n    ThreadPool &pool_;\n  };\n  friend struct worker;\n\n  std::vector<std::thread> threads_;\n  std::list<std::function<void()>> jobs_;\n\n  bool shutdown_;\n  size_t max_queued_requests_ = 0;\n\n  std::condition_variable cond_;\n  std::mutex mutex_;\n};\n\nusing Logger = std::function<void(const Request &, const Response &)>;\n\n// Forward declaration for Error type\nenum class Error;\nusing ErrorLogger = std::function<void(const Error &, const Request *)>;\n\nusing SocketOptions = std::function<void(socket_t sock)>;\n\nvoid default_socket_options(socket_t sock);\n\nconst char *status_message(int status);\n\nstd::string to_string(Error error);\n\nstd::ostream &operator<<(std::ostream &os, const Error &obj);\n\nstd::string get_bearer_token_auth(const Request &req);\n\nnamespace detail {\n\nclass MatcherBase {\npublic:\n  MatcherBase(std::string pattern) : pattern_(std::move(pattern)) {}\n  virtual ~MatcherBase() = default;\n\n  const std::string &pattern() const { return pattern_; }\n\n  // Match request path and populate its matches and\n  virtual bool match(Request &request) const = 0;\n\nprivate:\n  std::string pattern_;\n};\n\n/**\n * Captures parameters in request path and stores them in Request::path_params\n *\n * Capture name is a substring of a pattern from : to /.\n * The rest of the pattern is matched against the request path directly\n * Parameters are captured starting from the next character after\n * the end of the last matched static pattern fragment until the next /.\n *\n * Example pattern:\n * \"/path/fragments/:capture/more/fragments/:second_capture\"\n * Static fragments:\n * \"/path/fragments/\", \"more/fragments/\"\n *\n * Given the following request path:\n * \"/path/fragments/:1/more/fragments/:2\"\n * the resulting capture will be\n * {{\"capture\", \"1\"}, {\"second_capture\", \"2\"}}\n */\nclass PathParamsMatcher final : public MatcherBase {\npublic:\n  PathParamsMatcher(const std::string &pattern);\n\n  bool match(Request &request) const override;\n\nprivate:\n  // Treat segment separators as the end of path parameter capture\n  // Does not need to handle query parameters as they are parsed before path\n  // matching\n  static constexpr char separator = '/';\n\n  // Contains static path fragments to match against, excluding the '/' after\n  // path params\n  // Fragments are separated by path params\n  std::vector<std::string> static_fragments_;\n  // Stores the names of the path parameters to be used as keys in the\n  // Request::path_params map\n  std::vector<std::string> param_names_;\n};\n\n/**\n * Performs std::regex_match on request path\n * and stores the result in Request::matches\n *\n * Note that regex match is performed directly on the whole request.\n * This means that wildcard patterns may match multiple path segments with /:\n * \"/begin/(.*)/end\" will match both \"/begin/middle/end\" and \"/begin/1/2/end\".\n */\nclass RegexMatcher final : public MatcherBase {\npublic:\n  RegexMatcher(const std::string &pattern)\n      : MatcherBase(pattern), regex_(pattern) {}\n\n  bool match(Request &request) const override;\n\nprivate:\n  std::regex regex_;\n};\n\nint close_socket(socket_t sock);\n\nssize_t write_headers(Stream &strm, const Headers &headers);\n\nbool set_socket_opt_time(socket_t sock, int level, int optname, time_t sec,\n                         time_t usec);\n\n} // namespace detail\n\nclass Server {\npublic:\n  using Handler = std::function<void(const Request &, Response &)>;\n\n  using ExceptionHandler =\n      std::function<void(const Request &, Response &, std::exception_ptr ep)>;\n\n  enum class HandlerResponse {\n    Handled,\n    Unhandled,\n  };\n  using HandlerWithResponse =\n      std::function<HandlerResponse(const Request &, Response &)>;\n\n  using HandlerWithContentReader = std::function<void(\n      const Request &, Response &, const ContentReader &content_reader)>;\n\n  using Expect100ContinueHandler =\n      std::function<int(const Request &, Response &)>;\n\n  Server();\n\n  virtual ~Server();\n\n  virtual bool is_valid() const;\n\n  Server &Get(const std::string &pattern, Handler handler);\n  Server &Post(const std::string &pattern, Handler handler);\n  Server &Post(const std::string &pattern, HandlerWithContentReader handler);\n  Server &Put(const std::string &pattern, Handler handler);\n  Server &Put(const std::string &pattern, HandlerWithContentReader handler);\n  Server &Patch(const std::string &pattern, Handler handler);\n  Server &Patch(const std::string &pattern, HandlerWithContentReader handler);\n  Server &Delete(const std::string &pattern, Handler handler);\n  Server &Delete(const std::string &pattern, HandlerWithContentReader handler);\n  Server &Options(const std::string &pattern, Handler handler);\n\n  bool set_base_dir(const std::string &dir,\n                    const std::string &mount_point = std::string());\n  bool set_mount_point(const std::string &mount_point, const std::string &dir,\n                       Headers headers = Headers());\n  bool remove_mount_point(const std::string &mount_point);\n  Server &set_file_extension_and_mimetype_mapping(const std::string &ext,\n                                                  const std::string &mime);\n  Server &set_default_file_mimetype(const std::string &mime);\n  Server &set_file_request_handler(Handler handler);\n\n  template <class ErrorHandlerFunc>\n  Server &set_error_handler(ErrorHandlerFunc &&handler) {\n    return set_error_handler_core(\n        std::forward<ErrorHandlerFunc>(handler),\n        std::is_convertible<ErrorHandlerFunc, HandlerWithResponse>{});\n  }\n\n  Server &set_exception_handler(ExceptionHandler handler);\n\n  Server &set_pre_routing_handler(HandlerWithResponse handler);\n  Server &set_post_routing_handler(Handler handler);\n\n  Server &set_pre_request_handler(HandlerWithResponse handler);\n\n  Server &set_expect_100_continue_handler(Expect100ContinueHandler handler);\n  Server &set_logger(Logger logger);\n  Server &set_pre_compression_logger(Logger logger);\n  Server &set_error_logger(ErrorLogger error_logger);\n\n  Server &set_address_family(int family);\n  Server &set_tcp_nodelay(bool on);\n  Server &set_ipv6_v6only(bool on);\n  Server &set_socket_options(SocketOptions socket_options);\n\n  Server &set_default_headers(Headers headers);\n  Server &\n  set_header_writer(std::function<ssize_t(Stream &, Headers &)> const &writer);\n\n  Server &set_trusted_proxies(const std::vector<std::string> &proxies);\n\n  Server &set_keep_alive_max_count(size_t count);\n  Server &set_keep_alive_timeout(time_t sec);\n\n  Server &set_read_timeout(time_t sec, time_t usec = 0);\n  template <class Rep, class Period>\n  Server &set_read_timeout(const std::chrono::duration<Rep, Period> &duration);\n\n  Server &set_write_timeout(time_t sec, time_t usec = 0);\n  template <class Rep, class Period>\n  Server &set_write_timeout(const std::chrono::duration<Rep, Period> &duration);\n\n  Server &set_idle_interval(time_t sec, time_t usec = 0);\n  template <class Rep, class Period>\n  Server &set_idle_interval(const std::chrono::duration<Rep, Period> &duration);\n\n  Server &set_payload_max_length(size_t length);\n\n  bool bind_to_port(const std::string &host, int port, int socket_flags = 0);\n  int bind_to_any_port(const std::string &host, int socket_flags = 0);\n  bool listen_after_bind();\n\n  bool listen(const std::string &host, int port, int socket_flags = 0);\n\n  bool is_running() const;\n  void wait_until_ready() const;\n  void stop();\n  void decommission();\n\n  std::function<TaskQueue *(void)> new_task_queue;\n\nprotected:\n  bool process_request(Stream &strm, const std::string &remote_addr,\n                       int remote_port, const std::string &local_addr,\n                       int local_port, bool close_connection,\n                       bool &connection_closed,\n                       const std::function<void(Request &)> &setup_request);\n\n  std::atomic<socket_t> svr_sock_{INVALID_SOCKET};\n\n  std::vector<std::string> trusted_proxies_;\n\n  size_t keep_alive_max_count_ = CPPHTTPLIB_KEEPALIVE_MAX_COUNT;\n  time_t keep_alive_timeout_sec_ = CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND;\n  time_t read_timeout_sec_ = CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND;\n  time_t read_timeout_usec_ = CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND;\n  time_t write_timeout_sec_ = CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND;\n  time_t write_timeout_usec_ = CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND;\n  time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND;\n  time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND;\n  size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;\n\nprivate:\n  using Handlers =\n      std::vector<std::pair<std::unique_ptr<detail::MatcherBase>, Handler>>;\n  using HandlersForContentReader =\n      std::vector<std::pair<std::unique_ptr<detail::MatcherBase>,\n                            HandlerWithContentReader>>;\n\n  static std::unique_ptr<detail::MatcherBase>\n  make_matcher(const std::string &pattern);\n\n  Server &set_error_handler_core(HandlerWithResponse handler, std::true_type);\n  Server &set_error_handler_core(Handler handler, std::false_type);\n\n  socket_t create_server_socket(const std::string &host, int port,\n                                int socket_flags,\n                                SocketOptions socket_options) const;\n  int bind_internal(const std::string &host, int port, int socket_flags);\n  bool listen_internal();\n\n  bool routing(Request &req, Response &res, Stream &strm);\n  bool handle_file_request(Request &req, Response &res);\n  bool check_if_not_modified(const Request &req, Response &res,\n                             const std::string &etag, time_t mtime) const;\n  bool check_if_range(Request &req, const std::string &etag,\n                      time_t mtime) const;\n  bool dispatch_request(Request &req, Response &res,\n                        const Handlers &handlers) const;\n  bool dispatch_request_for_content_reader(\n      Request &req, Response &res, ContentReader content_reader,\n      const HandlersForContentReader &handlers) const;\n\n  bool parse_request_line(const char *s, Request &req) const;\n  void apply_ranges(const Request &req, Response &res,\n                    std::string &content_type, std::string &boundary) const;\n  bool write_response(Stream &strm, bool close_connection, Request &req,\n                      Response &res);\n  bool write_response_with_content(Stream &strm, bool close_connection,\n                                   const Request &req, Response &res);\n  bool write_response_core(Stream &strm, bool close_connection,\n                           const Request &req, Response &res,\n                           bool need_apply_ranges);\n  bool write_content_with_provider(Stream &strm, const Request &req,\n                                   Response &res, const std::string &boundary,\n                                   const std::string &content_type);\n  bool read_content(Stream &strm, Request &req, Response &res);\n  bool read_content_with_content_receiver(Stream &strm, Request &req,\n                                          Response &res,\n                                          ContentReceiver receiver,\n                                          FormDataHeader multipart_header,\n                                          ContentReceiver multipart_receiver);\n  bool read_content_core(Stream &strm, Request &req, Response &res,\n                         ContentReceiver receiver,\n                         FormDataHeader multipart_header,\n                         ContentReceiver multipart_receiver) const;\n\n  virtual bool process_and_close_socket(socket_t sock);\n\n  void output_log(const Request &req, const Response &res) const;\n  void output_pre_compression_log(const Request &req,\n                                  const Response &res) const;\n  void output_error_log(const Error &err, const Request *req) const;\n\n  std::atomic<bool> is_running_{false};\n  std::atomic<bool> is_decommissioned{false};\n\n  struct MountPointEntry {\n    std::string mount_point;\n    std::string base_dir;\n    Headers headers;\n  };\n  std::vector<MountPointEntry> base_dirs_;\n  std::map<std::string, std::string> file_extension_and_mimetype_map_;\n  std::string default_file_mimetype_ = \"application/octet-stream\";\n  Handler file_request_handler_;\n\n  Handlers get_handlers_;\n  Handlers post_handlers_;\n  HandlersForContentReader post_handlers_for_content_reader_;\n  Handlers put_handlers_;\n  HandlersForContentReader put_handlers_for_content_reader_;\n  Handlers patch_handlers_;\n  HandlersForContentReader patch_handlers_for_content_reader_;\n  Handlers delete_handlers_;\n  HandlersForContentReader delete_handlers_for_content_reader_;\n  Handlers options_handlers_;\n\n  HandlerWithResponse error_handler_;\n  ExceptionHandler exception_handler_;\n  HandlerWithResponse pre_routing_handler_;\n  Handler post_routing_handler_;\n  HandlerWithResponse pre_request_handler_;\n  Expect100ContinueHandler expect_100_continue_handler_;\n\n  mutable std::mutex logger_mutex_;\n  Logger logger_;\n  Logger pre_compression_logger_;\n  ErrorLogger error_logger_;\n\n  int address_family_ = AF_UNSPEC;\n  bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY;\n  bool ipv6_v6only_ = CPPHTTPLIB_IPV6_V6ONLY;\n  SocketOptions socket_options_ = default_socket_options;\n\n  Headers default_headers_;\n  std::function<ssize_t(Stream &, Headers &)> header_writer_ =\n      detail::write_headers;\n};\n\nclass Result {\npublic:\n  Result() = default;\n  Result(std::unique_ptr<Response> &&res, Error err,\n         Headers &&request_headers = Headers{})\n      : res_(std::move(res)), err_(err),\n        request_headers_(std::move(request_headers)) {}\n  // Response\n  operator bool() const { return res_ != nullptr; }\n  bool operator==(std::nullptr_t) const { return res_ == nullptr; }\n  bool operator!=(std::nullptr_t) const { return res_ != nullptr; }\n  const Response &value() const { return *res_; }\n  Response &value() { return *res_; }\n  const Response &operator*() const { return *res_; }\n  Response &operator*() { return *res_; }\n  const Response *operator->() const { return res_.get(); }\n  Response *operator->() { return res_.get(); }\n\n  // Error\n  Error error() const { return err_; }\n\n  // Request Headers\n  bool has_request_header(const std::string &key) const;\n  std::string get_request_header_value(const std::string &key,\n                                       const char *def = \"\",\n                                       size_t id = 0) const;\n  size_t get_request_header_value_u64(const std::string &key, size_t def = 0,\n                                      size_t id = 0) const;\n  size_t get_request_header_value_count(const std::string &key) const;\n\nprivate:\n  std::unique_ptr<Response> res_;\n  Error err_ = Error::Unknown;\n  Headers request_headers_;\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\npublic:\n  Result(std::unique_ptr<Response> &&res, Error err, Headers &&request_headers,\n         int ssl_error)\n      : res_(std::move(res)), err_(err),\n        request_headers_(std::move(request_headers)), ssl_error_(ssl_error) {}\n  Result(std::unique_ptr<Response> &&res, Error err, Headers &&request_headers,\n         int ssl_error, unsigned long ssl_backend_error)\n      : res_(std::move(res)), err_(err),\n        request_headers_(std::move(request_headers)), ssl_error_(ssl_error),\n        ssl_backend_error_(ssl_backend_error) {}\n\n  int ssl_error() const { return ssl_error_; }\n  unsigned long ssl_backend_error() const { return ssl_backend_error_; }\n\nprivate:\n  int ssl_error_ = 0;\n  unsigned long ssl_backend_error_ = 0;\n#endif\n\n#ifdef CPPHTTPLIB_OPENSSL_SUPPORT\npublic:\n  [[deprecated(\"Use ssl_backend_error() instead\")]]\n  unsigned long ssl_openssl_error() const {\n    return ssl_backend_error_;\n  }\n#endif\n};\n\nstruct ClientConnection {\n  socket_t sock = INVALID_SOCKET;\n\n  bool is_open() const { return sock != INVALID_SOCKET; }\n\n  ClientConnection() = default;\n\n  ~ClientConnection();\n\n  ClientConnection(const ClientConnection &) = delete;\n  ClientConnection &operator=(const ClientConnection &) = delete;\n\n  ClientConnection(ClientConnection &&other) noexcept\n      : sock(other.sock)\n#ifdef CPPHTTPLIB_SSL_ENABLED\n        ,\n        session(other.session)\n#endif\n  {\n    other.sock = INVALID_SOCKET;\n#ifdef CPPHTTPLIB_SSL_ENABLED\n    other.session = nullptr;\n#endif\n  }\n\n  ClientConnection &operator=(ClientConnection &&other) noexcept {\n    if (this != &other) {\n      sock = other.sock;\n      other.sock = INVALID_SOCKET;\n#ifdef CPPHTTPLIB_SSL_ENABLED\n      session = other.session;\n      other.session = nullptr;\n#endif\n    }\n    return *this;\n  }\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n  tls::session_t session = nullptr;\n#endif\n};\n\nnamespace detail {\n\nstruct ChunkedDecoder;\n\nstruct BodyReader {\n  Stream *stream = nullptr;\n  bool has_content_length = false;\n  size_t content_length = 0;\n  size_t payload_max_length = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;\n  size_t bytes_read = 0;\n  bool chunked = false;\n  bool eof = false;\n  std::unique_ptr<ChunkedDecoder> chunked_decoder;\n  Error last_error = Error::Success;\n\n  ssize_t read(char *buf, size_t len);\n  bool has_error() const { return last_error != Error::Success; }\n};\n\ninline ssize_t read_body_content(Stream *stream, BodyReader &br, char *buf,\n                                 size_t len) {\n  (void)stream;\n  return br.read(buf, len);\n}\n\nclass decompressor;\n\n} // namespace detail\n\nclass ClientImpl {\npublic:\n  explicit ClientImpl(const std::string &host);\n\n  explicit ClientImpl(const std::string &host, int port);\n\n  explicit ClientImpl(const std::string &host, int port,\n                      const std::string &client_cert_path,\n                      const std::string &client_key_path);\n\n  virtual ~ClientImpl();\n\n  virtual bool is_valid() const;\n\n  struct StreamHandle {\n    std::unique_ptr<Response> response;\n    Error error = Error::Success;\n\n    StreamHandle() = default;\n    StreamHandle(const StreamHandle &) = delete;\n    StreamHandle &operator=(const StreamHandle &) = delete;\n    StreamHandle(StreamHandle &&) = default;\n    StreamHandle &operator=(StreamHandle &&) = default;\n    ~StreamHandle() = default;\n\n    bool is_valid() const {\n      return response != nullptr && error == Error::Success;\n    }\n\n    ssize_t read(char *buf, size_t len);\n    void parse_trailers_if_needed();\n    Error get_read_error() const { return body_reader_.last_error; }\n    bool has_read_error() const { return body_reader_.has_error(); }\n\n    bool trailers_parsed_ = false;\n\n  private:\n    friend class ClientImpl;\n\n    ssize_t read_with_decompression(char *buf, size_t len);\n\n    std::unique_ptr<ClientConnection> connection_;\n    std::unique_ptr<Stream> socket_stream_;\n    Stream *stream_ = nullptr;\n    detail::BodyReader body_reader_;\n\n    std::unique_ptr<detail::decompressor> decompressor_;\n    std::string decompress_buffer_;\n    size_t decompress_offset_ = 0;\n    size_t decompressed_bytes_read_ = 0;\n  };\n\n  // clang-format off\n  Result Get(const std::string &path, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, const Params &params, const Headers &headers, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, const Params &params, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, const Params &params, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n\n  Result Head(const std::string &path);\n  Result Head(const std::string &path, const Headers &headers);\n\n  Result Post(const std::string &path);\n  Result Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Params &params);\n  Result Post(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers);\n  Result Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, const Params &params);\n  Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n\n  Result Put(const std::string &path);\n  Result Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Params &params);\n  Result Put(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers);\n  Result Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, const Params &params);\n  Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n\n  Result Patch(const std::string &path);\n  Result Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Params &params);\n  Result Patch(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, const Params &params);\n  Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n\n  Result Delete(const std::string &path, DownloadProgress progress = nullptr);\n  Result Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr);\n  Result Delete(const std::string &path, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr);\n  Result Delete(const std::string &path, const Params &params, DownloadProgress progress = nullptr);\n  Result Delete(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr);\n  Result Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr);\n  Result Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr);\n  Result Delete(const std::string &path, const Headers &headers, const Params &params, DownloadProgress progress = nullptr);\n\n  Result Options(const std::string &path);\n  Result Options(const std::string &path, const Headers &headers);\n  // clang-format on\n\n  // Streaming API: Open a stream for reading response body incrementally\n  // Socket ownership is transferred to StreamHandle for true streaming\n  // Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE, etc.)\n  StreamHandle open_stream(const std::string &method, const std::string &path,\n                           const Params &params = {},\n                           const Headers &headers = {},\n                           const std::string &body = {},\n                           const std::string &content_type = {});\n\n  bool send(Request &req, Response &res, Error &error);\n  Result send(const Request &req);\n\n  void stop();\n\n  std::string host() const;\n  int port() const;\n\n  size_t is_socket_open() const;\n  socket_t socket() const;\n\n  void set_hostname_addr_map(std::map<std::string, std::string> addr_map);\n\n  void set_default_headers(Headers headers);\n\n  void\n  set_header_writer(std::function<ssize_t(Stream &, Headers &)> const &writer);\n\n  void set_address_family(int family);\n  void set_tcp_nodelay(bool on);\n  void set_ipv6_v6only(bool on);\n  void set_socket_options(SocketOptions socket_options);\n\n  void set_connection_timeout(time_t sec, time_t usec = 0);\n  template <class Rep, class Period>\n  void\n  set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);\n\n  void set_read_timeout(time_t sec, time_t usec = 0);\n  template <class Rep, class Period>\n  void set_read_timeout(const std::chrono::duration<Rep, Period> &duration);\n\n  void set_write_timeout(time_t sec, time_t usec = 0);\n  template <class Rep, class Period>\n  void set_write_timeout(const std::chrono::duration<Rep, Period> &duration);\n\n  void set_max_timeout(time_t msec);\n  template <class Rep, class Period>\n  void set_max_timeout(const std::chrono::duration<Rep, Period> &duration);\n\n  void set_basic_auth(const std::string &username, const std::string &password);\n  void set_bearer_token_auth(const std::string &token);\n\n  void set_keep_alive(bool on);\n  void set_follow_location(bool on);\n\n  void set_path_encode(bool on);\n\n  void set_compress(bool on);\n\n  void set_decompress(bool on);\n\n  void set_payload_max_length(size_t length);\n\n  void set_interface(const std::string &intf);\n\n  void set_proxy(const std::string &host, int port);\n  void set_proxy_basic_auth(const std::string &username,\n                            const std::string &password);\n  void set_proxy_bearer_token_auth(const std::string &token);\n\n  void set_logger(Logger logger);\n  void set_error_logger(ErrorLogger error_logger);\n\nprotected:\n  struct Socket {\n    socket_t sock = INVALID_SOCKET;\n\n    // For Mbed TLS compatibility: start_time for request timeout tracking\n    std::chrono::time_point<std::chrono::steady_clock> start_time_;\n\n    bool is_open() const { return sock != INVALID_SOCKET; }\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n    tls::session_t ssl = nullptr;\n#endif\n  };\n\n  virtual bool create_and_connect_socket(Socket &socket, Error &error);\n  virtual bool ensure_socket_connection(Socket &socket, Error &error);\n\n  // All of:\n  //   shutdown_ssl\n  //   shutdown_socket\n  //   close_socket\n  // should ONLY be called when socket_mutex_ is locked.\n  // Also, shutdown_ssl and close_socket should also NOT be called concurrently\n  // with a DIFFERENT thread sending requests using that socket.\n  virtual void shutdown_ssl(Socket &socket, bool shutdown_gracefully);\n  void shutdown_socket(Socket &socket) const;\n  void close_socket(Socket &socket);\n\n  bool process_request(Stream &strm, Request &req, Response &res,\n                       bool close_connection, Error &error);\n\n  bool write_content_with_provider(Stream &strm, const Request &req,\n                                   Error &error) const;\n\n  void copy_settings(const ClientImpl &rhs);\n\n  void output_log(const Request &req, const Response &res) const;\n  void output_error_log(const Error &err, const Request *req) const;\n\n  // Socket endpoint information\n  const std::string host_;\n  const int port_;\n\n  // Current open socket\n  Socket socket_;\n  mutable std::mutex socket_mutex_;\n  std::recursive_mutex request_mutex_;\n\n  // These are all protected under socket_mutex\n  size_t socket_requests_in_flight_ = 0;\n  std::thread::id socket_requests_are_from_thread_ = std::thread::id();\n  bool socket_should_be_closed_when_request_is_done_ = false;\n\n  // Hostname-IP map\n  std::map<std::string, std::string> addr_map_;\n\n  // Default headers\n  Headers default_headers_;\n\n  // Header writer\n  std::function<ssize_t(Stream &, Headers &)> header_writer_ =\n      detail::write_headers;\n\n  // Settings\n  std::string client_cert_path_;\n  std::string client_key_path_;\n\n  time_t connection_timeout_sec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND;\n  time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND;\n  time_t read_timeout_sec_ = CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND;\n  time_t read_timeout_usec_ = CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND;\n  time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND;\n  time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND;\n  time_t max_timeout_msec_ = CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND;\n\n  std::string basic_auth_username_;\n  std::string basic_auth_password_;\n  std::string bearer_token_auth_token_;\n\n  bool keep_alive_ = false;\n  bool follow_location_ = false;\n\n  bool path_encode_ = true;\n\n  int address_family_ = AF_UNSPEC;\n  bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY;\n  bool ipv6_v6only_ = CPPHTTPLIB_IPV6_V6ONLY;\n  SocketOptions socket_options_ = nullptr;\n\n  bool compress_ = false;\n  bool decompress_ = true;\n\n  size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;\n  bool has_payload_max_length_ = false;\n\n  std::string interface_;\n\n  std::string proxy_host_;\n  int proxy_port_ = -1;\n\n  std::string proxy_basic_auth_username_;\n  std::string proxy_basic_auth_password_;\n  std::string proxy_bearer_token_auth_token_;\n\n  mutable std::mutex logger_mutex_;\n  Logger logger_;\n  ErrorLogger error_logger_;\n\nprivate:\n  bool send_(Request &req, Response &res, Error &error);\n  Result send_(Request &&req);\n\n  socket_t create_client_socket(Error &error) const;\n  bool read_response_line(Stream &strm, const Request &req, Response &res,\n                          bool skip_100_continue = true) const;\n  bool write_request(Stream &strm, Request &req, bool close_connection,\n                     Error &error, bool skip_body = false);\n  bool write_request_body(Stream &strm, Request &req, Error &error);\n  void prepare_default_headers(Request &r, bool for_stream,\n                               const std::string &ct);\n  bool redirect(Request &req, Response &res, Error &error);\n  bool create_redirect_client(const std::string &scheme,\n                              const std::string &host, int port, Request &req,\n                              Response &res, const std::string &path,\n                              const std::string &location, Error &error);\n  template <typename ClientType> void setup_redirect_client(ClientType &client);\n  bool handle_request(Stream &strm, Request &req, Response &res,\n                      bool close_connection, Error &error);\n  std::unique_ptr<Response> send_with_content_provider_and_receiver(\n      Request &req, const char *body, size_t content_length,\n      ContentProvider content_provider,\n      ContentProviderWithoutLength content_provider_without_length,\n      const std::string &content_type, ContentReceiver content_receiver,\n      Error &error);\n  Result send_with_content_provider_and_receiver(\n      const std::string &method, const std::string &path,\n      const Headers &headers, const char *body, size_t content_length,\n      ContentProvider content_provider,\n      ContentProviderWithoutLength content_provider_without_length,\n      const std::string &content_type, ContentReceiver content_receiver,\n      UploadProgress progress);\n  ContentProviderWithoutLength get_multipart_content_provider(\n      const std::string &boundary, const UploadFormDataItems &items,\n      const FormDataProviderItems &provider_items) const;\n\n  virtual bool\n  process_socket(const Socket &socket,\n                 std::chrono::time_point<std::chrono::steady_clock> start_time,\n                 std::function<bool(Stream &strm)> callback);\n  virtual bool is_ssl() const;\n\n  void transfer_socket_ownership_to_handle(StreamHandle &handle);\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\npublic:\n  void set_digest_auth(const std::string &username,\n                       const std::string &password);\n  void set_proxy_digest_auth(const std::string &username,\n                             const std::string &password);\n  void set_ca_cert_path(const std::string &ca_cert_file_path,\n                        const std::string &ca_cert_dir_path = std::string());\n  void enable_server_certificate_verification(bool enabled);\n  void enable_server_hostname_verification(bool enabled);\n\nprotected:\n  std::string digest_auth_username_;\n  std::string digest_auth_password_;\n  std::string proxy_digest_auth_username_;\n  std::string proxy_digest_auth_password_;\n  std::string ca_cert_file_path_;\n  std::string ca_cert_dir_path_;\n  bool server_certificate_verification_ = true;\n  bool server_hostname_verification_ = true;\n  std::string ca_cert_pem_; // Store CA cert PEM for redirect transfer\n  int last_ssl_error_ = 0;\n  unsigned long last_backend_error_ = 0;\n#endif\n\n#ifdef CPPHTTPLIB_OPENSSL_SUPPORT\npublic:\n  [[deprecated(\"Use load_ca_cert_store() instead\")]]\n  void set_ca_cert_store(X509_STORE *ca_cert_store);\n\n  [[deprecated(\"Use tls::create_ca_store() instead\")]]\n  X509_STORE *create_ca_cert_store(const char *ca_cert, std::size_t size) const;\n\n  [[deprecated(\"Use set_server_certificate_verifier(VerifyCallback) instead\")]]\n  virtual void set_server_certificate_verifier(\n      std::function<SSLVerifierResponse(SSL *ssl)> verifier);\n#endif\n};\n\nclass Client {\npublic:\n  // Universal interface\n  explicit Client(const std::string &scheme_host_port);\n\n  explicit Client(const std::string &scheme_host_port,\n                  const std::string &client_cert_path,\n                  const std::string &client_key_path);\n\n  // HTTP only interface\n  explicit Client(const std::string &host, int port);\n\n  explicit Client(const std::string &host, int port,\n                  const std::string &client_cert_path,\n                  const std::string &client_key_path);\n\n  Client(Client &&) = default;\n  Client &operator=(Client &&) = default;\n\n  ~Client();\n\n  bool is_valid() const;\n\n  // clang-format off\n  Result Get(const std::string &path, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, const Params &params, const Headers &headers, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, const Params &params, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n  Result Get(const std::string &path, const Params &params, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n\n  Result Head(const std::string &path);\n  Result Head(const std::string &path, const Headers &headers);\n\n  Result Post(const std::string &path);\n  Result Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Params &params);\n  Result Post(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers);\n  Result Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, const Params &params);\n  Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr);\n  Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n\n  Result Put(const std::string &path);\n  Result Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Params &params);\n  Result Put(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers);\n  Result Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, const Params &params);\n  Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr);\n  Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n\n  Result Patch(const std::string &path);\n  Result Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Params &params);\n  Result Patch(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers);\n  Result Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, const Params &params);\n  Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr);\n  Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);\n\n  Result Delete(const std::string &path, DownloadProgress progress = nullptr);\n  Result Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr);\n  Result Delete(const std::string &path, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr);\n  Result Delete(const std::string &path, const Params &params, DownloadProgress progress = nullptr);\n  Result Delete(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr);\n  Result Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr);\n  Result Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr);\n  Result Delete(const std::string &path, const Headers &headers, const Params &params, DownloadProgress progress = nullptr);\n\n  Result Options(const std::string &path);\n  Result Options(const std::string &path, const Headers &headers);\n  // clang-format on\n\n  // Streaming API: Open a stream for reading response body incrementally\n  // Socket ownership is transferred to StreamHandle for true streaming\n  // Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE, etc.)\n  ClientImpl::StreamHandle open_stream(const std::string &method,\n                                       const std::string &path,\n                                       const Params &params = {},\n                                       const Headers &headers = {},\n                                       const std::string &body = {},\n                                       const std::string &content_type = {});\n\n  bool send(Request &req, Response &res, Error &error);\n  Result send(const Request &req);\n\n  void stop();\n\n  std::string host() const;\n  int port() const;\n\n  size_t is_socket_open() const;\n  socket_t socket() const;\n\n  void set_hostname_addr_map(std::map<std::string, std::string> addr_map);\n\n  void set_default_headers(Headers headers);\n\n  void\n  set_header_writer(std::function<ssize_t(Stream &, Headers &)> const &writer);\n\n  void set_address_family(int family);\n  void set_tcp_nodelay(bool on);\n  void set_socket_options(SocketOptions socket_options);\n\n  void set_connection_timeout(time_t sec, time_t usec = 0);\n  template <class Rep, class Period>\n  void\n  set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);\n\n  void set_read_timeout(time_t sec, time_t usec = 0);\n  template <class Rep, class Period>\n  void set_read_timeout(const std::chrono::duration<Rep, Period> &duration);\n\n  void set_write_timeout(time_t sec, time_t usec = 0);\n  template <class Rep, class Period>\n  void set_write_timeout(const std::chrono::duration<Rep, Period> &duration);\n\n  void set_max_timeout(time_t msec);\n  template <class Rep, class Period>\n  void set_max_timeout(const std::chrono::duration<Rep, Period> &duration);\n\n  void set_basic_auth(const std::string &username, const std::string &password);\n  void set_bearer_token_auth(const std::string &token);\n\n  void set_keep_alive(bool on);\n  void set_follow_location(bool on);\n\n  void set_path_encode(bool on);\n  void set_url_encode(bool on);\n\n  void set_compress(bool on);\n\n  void set_decompress(bool on);\n\n  void set_payload_max_length(size_t length);\n\n  void set_interface(const std::string &intf);\n\n  void set_proxy(const std::string &host, int port);\n  void set_proxy_basic_auth(const std::string &username,\n                            const std::string &password);\n  void set_proxy_bearer_token_auth(const std::string &token);\n  void set_logger(Logger logger);\n  void set_error_logger(ErrorLogger error_logger);\n\nprivate:\n  std::unique_ptr<ClientImpl> cli_;\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\npublic:\n  void set_digest_auth(const std::string &username,\n                       const std::string &password);\n  void set_proxy_digest_auth(const std::string &username,\n                             const std::string &password);\n  void enable_server_certificate_verification(bool enabled);\n  void enable_server_hostname_verification(bool enabled);\n  void set_ca_cert_path(const std::string &ca_cert_file_path,\n                        const std::string &ca_cert_dir_path = std::string());\n\n  void set_ca_cert_store(tls::ca_store_t ca_cert_store);\n  void load_ca_cert_store(const char *ca_cert, std::size_t size);\n\n  void set_server_certificate_verifier(tls::VerifyCallback verifier);\n\n  void set_session_verifier(\n      std::function<SSLVerifierResponse(tls::session_t)> verifier);\n\n  tls::ctx_t tls_context() const;\n\n#if defined(_WIN32) &&                                                         \\\n    !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)\n  void enable_windows_certificate_verification(bool enabled);\n#endif\n\nprivate:\n  bool is_ssl_ = false;\n#endif\n\n#ifdef CPPHTTPLIB_OPENSSL_SUPPORT\npublic:\n  [[deprecated(\"Use tls_context() instead\")]]\n  SSL_CTX *ssl_context() const;\n\n  [[deprecated(\"Use set_session_verifier(session_t) instead\")]]\n  void set_server_certificate_verifier(\n      std::function<SSLVerifierResponse(SSL *ssl)> verifier);\n\n  [[deprecated(\"Use Result::ssl_backend_error() instead\")]]\n  long get_verify_result() const;\n#endif\n};\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\nclass SSLServer : public Server {\npublic:\n  SSLServer(const char *cert_path, const char *private_key_path,\n            const char *client_ca_cert_file_path = nullptr,\n            const char *client_ca_cert_dir_path = nullptr,\n            const char *private_key_password = nullptr);\n\n  struct PemMemory {\n    const char *cert_pem;\n    size_t cert_pem_len;\n    const char *key_pem;\n    size_t key_pem_len;\n    const char *client_ca_pem;\n    size_t client_ca_pem_len;\n    const char *private_key_password;\n  };\n  explicit SSLServer(const PemMemory &pem);\n\n  // The callback receives the ctx_t handle which can be cast to the\n  // appropriate backend type (SSL_CTX* for OpenSSL,\n  // tls::impl::MbedTlsContext* for Mbed TLS)\n  explicit SSLServer(const tls::ContextSetupCallback &setup_callback);\n\n  ~SSLServer() override;\n\n  bool is_valid() const override;\n\n  bool update_certs_pem(const char *cert_pem, const char *key_pem,\n                        const char *client_ca_pem = nullptr,\n                        const char *password = nullptr);\n\n  tls::ctx_t tls_context() const { return ctx_; }\n\n  int ssl_last_error() const { return last_ssl_error_; }\n\nprivate:\n  bool process_and_close_socket(socket_t sock) override;\n\n  tls::ctx_t ctx_ = nullptr;\n  std::mutex ctx_mutex_;\n\n  int last_ssl_error_ = 0;\n\n#ifdef CPPHTTPLIB_OPENSSL_SUPPORT\npublic:\n  [[deprecated(\"Use SSLServer(PemMemory) or \"\n               \"SSLServer(ContextSetupCallback) instead\")]]\n  SSLServer(X509 *cert, EVP_PKEY *private_key,\n            X509_STORE *client_ca_cert_store = nullptr);\n\n  [[deprecated(\"Use SSLServer(ContextSetupCallback) instead\")]]\n  SSLServer(\n      const std::function<bool(SSL_CTX &ssl_ctx)> &setup_ssl_ctx_callback);\n\n  [[deprecated(\"Use tls_context() instead\")]]\n  SSL_CTX *ssl_context() const;\n\n  [[deprecated(\"Use update_certs_pem() instead\")]]\n  void update_certs(X509 *cert, EVP_PKEY *private_key,\n                    X509_STORE *client_ca_cert_store = nullptr);\n#endif\n};\n\nclass SSLClient final : public ClientImpl {\npublic:\n  explicit SSLClient(const std::string &host);\n\n  explicit SSLClient(const std::string &host, int port);\n\n  explicit SSLClient(const std::string &host, int port,\n                     const std::string &client_cert_path,\n                     const std::string &client_key_path,\n                     const std::string &private_key_password = std::string());\n\n  struct PemMemory {\n    const char *cert_pem;\n    size_t cert_pem_len;\n    const char *key_pem;\n    size_t key_pem_len;\n    const char *private_key_password;\n  };\n  explicit SSLClient(const std::string &host, int port, const PemMemory &pem);\n\n  ~SSLClient() override;\n\n  bool is_valid() const override;\n\n  void set_ca_cert_store(tls::ca_store_t ca_cert_store);\n  void load_ca_cert_store(const char *ca_cert, std::size_t size);\n\n  void set_server_certificate_verifier(tls::VerifyCallback verifier);\n\n  // Post-handshake session verifier (backend-independent)\n  void set_session_verifier(\n      std::function<SSLVerifierResponse(tls::session_t)> verifier);\n\n  tls::ctx_t tls_context() const { return ctx_; }\n\n#if defined(_WIN32) &&                                                         \\\n    !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)\n  void enable_windows_certificate_verification(bool enabled);\n#endif\n\nprivate:\n  bool create_and_connect_socket(Socket &socket, Error &error) override;\n  bool ensure_socket_connection(Socket &socket, Error &error) override;\n  void shutdown_ssl(Socket &socket, bool shutdown_gracefully) override;\n  void shutdown_ssl_impl(Socket &socket, bool shutdown_gracefully);\n\n  bool\n  process_socket(const Socket &socket,\n                 std::chrono::time_point<std::chrono::steady_clock> start_time,\n                 std::function<bool(Stream &strm)> callback) override;\n  bool is_ssl() const override;\n\n  bool connect_with_proxy(\n      Socket &sock,\n      std::chrono::time_point<std::chrono::steady_clock> start_time,\n      Response &res, bool &success, Error &error);\n  bool initialize_ssl(Socket &socket, Error &error);\n\n  bool load_certs();\n\n  tls::ctx_t ctx_ = nullptr;\n  std::mutex ctx_mutex_;\n  std::once_flag initialize_cert_;\n\n  long verify_result_ = 0;\n\n  std::function<SSLVerifierResponse(tls::session_t)> session_verifier_;\n\n#if defined(_WIN32) &&                                                         \\\n    !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)\n  bool enable_windows_cert_verification_ = true;\n#endif\n\n  friend class ClientImpl;\n\n#ifdef CPPHTTPLIB_OPENSSL_SUPPORT\npublic:\n  [[deprecated(\"Use SSLClient(host, port, PemMemory) instead\")]]\n  explicit SSLClient(const std::string &host, int port, X509 *client_cert,\n                     EVP_PKEY *client_key,\n                     const std::string &private_key_password = std::string());\n\n  [[deprecated(\"Use Result::ssl_backend_error() instead\")]]\n  long get_verify_result() const;\n\n  [[deprecated(\"Use tls_context() instead\")]]\n  SSL_CTX *ssl_context() const;\n\n  [[deprecated(\"Use set_session_verifier(session_t) instead\")]]\n  void set_server_certificate_verifier(\n      std::function<SSLVerifierResponse(SSL *ssl)> verifier) override;\n\nprivate:\n  bool verify_host(X509 *server_cert) const;\n  bool verify_host_with_subject_alt_name(X509 *server_cert) const;\n  bool verify_host_with_common_name(X509 *server_cert) const;\n#endif\n};\n#endif // CPPHTTPLIB_SSL_ENABLED\n\nnamespace detail {\n\ntemplate <typename T, typename U>\ninline void duration_to_sec_and_usec(const T &duration, U callback) {\n  auto sec = std::chrono::duration_cast<std::chrono::seconds>(duration).count();\n  auto usec = std::chrono::duration_cast<std::chrono::microseconds>(\n                  duration - std::chrono::seconds(sec))\n                  .count();\n  callback(static_cast<time_t>(sec), static_cast<time_t>(usec));\n}\n\ntemplate <size_t N> inline constexpr size_t str_len(const char (&)[N]) {\n  return N - 1;\n}\n\ninline bool is_numeric(const std::string &str) {\n  return !str.empty() &&\n         std::all_of(str.cbegin(), str.cend(),\n                     [](unsigned char c) { return std::isdigit(c); });\n}\n\ninline size_t get_header_value_u64(const Headers &headers,\n                                   const std::string &key, size_t def,\n                                   size_t id, bool &is_invalid_value) {\n  is_invalid_value = false;\n  auto rng = headers.equal_range(key);\n  auto it = rng.first;\n  std::advance(it, static_cast<ssize_t>(id));\n  if (it != rng.second) {\n    if (is_numeric(it->second)) {\n      return std::strtoull(it->second.data(), nullptr, 10);\n    } else {\n      is_invalid_value = true;\n    }\n  }\n  return def;\n}\n\ninline size_t get_header_value_u64(const Headers &headers,\n                                   const std::string &key, size_t def,\n                                   size_t id) {\n  auto dummy = false;\n  return get_header_value_u64(headers, key, def, id, dummy);\n}\n\n} // namespace detail\n\ntemplate <class Rep, class Period>\ninline Server &\nServer::set_read_timeout(const std::chrono::duration<Rep, Period> &duration) {\n  detail::duration_to_sec_and_usec(\n      duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); });\n  return *this;\n}\n\ntemplate <class Rep, class Period>\ninline Server &\nServer::set_write_timeout(const std::chrono::duration<Rep, Period> &duration) {\n  detail::duration_to_sec_and_usec(\n      duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); });\n  return *this;\n}\n\ntemplate <class Rep, class Period>\ninline Server &\nServer::set_idle_interval(const std::chrono::duration<Rep, Period> &duration) {\n  detail::duration_to_sec_and_usec(\n      duration, [&](time_t sec, time_t usec) { set_idle_interval(sec, usec); });\n  return *this;\n}\n\ntemplate <class Rep, class Period>\ninline void ClientImpl::set_connection_timeout(\n    const std::chrono::duration<Rep, Period> &duration) {\n  detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t usec) {\n    set_connection_timeout(sec, usec);\n  });\n}\n\ntemplate <class Rep, class Period>\ninline void ClientImpl::set_read_timeout(\n    const std::chrono::duration<Rep, Period> &duration) {\n  detail::duration_to_sec_and_usec(\n      duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); });\n}\n\ntemplate <class Rep, class Period>\ninline void ClientImpl::set_write_timeout(\n    const std::chrono::duration<Rep, Period> &duration) {\n  detail::duration_to_sec_and_usec(\n      duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); });\n}\n\ntemplate <class Rep, class Period>\ninline void ClientImpl::set_max_timeout(\n    const std::chrono::duration<Rep, Period> &duration) {\n  auto msec =\n      std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();\n  set_max_timeout(msec);\n}\n\ntemplate <class Rep, class Period>\ninline void Client::set_connection_timeout(\n    const std::chrono::duration<Rep, Period> &duration) {\n  cli_->set_connection_timeout(duration);\n}\n\ntemplate <class Rep, class Period>\ninline void\nClient::set_read_timeout(const std::chrono::duration<Rep, Period> &duration) {\n  cli_->set_read_timeout(duration);\n}\n\ntemplate <class Rep, class Period>\ninline void\nClient::set_write_timeout(const std::chrono::duration<Rep, Period> &duration) {\n  cli_->set_write_timeout(duration);\n}\n\ninline void Client::set_max_timeout(time_t msec) {\n  cli_->set_max_timeout(msec);\n}\n\ntemplate <class Rep, class Period>\ninline void\nClient::set_max_timeout(const std::chrono::duration<Rep, Period> &duration) {\n  cli_->set_max_timeout(duration);\n}\n\n/*\n * Forward declarations and types that will be part of the .h file if split into\n * .h + .cc.\n */\n\nstd::string hosted_at(const std::string &hostname);\n\nvoid hosted_at(const std::string &hostname, std::vector<std::string> &addrs);\n\n// JavaScript-style URL encoding/decoding functions\nstd::string encode_uri_component(const std::string &value);\nstd::string encode_uri(const std::string &value);\nstd::string decode_uri_component(const std::string &value);\nstd::string decode_uri(const std::string &value);\n\n// RFC 3986 compliant URL component encoding/decoding functions\nstd::string encode_path_component(const std::string &component);\nstd::string decode_path_component(const std::string &component);\nstd::string encode_query_component(const std::string &component,\n                                   bool space_as_plus = true);\nstd::string decode_query_component(const std::string &component,\n                                   bool plus_as_space = true);\n\nstd::string append_query_params(const std::string &path, const Params &params);\n\nstd::pair<std::string, std::string> make_range_header(const Ranges &ranges);\n\nstd::pair<std::string, std::string>\nmake_basic_authentication_header(const std::string &username,\n                                 const std::string &password,\n                                 bool is_proxy = false);\n\nnamespace detail {\n\n#if defined(_WIN32)\ninline std::wstring u8string_to_wstring(const char *s) {\n  if (!s) { return std::wstring(); }\n\n  auto len = static_cast<int>(strlen(s));\n  if (!len) { return std::wstring(); }\n\n  auto wlen = ::MultiByteToWideChar(CP_UTF8, 0, s, len, nullptr, 0);\n  if (!wlen) { return std::wstring(); }\n\n  std::wstring ws;\n  ws.resize(wlen);\n  wlen = ::MultiByteToWideChar(\n      CP_UTF8, 0, s, len,\n      const_cast<LPWSTR>(reinterpret_cast<LPCWSTR>(ws.data())), wlen);\n  if (wlen != static_cast<int>(ws.size())) { ws.clear(); }\n  return ws;\n}\n#endif\n\nstruct FileStat {\n  FileStat(const std::string &path);\n  bool is_file() const;\n  bool is_dir() const;\n  time_t mtime() const;\n  size_t size() const;\n\nprivate:\n#if defined(_WIN32)\n  struct _stat st_;\n#else\n  struct stat st_;\n#endif\n  int ret_ = -1;\n};\n\nstd::string make_host_and_port_string(const std::string &host, int port,\n                                      bool is_ssl);\n\nstd::string trim_copy(const std::string &s);\n\nvoid divide(\n    const char *data, std::size_t size, char d,\n    std::function<void(const char *, std::size_t, const char *, std::size_t)>\n        fn);\n\nvoid divide(\n    const std::string &str, char d,\n    std::function<void(const char *, std::size_t, const char *, std::size_t)>\n        fn);\n\nvoid split(const char *b, const char *e, char d,\n           std::function<void(const char *, const char *)> fn);\n\nvoid split(const char *b, const char *e, char d, size_t m,\n           std::function<void(const char *, const char *)> fn);\n\nbool process_client_socket(\n    socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec,\n    time_t write_timeout_sec, time_t write_timeout_usec,\n    time_t max_timeout_msec,\n    std::chrono::time_point<std::chrono::steady_clock> start_time,\n    std::function<bool(Stream &)> callback);\n\nsocket_t create_client_socket(const std::string &host, const std::string &ip,\n                              int port, int address_family, bool tcp_nodelay,\n                              bool ipv6_v6only, SocketOptions socket_options,\n                              time_t connection_timeout_sec,\n                              time_t connection_timeout_usec,\n                              time_t read_timeout_sec, time_t read_timeout_usec,\n                              time_t write_timeout_sec,\n                              time_t write_timeout_usec,\n                              const std::string &intf, Error &error);\n\nconst char *get_header_value(const Headers &headers, const std::string &key,\n                             const char *def, size_t id);\n\nstd::string params_to_query_str(const Params &params);\n\nvoid parse_query_text(const char *data, std::size_t size, Params &params);\n\nvoid parse_query_text(const std::string &s, Params &params);\n\nbool parse_multipart_boundary(const std::string &content_type,\n                              std::string &boundary);\n\nbool parse_range_header(const std::string &s, Ranges &ranges);\n\nbool parse_accept_header(const std::string &s,\n                         std::vector<std::string> &content_types);\n\nint close_socket(socket_t sock);\n\nssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags);\n\nssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags);\n\nenum class EncodingType { None = 0, Gzip, Brotli, Zstd };\n\nEncodingType encoding_type(const Request &req, const Response &res);\n\nclass BufferStream final : public Stream {\npublic:\n  BufferStream() = default;\n  ~BufferStream() override = default;\n\n  bool is_readable() const override;\n  bool wait_readable() const override;\n  bool wait_writable() const override;\n  ssize_t read(char *ptr, size_t size) override;\n  ssize_t write(const char *ptr, size_t size) override;\n  void get_remote_ip_and_port(std::string &ip, int &port) const override;\n  void get_local_ip_and_port(std::string &ip, int &port) const override;\n  socket_t socket() const override;\n  time_t duration() const override;\n\n  const std::string &get_buffer() const;\n\nprivate:\n  std::string buffer;\n  size_t position = 0;\n};\n\nclass compressor {\npublic:\n  virtual ~compressor() = default;\n\n  typedef std::function<bool(const char *data, size_t data_len)> Callback;\n  virtual bool compress(const char *data, size_t data_length, bool last,\n                        Callback callback) = 0;\n};\n\nclass decompressor {\npublic:\n  virtual ~decompressor() = default;\n\n  virtual bool is_valid() const = 0;\n\n  typedef std::function<bool(const char *data, size_t data_len)> Callback;\n  virtual bool decompress(const char *data, size_t data_length,\n                          Callback callback) = 0;\n};\n\nclass nocompressor final : public compressor {\npublic:\n  ~nocompressor() override = default;\n\n  bool compress(const char *data, size_t data_length, bool /*last*/,\n                Callback callback) override;\n};\n\n#ifdef CPPHTTPLIB_ZLIB_SUPPORT\nclass gzip_compressor final : public compressor {\npublic:\n  gzip_compressor();\n  ~gzip_compressor() override;\n\n  bool compress(const char *data, size_t data_length, bool last,\n                Callback callback) override;\n\nprivate:\n  bool is_valid_ = false;\n  z_stream strm_;\n};\n\nclass gzip_decompressor final : public decompressor {\npublic:\n  gzip_decompressor();\n  ~gzip_decompressor() override;\n\n  bool is_valid() const override;\n\n  bool decompress(const char *data, size_t data_length,\n                  Callback callback) override;\n\nprivate:\n  bool is_valid_ = false;\n  z_stream strm_;\n};\n#endif\n\n#ifdef CPPHTTPLIB_BROTLI_SUPPORT\nclass brotli_compressor final : public compressor {\npublic:\n  brotli_compressor();\n  ~brotli_compressor();\n\n  bool compress(const char *data, size_t data_length, bool last,\n                Callback callback) override;\n\nprivate:\n  BrotliEncoderState *state_ = nullptr;\n};\n\nclass brotli_decompressor final : public decompressor {\npublic:\n  brotli_decompressor();\n  ~brotli_decompressor();\n\n  bool is_valid() const override;\n\n  bool decompress(const char *data, size_t data_length,\n                  Callback callback) override;\n\nprivate:\n  BrotliDecoderResult decoder_r;\n  BrotliDecoderState *decoder_s = nullptr;\n};\n#endif\n\n#ifdef CPPHTTPLIB_ZSTD_SUPPORT\nclass zstd_compressor : public compressor {\npublic:\n  zstd_compressor();\n  ~zstd_compressor();\n\n  bool compress(const char *data, size_t data_length, bool last,\n                Callback callback) override;\n\nprivate:\n  ZSTD_CCtx *ctx_ = nullptr;\n};\n\nclass zstd_decompressor : public decompressor {\npublic:\n  zstd_decompressor();\n  ~zstd_decompressor();\n\n  bool is_valid() const override;\n\n  bool decompress(const char *data, size_t data_length,\n                  Callback callback) override;\n\nprivate:\n  ZSTD_DCtx *ctx_ = nullptr;\n};\n#endif\n\n// NOTE: until the read size reaches `fixed_buffer_size`, use `fixed_buffer`\n// to store data. The call can set memory on stack for performance.\nclass stream_line_reader {\npublic:\n  stream_line_reader(Stream &strm, char *fixed_buffer,\n                     size_t fixed_buffer_size);\n  const char *ptr() const;\n  size_t size() const;\n  bool end_with_crlf() const;\n  bool getline();\n\nprivate:\n  void append(char c);\n\n  Stream &strm_;\n  char *fixed_buffer_;\n  const size_t fixed_buffer_size_;\n  size_t fixed_buffer_used_size_ = 0;\n  std::string growable_buffer_;\n};\n\nbool parse_trailers(stream_line_reader &line_reader, Headers &dest,\n                    const Headers &src_headers);\n\nstruct ChunkedDecoder {\n  Stream &strm;\n  size_t chunk_remaining = 0;\n  bool finished = false;\n  char line_buf[64];\n  size_t last_chunk_total = 0;\n  size_t last_chunk_offset = 0;\n\n  explicit ChunkedDecoder(Stream &s);\n\n  ssize_t read_payload(char *buf, size_t len, size_t &out_chunk_offset,\n                       size_t &out_chunk_total);\n\n  bool parse_trailers_into(Headers &dest, const Headers &src_headers);\n};\n\nclass mmap {\npublic:\n  mmap(const char *path);\n  ~mmap();\n\n  bool open(const char *path);\n  void close();\n\n  bool is_open() const;\n  size_t size() const;\n  const char *data() const;\n\nprivate:\n#if defined(_WIN32)\n  HANDLE hFile_ = NULL;\n  HANDLE hMapping_ = NULL;\n#else\n  int fd_ = -1;\n#endif\n  size_t size_ = 0;\n  void *addr_ = nullptr;\n  bool is_open_empty_file = false;\n};\n\n// NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5\nnamespace fields {\n\nbool is_token_char(char c);\nbool is_token(const std::string &s);\nbool is_field_name(const std::string &s);\nbool is_vchar(char c);\nbool is_obs_text(char c);\nbool is_field_vchar(char c);\nbool is_field_content(const std::string &s);\nbool is_field_value(const std::string &s);\n\n} // namespace fields\n} // namespace detail\n\n/*\n * TLS Abstraction Layer Declarations\n */\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n// TLS abstraction layer - backend-specific type declarations\n#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT\nnamespace tls {\nnamespace impl {\n\n// Mbed TLS context wrapper (holds config, entropy, DRBG, CA chain, own\n// cert/key). This struct is accessible via tls::impl for use in SSL context\n// setup callbacks (cast ctx_t to tls::impl::MbedTlsContext*).\nstruct MbedTlsContext {\n  mbedtls_ssl_config conf;\n  mbedtls_entropy_context entropy;\n  mbedtls_ctr_drbg_context ctr_drbg;\n  mbedtls_x509_crt ca_chain;\n  mbedtls_x509_crt own_cert;\n  mbedtls_pk_context own_key;\n  bool is_server = false;\n  bool verify_client = false;\n  bool has_verify_callback = false;\n\n  MbedTlsContext();\n  ~MbedTlsContext();\n\n  MbedTlsContext(const MbedTlsContext &) = delete;\n  MbedTlsContext &operator=(const MbedTlsContext &) = delete;\n};\n\n} // namespace impl\n} // namespace tls\n#endif\n\n#endif // CPPHTTPLIB_SSL_ENABLED\n\nnamespace stream {\n\nclass Result {\npublic:\n  Result();\n  explicit Result(ClientImpl::StreamHandle &&handle, size_t chunk_size = 8192);\n  Result(Result &&other) noexcept;\n  Result &operator=(Result &&other) noexcept;\n  Result(const Result &) = delete;\n  Result &operator=(const Result &) = delete;\n\n  // Response info\n  bool is_valid() const;\n  explicit operator bool() const;\n  int status() const;\n  const Headers &headers() const;\n  std::string get_header_value(const std::string &key,\n                               const char *def = \"\") const;\n  bool has_header(const std::string &key) const;\n  Error error() const;\n  Error read_error() const;\n  bool has_read_error() const;\n\n  // Stream reading\n  bool next();\n  const char *data() const;\n  size_t size() const;\n  std::string read_all();\n\nprivate:\n  ClientImpl::StreamHandle handle_;\n  std::string buffer_;\n  size_t current_size_ = 0;\n  size_t chunk_size_;\n  bool finished_ = false;\n};\n\n// GET\ntemplate <typename ClientType>\ninline Result Get(ClientType &cli, const std::string &path,\n                  size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"GET\", path), chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Get(ClientType &cli, const std::string &path,\n                  const Headers &headers, size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"GET\", path, {}, headers), chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Get(ClientType &cli, const std::string &path,\n                  const Params &params, size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"GET\", path, params), chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Get(ClientType &cli, const std::string &path,\n                  const Params &params, const Headers &headers,\n                  size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"GET\", path, params, headers), chunk_size};\n}\n\n// POST\ntemplate <typename ClientType>\ninline Result Post(ClientType &cli, const std::string &path,\n                   const std::string &body, const std::string &content_type,\n                   size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"POST\", path, {}, {}, body, content_type),\n                chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Post(ClientType &cli, const std::string &path,\n                   const Headers &headers, const std::string &body,\n                   const std::string &content_type, size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"POST\", path, {}, headers, body, content_type),\n                chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Post(ClientType &cli, const std::string &path,\n                   const Params &params, const std::string &body,\n                   const std::string &content_type, size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"POST\", path, params, {}, body, content_type),\n                chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Post(ClientType &cli, const std::string &path,\n                   const Params &params, const Headers &headers,\n                   const std::string &body, const std::string &content_type,\n                   size_t chunk_size = 8192) {\n  return Result{\n      cli.open_stream(\"POST\", path, params, headers, body, content_type),\n      chunk_size};\n}\n\n// PUT\ntemplate <typename ClientType>\ninline Result Put(ClientType &cli, const std::string &path,\n                  const std::string &body, const std::string &content_type,\n                  size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"PUT\", path, {}, {}, body, content_type),\n                chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Put(ClientType &cli, const std::string &path,\n                  const Headers &headers, const std::string &body,\n                  const std::string &content_type, size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"PUT\", path, {}, headers, body, content_type),\n                chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Put(ClientType &cli, const std::string &path,\n                  const Params &params, const std::string &body,\n                  const std::string &content_type, size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"PUT\", path, params, {}, body, content_type),\n                chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Put(ClientType &cli, const std::string &path,\n                  const Params &params, const Headers &headers,\n                  const std::string &body, const std::string &content_type,\n                  size_t chunk_size = 8192) {\n  return Result{\n      cli.open_stream(\"PUT\", path, params, headers, body, content_type),\n      chunk_size};\n}\n\n// PATCH\ntemplate <typename ClientType>\ninline Result Patch(ClientType &cli, const std::string &path,\n                    const std::string &body, const std::string &content_type,\n                    size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"PATCH\", path, {}, {}, body, content_type),\n                chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Patch(ClientType &cli, const std::string &path,\n                    const Headers &headers, const std::string &body,\n                    const std::string &content_type, size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"PATCH\", path, {}, headers, body, content_type),\n                chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Patch(ClientType &cli, const std::string &path,\n                    const Params &params, const std::string &body,\n                    const std::string &content_type, size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"PATCH\", path, params, {}, body, content_type),\n                chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Patch(ClientType &cli, const std::string &path,\n                    const Params &params, const Headers &headers,\n                    const std::string &body, const std::string &content_type,\n                    size_t chunk_size = 8192) {\n  return Result{\n      cli.open_stream(\"PATCH\", path, params, headers, body, content_type),\n      chunk_size};\n}\n\n// DELETE\ntemplate <typename ClientType>\ninline Result Delete(ClientType &cli, const std::string &path,\n                     size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"DELETE\", path), chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Delete(ClientType &cli, const std::string &path,\n                     const Headers &headers, size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"DELETE\", path, {}, headers), chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Delete(ClientType &cli, const std::string &path,\n                     const std::string &body, const std::string &content_type,\n                     size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"DELETE\", path, {}, {}, body, content_type),\n                chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Delete(ClientType &cli, const std::string &path,\n                     const Headers &headers, const std::string &body,\n                     const std::string &content_type,\n                     size_t chunk_size = 8192) {\n  return Result{\n      cli.open_stream(\"DELETE\", path, {}, headers, body, content_type),\n      chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Delete(ClientType &cli, const std::string &path,\n                     const Params &params, size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"DELETE\", path, params), chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Delete(ClientType &cli, const std::string &path,\n                     const Params &params, const Headers &headers,\n                     size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"DELETE\", path, params, headers), chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Delete(ClientType &cli, const std::string &path,\n                     const Params &params, const std::string &body,\n                     const std::string &content_type,\n                     size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"DELETE\", path, params, {}, body, content_type),\n                chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Delete(ClientType &cli, const std::string &path,\n                     const Params &params, const Headers &headers,\n                     const std::string &body, const std::string &content_type,\n                     size_t chunk_size = 8192) {\n  return Result{\n      cli.open_stream(\"DELETE\", path, params, headers, body, content_type),\n      chunk_size};\n}\n\n// HEAD\ntemplate <typename ClientType>\ninline Result Head(ClientType &cli, const std::string &path,\n                   size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"HEAD\", path), chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Head(ClientType &cli, const std::string &path,\n                   const Headers &headers, size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"HEAD\", path, {}, headers), chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Head(ClientType &cli, const std::string &path,\n                   const Params &params, size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"HEAD\", path, params), chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Head(ClientType &cli, const std::string &path,\n                   const Params &params, const Headers &headers,\n                   size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"HEAD\", path, params, headers), chunk_size};\n}\n\n// OPTIONS\ntemplate <typename ClientType>\ninline Result Options(ClientType &cli, const std::string &path,\n                      size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"OPTIONS\", path), chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Options(ClientType &cli, const std::string &path,\n                      const Headers &headers, size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"OPTIONS\", path, {}, headers), chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Options(ClientType &cli, const std::string &path,\n                      const Params &params, size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"OPTIONS\", path, params), chunk_size};\n}\n\ntemplate <typename ClientType>\ninline Result Options(ClientType &cli, const std::string &path,\n                      const Params &params, const Headers &headers,\n                      size_t chunk_size = 8192) {\n  return Result{cli.open_stream(\"OPTIONS\", path, params, headers), chunk_size};\n}\n\n} // namespace stream\n\nnamespace sse {\n\nstruct SSEMessage {\n  std::string event; // Event type (default: \"message\")\n  std::string data;  // Event payload\n  std::string id;    // Event ID for Last-Event-ID header\n\n  SSEMessage();\n  void clear();\n};\n\nclass SSEClient {\npublic:\n  using MessageHandler = std::function<void(const SSEMessage &)>;\n  using ErrorHandler = std::function<void(Error)>;\n  using OpenHandler = std::function<void()>;\n\n  SSEClient(Client &client, const std::string &path);\n  SSEClient(Client &client, const std::string &path, const Headers &headers);\n  ~SSEClient();\n\n  SSEClient(const SSEClient &) = delete;\n  SSEClient &operator=(const SSEClient &) = delete;\n\n  // Event handlers\n  SSEClient &on_message(MessageHandler handler);\n  SSEClient &on_event(const std::string &type, MessageHandler handler);\n  SSEClient &on_open(OpenHandler handler);\n  SSEClient &on_error(ErrorHandler handler);\n  SSEClient &set_reconnect_interval(int ms);\n  SSEClient &set_max_reconnect_attempts(int n);\n\n  // State accessors\n  bool is_connected() const;\n  const std::string &last_event_id() const;\n\n  // Blocking start - runs event loop with auto-reconnect\n  void start();\n\n  // Non-blocking start - runs in background thread\n  void start_async();\n\n  // Stop the client (thread-safe)\n  void stop();\n\nprivate:\n  bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms);\n  void run_event_loop();\n  void dispatch_event(const SSEMessage &msg);\n  bool should_reconnect(int count) const;\n  void wait_for_reconnect();\n\n  // Client and path\n  Client &client_;\n  std::string path_;\n  Headers headers_;\n\n  // Callbacks\n  MessageHandler on_message_;\n  std::map<std::string, MessageHandler> event_handlers_;\n  OpenHandler on_open_;\n  ErrorHandler on_error_;\n\n  // Configuration\n  int reconnect_interval_ms_ = 3000;\n  int max_reconnect_attempts_ = 0; // 0 = unlimited\n\n  // State\n  std::atomic<bool> running_{false};\n  std::atomic<bool> connected_{false};\n  std::string last_event_id_;\n\n  // Async support\n  std::thread async_thread_;\n};\n\n} // namespace sse\n\n// ----------------------------------------------------------------------------\n\n/*\n * Implementation that will be part of the .cc file if split into .h + .cc.\n */\n\nnamespace stream {\n\n// stream::Result implementations\ninline Result::Result() : chunk_size_(8192) {}\n\ninline Result::Result(ClientImpl::StreamHandle &&handle, size_t chunk_size)\n    : handle_(std::move(handle)), chunk_size_(chunk_size) {}\n\ninline Result::Result(Result &&other) noexcept\n    : handle_(std::move(other.handle_)), buffer_(std::move(other.buffer_)),\n      current_size_(other.current_size_), chunk_size_(other.chunk_size_),\n      finished_(other.finished_) {\n  other.current_size_ = 0;\n  other.finished_ = true;\n}\n\ninline Result &Result::operator=(Result &&other) noexcept {\n  if (this != &other) {\n    handle_ = std::move(other.handle_);\n    buffer_ = std::move(other.buffer_);\n    current_size_ = other.current_size_;\n    chunk_size_ = other.chunk_size_;\n    finished_ = other.finished_;\n    other.current_size_ = 0;\n    other.finished_ = true;\n  }\n  return *this;\n}\n\ninline bool Result::is_valid() const { return handle_.is_valid(); }\ninline Result::operator bool() const { return is_valid(); }\n\ninline int Result::status() const {\n  return handle_.response ? handle_.response->status : -1;\n}\n\ninline const Headers &Result::headers() const {\n  static const Headers empty_headers;\n  return handle_.response ? handle_.response->headers : empty_headers;\n}\n\ninline std::string Result::get_header_value(const std::string &key,\n                                            const char *def) const {\n  return handle_.response ? handle_.response->get_header_value(key, def) : def;\n}\n\ninline bool Result::has_header(const std::string &key) const {\n  return handle_.response ? handle_.response->has_header(key) : false;\n}\n\ninline Error Result::error() const { return handle_.error; }\ninline Error Result::read_error() const { return handle_.get_read_error(); }\ninline bool Result::has_read_error() const { return handle_.has_read_error(); }\n\ninline bool Result::next() {\n  if (!handle_.is_valid() || finished_) { return false; }\n\n  if (buffer_.size() < chunk_size_) { buffer_.resize(chunk_size_); }\n\n  ssize_t n = handle_.read(&buffer_[0], chunk_size_);\n  if (n > 0) {\n    current_size_ = static_cast<size_t>(n);\n    return true;\n  }\n\n  current_size_ = 0;\n  finished_ = true;\n  return false;\n}\n\ninline const char *Result::data() const { return buffer_.data(); }\ninline size_t Result::size() const { return current_size_; }\n\ninline std::string Result::read_all() {\n  std::string result;\n  while (next()) {\n    result.append(data(), size());\n  }\n  return result;\n}\n\n} // namespace stream\n\nnamespace sse {\n\n// SSEMessage implementations\ninline SSEMessage::SSEMessage() : event(\"message\") {}\n\ninline void SSEMessage::clear() {\n  event = \"message\";\n  data.clear();\n  id.clear();\n}\n\n// SSEClient implementations\ninline SSEClient::SSEClient(Client &client, const std::string &path)\n    : client_(client), path_(path) {}\n\ninline SSEClient::SSEClient(Client &client, const std::string &path,\n                            const Headers &headers)\n    : client_(client), path_(path), headers_(headers) {}\n\ninline SSEClient::~SSEClient() { stop(); }\n\ninline SSEClient &SSEClient::on_message(MessageHandler handler) {\n  on_message_ = std::move(handler);\n  return *this;\n}\n\ninline SSEClient &SSEClient::on_event(const std::string &type,\n                                      MessageHandler handler) {\n  event_handlers_[type] = std::move(handler);\n  return *this;\n}\n\ninline SSEClient &SSEClient::on_open(OpenHandler handler) {\n  on_open_ = std::move(handler);\n  return *this;\n}\n\ninline SSEClient &SSEClient::on_error(ErrorHandler handler) {\n  on_error_ = std::move(handler);\n  return *this;\n}\n\ninline SSEClient &SSEClient::set_reconnect_interval(int ms) {\n  reconnect_interval_ms_ = ms;\n  return *this;\n}\n\ninline SSEClient &SSEClient::set_max_reconnect_attempts(int n) {\n  max_reconnect_attempts_ = n;\n  return *this;\n}\n\ninline bool SSEClient::is_connected() const { return connected_.load(); }\n\ninline const std::string &SSEClient::last_event_id() const {\n  return last_event_id_;\n}\n\ninline void SSEClient::start() {\n  running_.store(true);\n  run_event_loop();\n}\n\ninline void SSEClient::start_async() {\n  running_.store(true);\n  async_thread_ = std::thread([this]() { run_event_loop(); });\n}\n\ninline void SSEClient::stop() {\n  running_.store(false);\n  client_.stop(); // Cancel any pending operations\n  if (async_thread_.joinable()) { async_thread_.join(); }\n}\n\ninline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,\n                                      int &retry_ms) {\n  // Blank line signals end of event\n  if (line.empty() || line == \"\\r\") { return true; }\n\n  // Lines starting with ':' are comments (ignored)\n  if (!line.empty() && line[0] == ':') { return false; }\n\n  // Find the colon separator\n  auto colon_pos = line.find(':');\n  if (colon_pos == std::string::npos) {\n    // Line with no colon is treated as field name with empty value\n    return false;\n  }\n\n  auto field = line.substr(0, colon_pos);\n  std::string value;\n\n  // Value starts after colon, skip optional single space\n  if (colon_pos + 1 < line.size()) {\n    auto value_start = colon_pos + 1;\n    if (line[value_start] == ' ') { value_start++; }\n    value = line.substr(value_start);\n    // Remove trailing \\r if present\n    if (!value.empty() && value.back() == '\\r') { value.pop_back(); }\n  }\n\n  // Handle known fields\n  if (field == \"event\") {\n    msg.event = value;\n  } else if (field == \"data\") {\n    // Multiple data lines are concatenated with newlines\n    if (!msg.data.empty()) { msg.data += \"\\n\"; }\n    msg.data += value;\n  } else if (field == \"id\") {\n    // Empty id is valid (clears the last event ID)\n    msg.id = value;\n  } else if (field == \"retry\") {\n    // Parse retry interval in milliseconds\n    {\n      int v = 0;\n      auto res =\n          detail::from_chars(value.data(), value.data() + value.size(), v);\n      if (res.ec == std::errc{}) { retry_ms = v; }\n    }\n  }\n  // Unknown fields are ignored per SSE spec\n\n  return false;\n}\n\ninline void SSEClient::run_event_loop() {\n  auto reconnect_count = 0;\n\n  while (running_.load()) {\n    // Build headers, including Last-Event-ID if we have one\n    auto request_headers = headers_;\n    if (!last_event_id_.empty()) {\n      request_headers.emplace(\"Last-Event-ID\", last_event_id_);\n    }\n\n    // Open streaming connection\n    auto result = stream::Get(client_, path_, request_headers);\n\n    // Connection error handling\n    if (!result) {\n      connected_.store(false);\n      if (on_error_) { on_error_(result.error()); }\n\n      if (!should_reconnect(reconnect_count)) { break; }\n      wait_for_reconnect();\n      reconnect_count++;\n      continue;\n    }\n\n    if (result.status() != 200) {\n      connected_.store(false);\n      // For certain errors, don't reconnect\n      if (result.status() == 204 || // No Content - server wants us to stop\n          result.status() == 404 || // Not Found\n          result.status() == 401 || // Unauthorized\n          result.status() == 403) { // Forbidden\n        if (on_error_) { on_error_(Error::Connection); }\n        break;\n      }\n\n      if (on_error_) { on_error_(Error::Connection); }\n\n      if (!should_reconnect(reconnect_count)) { break; }\n      wait_for_reconnect();\n      reconnect_count++;\n      continue;\n    }\n\n    // Connection successful\n    connected_.store(true);\n    reconnect_count = 0;\n    if (on_open_) { on_open_(); }\n\n    // Event receiving loop\n    std::string buffer;\n    SSEMessage current_msg;\n\n    while (running_.load() && result.next()) {\n      buffer.append(result.data(), result.size());\n\n      // Process complete lines in the buffer\n      size_t line_start = 0;\n      size_t newline_pos;\n\n      while ((newline_pos = buffer.find('\\n', line_start)) !=\n             std::string::npos) {\n        auto line = buffer.substr(line_start, newline_pos - line_start);\n        line_start = newline_pos + 1;\n\n        // Parse the line and check if event is complete\n        auto event_complete =\n            parse_sse_line(line, current_msg, reconnect_interval_ms_);\n\n        if (event_complete && !current_msg.data.empty()) {\n          // Update last_event_id for reconnection\n          if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; }\n\n          // Dispatch event to appropriate handler\n          dispatch_event(current_msg);\n\n          current_msg.clear();\n        }\n      }\n\n      // Keep unprocessed data in buffer\n      buffer.erase(0, line_start);\n    }\n\n    // Connection ended\n    connected_.store(false);\n\n    if (!running_.load()) { break; }\n\n    // Check for read errors\n    if (result.has_read_error()) {\n      if (on_error_) { on_error_(result.read_error()); }\n    }\n\n    if (!should_reconnect(reconnect_count)) { break; }\n    wait_for_reconnect();\n    reconnect_count++;\n  }\n\n  connected_.store(false);\n}\n\ninline void SSEClient::dispatch_event(const SSEMessage &msg) {\n  // Check for specific event type handler first\n  auto it = event_handlers_.find(msg.event);\n  if (it != event_handlers_.end()) {\n    it->second(msg);\n    return;\n  }\n\n  // Fall back to generic message handler\n  if (on_message_) { on_message_(msg); }\n}\n\ninline bool SSEClient::should_reconnect(int count) const {\n  if (!running_.load()) { return false; }\n  if (max_reconnect_attempts_ == 0) { return true; } // unlimited\n  return count < max_reconnect_attempts_;\n}\n\ninline void SSEClient::wait_for_reconnect() {\n  // Use small increments to check running_ flag frequently\n  auto waited = 0;\n  while (running_.load() && waited < reconnect_interval_ms_) {\n    std::this_thread::sleep_for(std::chrono::milliseconds(100));\n    waited += 100;\n  }\n}\n\n} // namespace sse\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n/*\n * TLS abstraction layer - internal function declarations\n * These are implementation details and not part of the public API.\n */\nnamespace tls {\n\n// Client context\nctx_t create_client_context();\nvoid free_context(ctx_t ctx);\nbool set_min_version(ctx_t ctx, Version version);\nbool load_ca_pem(ctx_t ctx, const char *pem, size_t len);\nbool load_ca_file(ctx_t ctx, const char *file_path);\nbool load_ca_dir(ctx_t ctx, const char *dir_path);\nbool load_system_certs(ctx_t ctx);\nbool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key,\n                         const char *password);\nbool set_client_cert_file(ctx_t ctx, const char *cert_path,\n                          const char *key_path, const char *password);\n\n// Server context\nctx_t create_server_context();\nbool set_server_cert_pem(ctx_t ctx, const char *cert, const char *key,\n                         const char *password);\nbool set_server_cert_file(ctx_t ctx, const char *cert_path,\n                          const char *key_path, const char *password);\nbool set_client_ca_file(ctx_t ctx, const char *ca_file, const char *ca_dir);\nvoid set_verify_client(ctx_t ctx, bool require);\n\n// Session management\nsession_t create_session(ctx_t ctx, socket_t sock);\nvoid free_session(session_t session);\nbool set_sni(session_t session, const char *hostname);\nbool set_hostname(session_t session, const char *hostname);\n\n// Handshake (non-blocking capable)\nTlsError connect(session_t session);\nTlsError accept(session_t session);\n\n// Handshake with timeout (blocking until timeout)\nbool connect_nonblocking(session_t session, socket_t sock, time_t timeout_sec,\n                         time_t timeout_usec, TlsError *err);\nbool accept_nonblocking(session_t session, socket_t sock, time_t timeout_sec,\n                        time_t timeout_usec, TlsError *err);\n\n// I/O (non-blocking capable)\nssize_t read(session_t session, void *buf, size_t len, TlsError &err);\nssize_t write(session_t session, const void *buf, size_t len, TlsError &err);\nint pending(const_session_t session);\nvoid shutdown(session_t session, bool graceful);\n\n// Connection state\nbool is_peer_closed(session_t session, socket_t sock);\n\n// Certificate verification\ncert_t get_peer_cert(const_session_t session);\nvoid free_cert(cert_t cert);\nbool verify_hostname(cert_t cert, const char *hostname);\nuint64_t hostname_mismatch_code();\nlong get_verify_result(const_session_t session);\n\n// Certificate introspection\nstd::string get_cert_subject_cn(cert_t cert);\nstd::string get_cert_issuer_name(cert_t cert);\nbool get_cert_sans(cert_t cert, std::vector<SanEntry> &sans);\nbool get_cert_validity(cert_t cert, time_t &not_before, time_t &not_after);\nstd::string get_cert_serial(cert_t cert);\nbool get_cert_der(cert_t cert, std::vector<unsigned char> &der);\nconst char *get_sni(const_session_t session);\n\n// CA store management\nca_store_t create_ca_store(const char *pem, size_t len);\nvoid free_ca_store(ca_store_t store);\nbool set_ca_store(ctx_t ctx, ca_store_t store);\nsize_t get_ca_certs(ctx_t ctx, std::vector<cert_t> &certs);\nstd::vector<std::string> get_ca_names(ctx_t ctx);\n\n// Dynamic certificate update (for servers)\nbool update_server_cert(ctx_t ctx, const char *cert_pem, const char *key_pem,\n                        const char *password);\nbool update_server_client_ca(ctx_t ctx, const char *ca_pem);\n\n// Certificate verification callback\nbool set_verify_callback(ctx_t ctx, VerifyCallback callback);\nlong get_verify_error(const_session_t session);\nstd::string verify_error_string(long error_code);\n\n// TlsError information\nuint64_t peek_error();\nuint64_t get_error();\nstd::string error_string(uint64_t code);\n\n} // namespace tls\n#endif // CPPHTTPLIB_SSL_ENABLED\n\n/*\n * Group 1: detail namespace - Non-SSL utilities\n */\n\nnamespace detail {\n\ninline bool set_socket_opt_impl(socket_t sock, int level, int optname,\n                                const void *optval, socklen_t optlen) {\n  return setsockopt(sock, level, optname,\n#ifdef _WIN32\n                    reinterpret_cast<const char *>(optval),\n#else\n                    optval,\n#endif\n                    optlen) == 0;\n}\n\ninline bool set_socket_opt(socket_t sock, int level, int optname, int optval) {\n  return set_socket_opt_impl(sock, level, optname, &optval, sizeof(optval));\n}\n\ninline bool set_socket_opt_time(socket_t sock, int level, int optname,\n                                time_t sec, time_t usec) {\n#ifdef _WIN32\n  auto timeout = static_cast<uint32_t>(sec * 1000 + usec / 1000);\n#else\n  timeval timeout;\n  timeout.tv_sec = static_cast<long>(sec);\n  timeout.tv_usec = static_cast<decltype(timeout.tv_usec)>(usec);\n#endif\n  return set_socket_opt_impl(sock, level, optname, &timeout, sizeof(timeout));\n}\n\ninline bool is_hex(char c, int &v) {\n  if (isdigit(c)) {\n    v = c - '0';\n    return true;\n  } else if ('A' <= c && c <= 'F') {\n    v = c - 'A' + 10;\n    return true;\n  } else if ('a' <= c && c <= 'f') {\n    v = c - 'a' + 10;\n    return true;\n  }\n  return false;\n}\n\ninline bool from_hex_to_i(const std::string &s, size_t i, size_t cnt,\n                          int &val) {\n  if (i >= s.size()) { return false; }\n\n  val = 0;\n  for (; cnt; i++, cnt--) {\n    if (!s[i]) { return false; }\n    auto v = 0;\n    if (is_hex(s[i], v)) {\n      val = val * 16 + v;\n    } else {\n      return false;\n    }\n  }\n  return true;\n}\n\ninline std::string from_i_to_hex(size_t n) {\n  static const auto charset = \"0123456789abcdef\";\n  std::string ret;\n  do {\n    ret = charset[n & 15] + ret;\n    n >>= 4;\n  } while (n > 0);\n  return ret;\n}\n\ninline std::string compute_etag(const FileStat &fs) {\n  if (!fs.is_file()) { return std::string(); }\n\n  // If mtime cannot be determined (negative value indicates an error\n  // or sentinel), do not generate an ETag. Returning a neutral / fixed\n  // value like 0 could collide with a real file that legitimately has\n  // mtime == 0 (epoch) and lead to misleading validators.\n  auto mtime_raw = fs.mtime();\n  if (mtime_raw < 0) { return std::string(); }\n\n  auto mtime = static_cast<size_t>(mtime_raw);\n  auto size = fs.size();\n\n  return std::string(\"W/\\\"\") + from_i_to_hex(mtime) + \"-\" +\n         from_i_to_hex(size) + \"\\\"\";\n}\n\n// Format time_t as HTTP-date (RFC 9110 Section 5.6.7): \"Sun, 06 Nov 1994\n// 08:49:37 GMT\" This implementation is defensive: it validates `mtime`, checks\n// return values from `gmtime_r`/`gmtime_s`, and ensures `strftime` succeeds.\ninline std::string file_mtime_to_http_date(time_t mtime) {\n  if (mtime < 0) { return std::string(); }\n\n  struct tm tm_buf;\n#ifdef _WIN32\n  if (gmtime_s(&tm_buf, &mtime) != 0) { return std::string(); }\n#else\n  if (gmtime_r(&mtime, &tm_buf) == nullptr) { return std::string(); }\n#endif\n  char buf[64];\n  if (strftime(buf, sizeof(buf), \"%a, %d %b %Y %H:%M:%S GMT\", &tm_buf) == 0) {\n    return std::string();\n  }\n\n  return std::string(buf);\n}\n\n// Parse HTTP-date (RFC 9110 Section 5.6.7) to time_t. Returns -1 on failure.\ninline time_t parse_http_date(const std::string &date_str) {\n  struct tm tm_buf;\n\n  // Create a classic locale object once for all parsing attempts\n  const std::locale classic_locale = std::locale::classic();\n\n  // Try to parse using std::get_time (C++11, cross-platform)\n  auto try_parse = [&](const char *fmt) -> bool {\n    std::istringstream ss(date_str);\n    ss.imbue(classic_locale);\n\n    memset(&tm_buf, 0, sizeof(tm_buf));\n    ss >> std::get_time(&tm_buf, fmt);\n\n    return !ss.fail();\n  };\n\n  // RFC 9110 preferred format (HTTP-date): \"Sun, 06 Nov 1994 08:49:37 GMT\"\n  if (!try_parse(\"%a, %d %b %Y %H:%M:%S\")) {\n    // RFC 850 format: \"Sunday, 06-Nov-94 08:49:37 GMT\"\n    if (!try_parse(\"%A, %d-%b-%y %H:%M:%S\")) {\n      // asctime format: \"Sun Nov  6 08:49:37 1994\"\n      if (!try_parse(\"%a %b %d %H:%M:%S %Y\")) {\n        return static_cast<time_t>(-1);\n      }\n    }\n  }\n\n#ifdef _WIN32\n  return _mkgmtime(&tm_buf);\n#elif defined _AIX\n  return mktime(&tm_buf);\n#else\n  return timegm(&tm_buf);\n#endif\n}\n\ninline bool is_weak_etag(const std::string &s) {\n  // Check if the string is a weak ETag (starts with 'W/\"')\n  return s.size() > 3 && s[0] == 'W' && s[1] == '/' && s[2] == '\"';\n}\n\ninline bool is_strong_etag(const std::string &s) {\n  // Check if the string is a strong ETag (starts and ends with '\"', at least 2\n  // chars)\n  return s.size() >= 2 && s[0] == '\"' && s.back() == '\"';\n}\n\ninline size_t to_utf8(int code, char *buff) {\n  if (code < 0x0080) {\n    buff[0] = static_cast<char>(code & 0x7F);\n    return 1;\n  } else if (code < 0x0800) {\n    buff[0] = static_cast<char>(0xC0 | ((code >> 6) & 0x1F));\n    buff[1] = static_cast<char>(0x80 | (code & 0x3F));\n    return 2;\n  } else if (code < 0xD800) {\n    buff[0] = static_cast<char>(0xE0 | ((code >> 12) & 0xF));\n    buff[1] = static_cast<char>(0x80 | ((code >> 6) & 0x3F));\n    buff[2] = static_cast<char>(0x80 | (code & 0x3F));\n    return 3;\n  } else if (code < 0xE000) { // D800 - DFFF is invalid...\n    return 0;\n  } else if (code < 0x10000) {\n    buff[0] = static_cast<char>(0xE0 | ((code >> 12) & 0xF));\n    buff[1] = static_cast<char>(0x80 | ((code >> 6) & 0x3F));\n    buff[2] = static_cast<char>(0x80 | (code & 0x3F));\n    return 3;\n  } else if (code < 0x110000) {\n    buff[0] = static_cast<char>(0xF0 | ((code >> 18) & 0x7));\n    buff[1] = static_cast<char>(0x80 | ((code >> 12) & 0x3F));\n    buff[2] = static_cast<char>(0x80 | ((code >> 6) & 0x3F));\n    buff[3] = static_cast<char>(0x80 | (code & 0x3F));\n    return 4;\n  }\n\n  // NOTREACHED\n  return 0;\n}\n\n// NOTE: This code came up with the following stackoverflow post:\n// https://stackoverflow.com/questions/180947/base64-decode-snippet-in-c\ninline std::string base64_encode(const std::string &in) {\n  static const auto lookup =\n      \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n\n  std::string out;\n  out.reserve(in.size());\n\n  auto val = 0;\n  auto valb = -6;\n\n  for (auto c : in) {\n    val = (val << 8) + static_cast<uint8_t>(c);\n    valb += 8;\n    while (valb >= 0) {\n      out.push_back(lookup[(val >> valb) & 0x3F]);\n      valb -= 6;\n    }\n  }\n\n  if (valb > -6) { out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]); }\n\n  while (out.size() % 4) {\n    out.push_back('=');\n  }\n\n  return out;\n}\n\ninline bool is_valid_path(const std::string &path) {\n  size_t level = 0;\n  size_t i = 0;\n\n  // Skip slash\n  while (i < path.size() && path[i] == '/') {\n    i++;\n  }\n\n  while (i < path.size()) {\n    // Read component\n    auto beg = i;\n    while (i < path.size() && path[i] != '/') {\n      if (path[i] == '\\0') {\n        return false;\n      } else if (path[i] == '\\\\') {\n        return false;\n      }\n      i++;\n    }\n\n    auto len = i - beg;\n    assert(len > 0);\n\n    if (!path.compare(beg, len, \".\")) {\n      ;\n    } else if (!path.compare(beg, len, \"..\")) {\n      if (level == 0) { return false; }\n      level--;\n    } else {\n      level++;\n    }\n\n    // Skip slash\n    while (i < path.size() && path[i] == '/') {\n      i++;\n    }\n  }\n\n  return true;\n}\n\ninline FileStat::FileStat(const std::string &path) {\n#if defined(_WIN32)\n  auto wpath = u8string_to_wstring(path.c_str());\n  ret_ = _wstat(wpath.c_str(), &st_);\n#else\n  ret_ = stat(path.c_str(), &st_);\n#endif\n}\ninline bool FileStat::is_file() const {\n  return ret_ >= 0 && S_ISREG(st_.st_mode);\n}\ninline bool FileStat::is_dir() const {\n  return ret_ >= 0 && S_ISDIR(st_.st_mode);\n}\n\ninline time_t FileStat::mtime() const {\n  return ret_ >= 0 ? static_cast<time_t>(st_.st_mtime)\n                   : static_cast<time_t>(-1);\n}\n\ninline size_t FileStat::size() const {\n  return ret_ >= 0 ? static_cast<size_t>(st_.st_size) : 0;\n}\n\ninline std::string encode_path(const std::string &s) {\n  std::string result;\n  result.reserve(s.size());\n\n  for (size_t i = 0; s[i]; i++) {\n    switch (s[i]) {\n    case ' ': result += \"%20\"; break;\n    case '+': result += \"%2B\"; break;\n    case '\\r': result += \"%0D\"; break;\n    case '\\n': result += \"%0A\"; break;\n    case '\\'': result += \"%27\"; break;\n    case ',': result += \"%2C\"; break;\n    // case ':': result += \"%3A\"; break; // ok? probably...\n    case ';': result += \"%3B\"; break;\n    default:\n      auto c = static_cast<uint8_t>(s[i]);\n      if (c >= 0x80) {\n        result += '%';\n        char hex[4];\n        auto len = snprintf(hex, sizeof(hex) - 1, \"%02X\", c);\n        assert(len == 2);\n        result.append(hex, static_cast<size_t>(len));\n      } else {\n        result += s[i];\n      }\n      break;\n    }\n  }\n\n  return result;\n}\n\ninline std::string file_extension(const std::string &path) {\n  std::smatch m;\n  thread_local auto re = std::regex(\"\\\\.([a-zA-Z0-9]+)$\");\n  if (std::regex_search(path, m, re)) { return m[1].str(); }\n  return std::string();\n}\n\ninline bool is_space_or_tab(char c) { return c == ' ' || c == '\\t'; }\n\ntemplate <typename T>\ninline bool parse_header(const char *beg, const char *end, T fn);\n\ntemplate <typename T>\ninline bool parse_header(const char *beg, const char *end, T fn) {\n  // Skip trailing spaces and tabs.\n  while (beg < end && is_space_or_tab(end[-1])) {\n    end--;\n  }\n\n  auto p = beg;\n  while (p < end && *p != ':') {\n    p++;\n  }\n\n  auto name = std::string(beg, p);\n  if (!detail::fields::is_field_name(name)) { return false; }\n\n  if (p == end) { return false; }\n\n  auto key_end = p;\n\n  if (*p++ != ':') { return false; }\n\n  while (p < end && is_space_or_tab(*p)) {\n    p++;\n  }\n\n  if (p <= end) {\n    auto key_len = key_end - beg;\n    if (!key_len) { return false; }\n\n    auto key = std::string(beg, key_end);\n    auto val = std::string(p, end);\n\n    if (!detail::fields::is_field_value(val)) { return false; }\n\n    if (case_ignore::equal(key, \"Location\") ||\n        case_ignore::equal(key, \"Referer\")) {\n      fn(key, val);\n    } else {\n      fn(key, decode_path_component(val));\n    }\n\n    return true;\n  }\n\n  return false;\n}\n\ninline bool parse_trailers(stream_line_reader &line_reader, Headers &dest,\n                           const Headers &src_headers) {\n  // NOTE: In RFC 9112, '7.1 Chunked Transfer Coding' mentions \"The chunked\n  // transfer coding is complete when a chunk with a chunk-size of zero is\n  // received, possibly followed by a trailer section, and finally terminated by\n  // an empty line\". https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1\n  //\n  // In '7.1.3. Decoding Chunked', however, the pseudo-code in the section\n  // doesn't care for the existence of the final CRLF. In other words, it seems\n  // to be ok whether the final CRLF exists or not in the chunked data.\n  // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1.3\n  //\n  // According to the reference code in RFC 9112, cpp-httplib now allows\n  // chunked transfer coding data without the final CRLF.\n\n  // RFC 7230 Section 4.1.2 - Headers prohibited in trailers\n  thread_local case_ignore::unordered_set<std::string> prohibited_trailers = {\n      \"transfer-encoding\",\n      \"content-length\",\n      \"host\",\n      \"authorization\",\n      \"www-authenticate\",\n      \"proxy-authenticate\",\n      \"proxy-authorization\",\n      \"cookie\",\n      \"set-cookie\",\n      \"cache-control\",\n      \"expect\",\n      \"max-forwards\",\n      \"pragma\",\n      \"range\",\n      \"te\",\n      \"age\",\n      \"expires\",\n      \"date\",\n      \"location\",\n      \"retry-after\",\n      \"vary\",\n      \"warning\",\n      \"content-encoding\",\n      \"content-type\",\n      \"content-range\",\n      \"trailer\"};\n\n  case_ignore::unordered_set<std::string> declared_trailers;\n  auto trailer_header = get_header_value(src_headers, \"Trailer\", \"\", 0);\n  if (trailer_header && std::strlen(trailer_header)) {\n    auto len = std::strlen(trailer_header);\n    split(trailer_header, trailer_header + len, ',',\n          [&](const char *b, const char *e) {\n            const char *kbeg = b;\n            const char *kend = e;\n            while (kbeg < kend && (*kbeg == ' ' || *kbeg == '\\t')) {\n              ++kbeg;\n            }\n            while (kend > kbeg && (kend[-1] == ' ' || kend[-1] == '\\t')) {\n              --kend;\n            }\n            std::string key(kbeg, static_cast<size_t>(kend - kbeg));\n            if (!key.empty() &&\n                prohibited_trailers.find(key) == prohibited_trailers.end()) {\n              declared_trailers.insert(key);\n            }\n          });\n  }\n\n  size_t trailer_header_count = 0;\n  while (strcmp(line_reader.ptr(), \"\\r\\n\") != 0) {\n    if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }\n    if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; }\n\n    constexpr auto line_terminator_len = 2;\n    auto line_beg = line_reader.ptr();\n    auto line_end =\n        line_reader.ptr() + line_reader.size() - line_terminator_len;\n\n    if (!parse_header(line_beg, line_end,\n                      [&](const std::string &key, const std::string &val) {\n                        if (declared_trailers.find(key) !=\n                            declared_trailers.end()) {\n                          dest.emplace(key, val);\n                          trailer_header_count++;\n                        }\n                      })) {\n      return false;\n    }\n\n    if (!line_reader.getline()) { return false; }\n  }\n\n  return true;\n}\n\ninline std::pair<size_t, size_t> trim(const char *b, const char *e, size_t left,\n                                      size_t right) {\n  while (b + left < e && is_space_or_tab(b[left])) {\n    left++;\n  }\n  while (right > 0 && is_space_or_tab(b[right - 1])) {\n    right--;\n  }\n  return std::make_pair(left, right);\n}\n\ninline std::string trim_copy(const std::string &s) {\n  auto r = trim(s.data(), s.data() + s.size(), 0, s.size());\n  return s.substr(r.first, r.second - r.first);\n}\n\ninline std::string trim_double_quotes_copy(const std::string &s) {\n  if (s.length() >= 2 && s.front() == '\"' && s.back() == '\"') {\n    return s.substr(1, s.size() - 2);\n  }\n  return s;\n}\n\ninline void\ndivide(const char *data, std::size_t size, char d,\n       std::function<void(const char *, std::size_t, const char *, std::size_t)>\n           fn) {\n  const auto it = std::find(data, data + size, d);\n  const auto found = static_cast<std::size_t>(it != data + size);\n  const auto lhs_data = data;\n  const auto lhs_size = static_cast<std::size_t>(it - data);\n  const auto rhs_data = it + found;\n  const auto rhs_size = size - lhs_size - found;\n\n  fn(lhs_data, lhs_size, rhs_data, rhs_size);\n}\n\ninline void\ndivide(const std::string &str, char d,\n       std::function<void(const char *, std::size_t, const char *, std::size_t)>\n           fn) {\n  divide(str.data(), str.size(), d, std::move(fn));\n}\n\ninline void split(const char *b, const char *e, char d,\n                  std::function<void(const char *, const char *)> fn) {\n  return split(b, e, d, (std::numeric_limits<size_t>::max)(), std::move(fn));\n}\n\ninline void split(const char *b, const char *e, char d, size_t m,\n                  std::function<void(const char *, const char *)> fn) {\n  size_t i = 0;\n  size_t beg = 0;\n  size_t count = 1;\n\n  while (e ? (b + i < e) : (b[i] != '\\0')) {\n    if (b[i] == d && count < m) {\n      auto r = trim(b, e, beg, i);\n      if (r.first < r.second) { fn(&b[r.first], &b[r.second]); }\n      beg = i + 1;\n      count++;\n    }\n    i++;\n  }\n\n  if (i) {\n    auto r = trim(b, e, beg, i);\n    if (r.first < r.second) { fn(&b[r.first], &b[r.second]); }\n  }\n}\n\ninline bool split_find(const char *b, const char *e, char d, size_t m,\n                       std::function<bool(const char *, const char *)> fn) {\n  size_t i = 0;\n  size_t beg = 0;\n  size_t count = 1;\n\n  while (e ? (b + i < e) : (b[i] != '\\0')) {\n    if (b[i] == d && count < m) {\n      auto r = trim(b, e, beg, i);\n      if (r.first < r.second) {\n        auto found = fn(&b[r.first], &b[r.second]);\n        if (found) { return true; }\n      }\n      beg = i + 1;\n      count++;\n    }\n    i++;\n  }\n\n  if (i) {\n    auto r = trim(b, e, beg, i);\n    if (r.first < r.second) {\n      auto found = fn(&b[r.first], &b[r.second]);\n      if (found) { return true; }\n    }\n  }\n\n  return false;\n}\n\ninline bool split_find(const char *b, const char *e, char d,\n                       std::function<bool(const char *, const char *)> fn) {\n  return split_find(b, e, d, (std::numeric_limits<size_t>::max)(),\n                    std::move(fn));\n}\n\ninline stream_line_reader::stream_line_reader(Stream &strm, char *fixed_buffer,\n                                              size_t fixed_buffer_size)\n    : strm_(strm), fixed_buffer_(fixed_buffer),\n      fixed_buffer_size_(fixed_buffer_size) {}\n\ninline const char *stream_line_reader::ptr() const {\n  if (growable_buffer_.empty()) {\n    return fixed_buffer_;\n  } else {\n    return growable_buffer_.data();\n  }\n}\n\ninline size_t stream_line_reader::size() const {\n  if (growable_buffer_.empty()) {\n    return fixed_buffer_used_size_;\n  } else {\n    return growable_buffer_.size();\n  }\n}\n\ninline bool stream_line_reader::end_with_crlf() const {\n  auto end = ptr() + size();\n  return size() >= 2 && end[-2] == '\\r' && end[-1] == '\\n';\n}\n\ninline bool stream_line_reader::getline() {\n  fixed_buffer_used_size_ = 0;\n  growable_buffer_.clear();\n\n#ifndef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR\n  char prev_byte = 0;\n#endif\n\n  for (size_t i = 0;; i++) {\n    if (size() >= CPPHTTPLIB_MAX_LINE_LENGTH) {\n      // Treat exceptionally long lines as an error to\n      // prevent infinite loops/memory exhaustion\n      return false;\n    }\n    char byte;\n    auto n = strm_.read(&byte, 1);\n\n    if (n < 0) {\n      return false;\n    } else if (n == 0) {\n      if (i == 0) {\n        return false;\n      } else {\n        break;\n      }\n    }\n\n    append(byte);\n\n#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR\n    if (byte == '\\n') { break; }\n#else\n    if (prev_byte == '\\r' && byte == '\\n') { break; }\n    prev_byte = byte;\n#endif\n  }\n\n  return true;\n}\n\ninline void stream_line_reader::append(char c) {\n  if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) {\n    fixed_buffer_[fixed_buffer_used_size_++] = c;\n    fixed_buffer_[fixed_buffer_used_size_] = '\\0';\n  } else {\n    if (growable_buffer_.empty()) {\n      assert(fixed_buffer_[fixed_buffer_used_size_] == '\\0');\n      growable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_);\n    }\n    growable_buffer_ += c;\n  }\n}\n\ninline mmap::mmap(const char *path) { open(path); }\n\ninline mmap::~mmap() { close(); }\n\ninline bool mmap::open(const char *path) {\n  close();\n\n#if defined(_WIN32)\n  auto wpath = u8string_to_wstring(path);\n  if (wpath.empty()) { return false; }\n\n  hFile_ = ::CreateFile2(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ,\n                         OPEN_EXISTING, NULL);\n\n  if (hFile_ == INVALID_HANDLE_VALUE) { return false; }\n\n  LARGE_INTEGER size{};\n  if (!::GetFileSizeEx(hFile_, &size)) { return false; }\n  // If the following line doesn't compile due to QuadPart, update Windows SDK.\n  // See:\n  // https://github.com/yhirose/cpp-httplib/issues/1903#issuecomment-2316520721\n  if (static_cast<ULONGLONG>(size.QuadPart) >\n      (std::numeric_limits<decltype(size_)>::max)()) {\n    // `size_t` might be 32-bits, on 32-bits Windows.\n    return false;\n  }\n  size_ = static_cast<size_t>(size.QuadPart);\n\n  hMapping_ =\n      ::CreateFileMappingFromApp(hFile_, NULL, PAGE_READONLY, size_, NULL);\n\n  // Special treatment for an empty file...\n  if (hMapping_ == NULL && size_ == 0) {\n    close();\n    is_open_empty_file = true;\n    return true;\n  }\n\n  if (hMapping_ == NULL) {\n    close();\n    return false;\n  }\n\n  addr_ = ::MapViewOfFileFromApp(hMapping_, FILE_MAP_READ, 0, 0);\n\n  if (addr_ == nullptr) {\n    close();\n    return false;\n  }\n#else\n  fd_ = ::open(path, O_RDONLY);\n  if (fd_ == -1) { return false; }\n\n  struct stat sb;\n  if (fstat(fd_, &sb) == -1) {\n    close();\n    return false;\n  }\n  size_ = static_cast<size_t>(sb.st_size);\n\n  addr_ = ::mmap(NULL, size_, PROT_READ, MAP_PRIVATE, fd_, 0);\n\n  // Special treatment for an empty file...\n  if (addr_ == MAP_FAILED && size_ == 0) {\n    close();\n    is_open_empty_file = true;\n    return false;\n  }\n#endif\n\n  return true;\n}\n\ninline bool mmap::is_open() const {\n  return is_open_empty_file ? true : addr_ != nullptr;\n}\n\ninline size_t mmap::size() const { return size_; }\n\ninline const char *mmap::data() const {\n  return is_open_empty_file ? \"\" : static_cast<const char *>(addr_);\n}\n\ninline void mmap::close() {\n#if defined(_WIN32)\n  if (addr_) {\n    ::UnmapViewOfFile(addr_);\n    addr_ = nullptr;\n  }\n\n  if (hMapping_) {\n    ::CloseHandle(hMapping_);\n    hMapping_ = NULL;\n  }\n\n  if (hFile_ != INVALID_HANDLE_VALUE) {\n    ::CloseHandle(hFile_);\n    hFile_ = INVALID_HANDLE_VALUE;\n  }\n\n  is_open_empty_file = false;\n#else\n  if (addr_ != nullptr) {\n    munmap(addr_, size_);\n    addr_ = nullptr;\n  }\n\n  if (fd_ != -1) {\n    ::close(fd_);\n    fd_ = -1;\n  }\n#endif\n  size_ = 0;\n}\ninline int close_socket(socket_t sock) {\n#ifdef _WIN32\n  return closesocket(sock);\n#else\n  return close(sock);\n#endif\n}\n\ntemplate <typename T> inline ssize_t handle_EINTR(T fn) {\n  ssize_t res = 0;\n  while (true) {\n    res = fn();\n    if (res < 0 && errno == EINTR) {\n      std::this_thread::sleep_for(std::chrono::microseconds{1});\n      continue;\n    }\n    break;\n  }\n  return res;\n}\n\ninline ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags) {\n  return handle_EINTR([&]() {\n    return recv(sock,\n#ifdef _WIN32\n                static_cast<char *>(ptr), static_cast<int>(size),\n#else\n                ptr, size,\n#endif\n                flags);\n  });\n}\n\ninline ssize_t send_socket(socket_t sock, const void *ptr, size_t size,\n                           int flags) {\n  return handle_EINTR([&]() {\n    return send(sock,\n#ifdef _WIN32\n                static_cast<const char *>(ptr), static_cast<int>(size),\n#else\n                ptr, size,\n#endif\n                flags);\n  });\n}\n\ninline int poll_wrapper(struct pollfd *fds, nfds_t nfds, int timeout) {\n#ifdef _WIN32\n  return ::WSAPoll(fds, nfds, timeout);\n#else\n  return ::poll(fds, nfds, timeout);\n#endif\n}\n\ntemplate <bool Read>\ninline ssize_t select_impl(socket_t sock, time_t sec, time_t usec) {\n#ifdef __APPLE__\n  if (sock >= FD_SETSIZE) { return -1; }\n\n  fd_set fds, *rfds, *wfds;\n  FD_ZERO(&fds);\n  FD_SET(sock, &fds);\n  rfds = (Read ? &fds : nullptr);\n  wfds = (Read ? nullptr : &fds);\n\n  timeval tv;\n  tv.tv_sec = static_cast<long>(sec);\n  tv.tv_usec = static_cast<decltype(tv.tv_usec)>(usec);\n\n  return handle_EINTR([&]() {\n    return select(static_cast<int>(sock + 1), rfds, wfds, nullptr, &tv);\n  });\n#else\n  struct pollfd pfd;\n  pfd.fd = sock;\n  pfd.events = (Read ? POLLIN : POLLOUT);\n\n  auto timeout = static_cast<int>(sec * 1000 + usec / 1000);\n\n  return handle_EINTR([&]() { return poll_wrapper(&pfd, 1, timeout); });\n#endif\n}\n\ninline ssize_t select_read(socket_t sock, time_t sec, time_t usec) {\n  return select_impl<true>(sock, sec, usec);\n}\n\ninline ssize_t select_write(socket_t sock, time_t sec, time_t usec) {\n  return select_impl<false>(sock, sec, usec);\n}\n\ninline Error wait_until_socket_is_ready(socket_t sock, time_t sec,\n                                        time_t usec) {\n#ifdef __APPLE__\n  if (sock >= FD_SETSIZE) { return Error::Connection; }\n\n  fd_set fdsr, fdsw;\n  FD_ZERO(&fdsr);\n  FD_ZERO(&fdsw);\n  FD_SET(sock, &fdsr);\n  FD_SET(sock, &fdsw);\n\n  timeval tv;\n  tv.tv_sec = static_cast<long>(sec);\n  tv.tv_usec = static_cast<decltype(tv.tv_usec)>(usec);\n\n  auto ret = handle_EINTR([&]() {\n    return select(static_cast<int>(sock + 1), &fdsr, &fdsw, nullptr, &tv);\n  });\n\n  if (ret == 0) { return Error::ConnectionTimeout; }\n\n  if (ret > 0 && (FD_ISSET(sock, &fdsr) || FD_ISSET(sock, &fdsw))) {\n    auto error = 0;\n    socklen_t len = sizeof(error);\n    auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR,\n                          reinterpret_cast<char *>(&error), &len);\n    auto successful = res >= 0 && !error;\n    return successful ? Error::Success : Error::Connection;\n  }\n\n  return Error::Connection;\n#else\n  struct pollfd pfd_read;\n  pfd_read.fd = sock;\n  pfd_read.events = POLLIN | POLLOUT;\n\n  auto timeout = static_cast<int>(sec * 1000 + usec / 1000);\n\n  auto poll_res =\n      handle_EINTR([&]() { return poll_wrapper(&pfd_read, 1, timeout); });\n\n  if (poll_res == 0) { return Error::ConnectionTimeout; }\n\n  if (poll_res > 0 && pfd_read.revents & (POLLIN | POLLOUT)) {\n    auto error = 0;\n    socklen_t len = sizeof(error);\n    auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR,\n                          reinterpret_cast<char *>(&error), &len);\n    auto successful = res >= 0 && !error;\n    return successful ? Error::Success : Error::Connection;\n  }\n\n  return Error::Connection;\n#endif\n}\n\ninline bool is_socket_alive(socket_t sock) {\n  const auto val = detail::select_read(sock, 0, 0);\n  if (val == 0) {\n    return true;\n  } else if (val < 0 && errno == EBADF) {\n    return false;\n  }\n  char buf[1];\n  return detail::read_socket(sock, &buf[0], sizeof(buf), MSG_PEEK) > 0;\n}\n\nclass SocketStream final : public Stream {\npublic:\n  SocketStream(socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec,\n               time_t write_timeout_sec, time_t write_timeout_usec,\n               time_t max_timeout_msec = 0,\n               std::chrono::time_point<std::chrono::steady_clock> start_time =\n                   (std::chrono::steady_clock::time_point::min)());\n  ~SocketStream() override;\n\n  bool is_readable() const override;\n  bool wait_readable() const override;\n  bool wait_writable() const override;\n  ssize_t read(char *ptr, size_t size) override;\n  ssize_t write(const char *ptr, size_t size) override;\n  void get_remote_ip_and_port(std::string &ip, int &port) const override;\n  void get_local_ip_and_port(std::string &ip, int &port) const override;\n  socket_t socket() const override;\n  time_t duration() const override;\n\nprivate:\n  socket_t sock_;\n  time_t read_timeout_sec_;\n  time_t read_timeout_usec_;\n  time_t write_timeout_sec_;\n  time_t write_timeout_usec_;\n  time_t max_timeout_msec_;\n  const std::chrono::time_point<std::chrono::steady_clock> start_time_;\n\n  std::vector<char> read_buff_;\n  size_t read_buff_off_ = 0;\n  size_t read_buff_content_size_ = 0;\n\n  static const size_t read_buff_size_ = 1024l * 4;\n};\n\ninline bool keep_alive(const std::atomic<socket_t> &svr_sock, socket_t sock,\n                       time_t keep_alive_timeout_sec) {\n  using namespace std::chrono;\n\n  const auto interval_usec =\n      CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND;\n\n  // Avoid expensive `steady_clock::now()` call for the first time\n  if (select_read(sock, 0, interval_usec) > 0) { return true; }\n\n  const auto start = steady_clock::now() - microseconds{interval_usec};\n  const auto timeout = seconds{keep_alive_timeout_sec};\n\n  while (true) {\n    if (svr_sock == INVALID_SOCKET) {\n      break; // Server socket is closed\n    }\n\n    auto val = select_read(sock, 0, interval_usec);\n    if (val < 0) {\n      break; // Ssocket error\n    } else if (val == 0) {\n      if (steady_clock::now() - start > timeout) {\n        break; // Timeout\n      }\n    } else {\n      return true; // Ready for read\n    }\n  }\n\n  return false;\n}\n\ntemplate <typename T>\ninline bool\nprocess_server_socket_core(const std::atomic<socket_t> &svr_sock, socket_t sock,\n                           size_t keep_alive_max_count,\n                           time_t keep_alive_timeout_sec, T callback) {\n  assert(keep_alive_max_count > 0);\n  auto ret = false;\n  auto count = keep_alive_max_count;\n  while (count > 0 && keep_alive(svr_sock, sock, keep_alive_timeout_sec)) {\n    auto close_connection = count == 1;\n    auto connection_closed = false;\n    ret = callback(close_connection, connection_closed);\n    if (!ret || connection_closed) { break; }\n    count--;\n  }\n  return ret;\n}\n\ntemplate <typename T>\ninline bool\nprocess_server_socket(const std::atomic<socket_t> &svr_sock, socket_t sock,\n                      size_t keep_alive_max_count,\n                      time_t keep_alive_timeout_sec, time_t read_timeout_sec,\n                      time_t read_timeout_usec, time_t write_timeout_sec,\n                      time_t write_timeout_usec, T callback) {\n  return process_server_socket_core(\n      svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec,\n      [&](bool close_connection, bool &connection_closed) {\n        SocketStream strm(sock, read_timeout_sec, read_timeout_usec,\n                          write_timeout_sec, write_timeout_usec);\n        return callback(strm, close_connection, connection_closed);\n      });\n}\n\ninline bool process_client_socket(\n    socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec,\n    time_t write_timeout_sec, time_t write_timeout_usec,\n    time_t max_timeout_msec,\n    std::chrono::time_point<std::chrono::steady_clock> start_time,\n    std::function<bool(Stream &)> callback) {\n  SocketStream strm(sock, read_timeout_sec, read_timeout_usec,\n                    write_timeout_sec, write_timeout_usec, max_timeout_msec,\n                    start_time);\n  return callback(strm);\n}\n\ninline int shutdown_socket(socket_t sock) {\n#ifdef _WIN32\n  return shutdown(sock, SD_BOTH);\n#else\n  return shutdown(sock, SHUT_RDWR);\n#endif\n}\n\ninline std::string escape_abstract_namespace_unix_domain(const std::string &s) {\n  if (s.size() > 1 && s[0] == '\\0') {\n    auto ret = s;\n    ret[0] = '@';\n    return ret;\n  }\n  return s;\n}\n\ninline std::string\nunescape_abstract_namespace_unix_domain(const std::string &s) {\n  if (s.size() > 1 && s[0] == '@') {\n    auto ret = s;\n    ret[0] = '\\0';\n    return ret;\n  }\n  return s;\n}\n\ninline int getaddrinfo_with_timeout(const char *node, const char *service,\n                                    const struct addrinfo *hints,\n                                    struct addrinfo **res, time_t timeout_sec) {\n#ifdef CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO\n  if (timeout_sec <= 0) {\n    // No timeout specified, use standard getaddrinfo\n    return getaddrinfo(node, service, hints, res);\n  }\n\n#ifdef _WIN32\n  // Windows-specific implementation using GetAddrInfoEx with overlapped I/O\n  OVERLAPPED overlapped = {0};\n  HANDLE event = CreateEventW(nullptr, TRUE, FALSE, nullptr);\n  if (!event) { return EAI_FAIL; }\n\n  overlapped.hEvent = event;\n\n  PADDRINFOEXW result_addrinfo = nullptr;\n  HANDLE cancel_handle = nullptr;\n\n  ADDRINFOEXW hints_ex = {0};\n  if (hints) {\n    hints_ex.ai_flags = hints->ai_flags;\n    hints_ex.ai_family = hints->ai_family;\n    hints_ex.ai_socktype = hints->ai_socktype;\n    hints_ex.ai_protocol = hints->ai_protocol;\n  }\n\n  auto wnode = u8string_to_wstring(node);\n  auto wservice = u8string_to_wstring(service);\n\n  auto ret = ::GetAddrInfoExW(wnode.data(), wservice.data(), NS_DNS, nullptr,\n                              hints ? &hints_ex : nullptr, &result_addrinfo,\n                              nullptr, &overlapped, nullptr, &cancel_handle);\n\n  if (ret == WSA_IO_PENDING) {\n    auto wait_result =\n        ::WaitForSingleObject(event, static_cast<DWORD>(timeout_sec * 1000));\n    if (wait_result == WAIT_TIMEOUT) {\n      if (cancel_handle) { ::GetAddrInfoExCancel(&cancel_handle); }\n      ::CloseHandle(event);\n      return EAI_AGAIN;\n    }\n\n    DWORD bytes_returned;\n    if (!::GetOverlappedResult((HANDLE)INVALID_SOCKET, &overlapped,\n                               &bytes_returned, FALSE)) {\n      ::CloseHandle(event);\n      return ::WSAGetLastError();\n    }\n  }\n\n  ::CloseHandle(event);\n\n  if (ret == NO_ERROR || ret == WSA_IO_PENDING) {\n    *res = reinterpret_cast<struct addrinfo *>(result_addrinfo);\n    return 0;\n  }\n\n  return ret;\n#elif TARGET_OS_MAC\n  if (!node) { return EAI_NONAME; }\n  // macOS implementation using CFHost API for asynchronous DNS resolution\n  CFStringRef hostname_ref = CFStringCreateWithCString(\n      kCFAllocatorDefault, node, kCFStringEncodingUTF8);\n  if (!hostname_ref) { return EAI_MEMORY; }\n\n  CFHostRef host_ref = CFHostCreateWithName(kCFAllocatorDefault, hostname_ref);\n  CFRelease(hostname_ref);\n  if (!host_ref) { return EAI_MEMORY; }\n\n  // Set up context for callback\n  struct CFHostContext {\n    bool completed = false;\n    bool success = false;\n    CFArrayRef addresses = nullptr;\n    std::mutex mutex;\n    std::condition_variable cv;\n  } context;\n\n  CFHostClientContext client_context;\n  memset(&client_context, 0, sizeof(client_context));\n  client_context.info = &context;\n\n  // Set callback\n  auto callback = [](CFHostRef theHost, CFHostInfoType /*typeInfo*/,\n                     const CFStreamError *error, void *info) {\n    auto ctx = static_cast<CFHostContext *>(info);\n    std::lock_guard<std::mutex> lock(ctx->mutex);\n\n    if (error && error->error != 0) {\n      ctx->success = false;\n    } else {\n      Boolean hasBeenResolved;\n      ctx->addresses = CFHostGetAddressing(theHost, &hasBeenResolved);\n      if (ctx->addresses && hasBeenResolved) {\n        CFRetain(ctx->addresses);\n        ctx->success = true;\n      } else {\n        ctx->success = false;\n      }\n    }\n    ctx->completed = true;\n    ctx->cv.notify_one();\n  };\n\n  if (!CFHostSetClient(host_ref, callback, &client_context)) {\n    CFRelease(host_ref);\n    return EAI_SYSTEM;\n  }\n\n  // Schedule on run loop\n  CFRunLoopRef run_loop = CFRunLoopGetCurrent();\n  CFHostScheduleWithRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode);\n\n  // Start resolution\n  CFStreamError stream_error;\n  if (!CFHostStartInfoResolution(host_ref, kCFHostAddresses, &stream_error)) {\n    CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode);\n    CFRelease(host_ref);\n    return EAI_FAIL;\n  }\n\n  // Wait for completion with timeout\n  auto timeout_time =\n      std::chrono::steady_clock::now() + std::chrono::seconds(timeout_sec);\n  bool timed_out = false;\n\n  {\n    std::unique_lock<std::mutex> lock(context.mutex);\n\n    while (!context.completed) {\n      auto now = std::chrono::steady_clock::now();\n      if (now >= timeout_time) {\n        timed_out = true;\n        break;\n      }\n\n      // Run the runloop for a short time\n      lock.unlock();\n      CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, true);\n      lock.lock();\n    }\n  }\n\n  // Clean up\n  CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode);\n  CFHostSetClient(host_ref, nullptr, nullptr);\n\n  if (timed_out || !context.completed) {\n    CFHostCancelInfoResolution(host_ref, kCFHostAddresses);\n    CFRelease(host_ref);\n    return EAI_AGAIN;\n  }\n\n  if (!context.success || !context.addresses) {\n    CFRelease(host_ref);\n    return EAI_NODATA;\n  }\n\n  // Convert CFArray to addrinfo\n  CFIndex count = CFArrayGetCount(context.addresses);\n  if (count == 0) {\n    CFRelease(context.addresses);\n    CFRelease(host_ref);\n    return EAI_NODATA;\n  }\n\n  struct addrinfo *result_addrinfo = nullptr;\n  struct addrinfo **current = &result_addrinfo;\n\n  for (CFIndex i = 0; i < count; i++) {\n    CFDataRef addr_data =\n        static_cast<CFDataRef>(CFArrayGetValueAtIndex(context.addresses, i));\n    if (!addr_data) continue;\n\n    const struct sockaddr *sockaddr_ptr =\n        reinterpret_cast<const struct sockaddr *>(CFDataGetBytePtr(addr_data));\n    socklen_t sockaddr_len = static_cast<socklen_t>(CFDataGetLength(addr_data));\n\n    // Allocate addrinfo structure\n    *current = static_cast<struct addrinfo *>(malloc(sizeof(struct addrinfo)));\n    if (!*current) {\n      freeaddrinfo(result_addrinfo);\n      CFRelease(context.addresses);\n      CFRelease(host_ref);\n      return EAI_MEMORY;\n    }\n\n    memset(*current, 0, sizeof(struct addrinfo));\n\n    // Set up addrinfo fields\n    (*current)->ai_family = sockaddr_ptr->sa_family;\n    (*current)->ai_socktype = hints ? hints->ai_socktype : SOCK_STREAM;\n    (*current)->ai_protocol = hints ? hints->ai_protocol : IPPROTO_TCP;\n    (*current)->ai_addrlen = sockaddr_len;\n\n    // Copy sockaddr\n    (*current)->ai_addr = static_cast<struct sockaddr *>(malloc(sockaddr_len));\n    if (!(*current)->ai_addr) {\n      freeaddrinfo(result_addrinfo);\n      CFRelease(context.addresses);\n      CFRelease(host_ref);\n      return EAI_MEMORY;\n    }\n    memcpy((*current)->ai_addr, sockaddr_ptr, sockaddr_len);\n\n    // Set port if service is specified\n    if (service && strlen(service) > 0) {\n      int port = atoi(service);\n      if (port > 0) {\n        if (sockaddr_ptr->sa_family == AF_INET) {\n          reinterpret_cast<struct sockaddr_in *>((*current)->ai_addr)\n              ->sin_port = htons(static_cast<uint16_t>(port));\n        } else if (sockaddr_ptr->sa_family == AF_INET6) {\n          reinterpret_cast<struct sockaddr_in6 *>((*current)->ai_addr)\n              ->sin6_port = htons(static_cast<uint16_t>(port));\n        }\n      }\n    }\n\n    current = &((*current)->ai_next);\n  }\n\n  CFRelease(context.addresses);\n  CFRelease(host_ref);\n\n  *res = result_addrinfo;\n  return 0;\n#elif defined(_GNU_SOURCE) && defined(__GLIBC__) &&                            \\\n    (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2))\n  // Linux implementation using getaddrinfo_a for asynchronous DNS resolution\n  struct gaicb request;\n  struct gaicb *requests[1] = {&request};\n  struct sigevent sevp;\n  struct timespec timeout;\n\n  // Initialize the request structure\n  memset(&request, 0, sizeof(request));\n  request.ar_name = node;\n  request.ar_service = service;\n  request.ar_request = hints;\n\n  // Set up timeout\n  timeout.tv_sec = timeout_sec;\n  timeout.tv_nsec = 0;\n\n  // Initialize sigevent structure (not used, but required)\n  memset(&sevp, 0, sizeof(sevp));\n  sevp.sigev_notify = SIGEV_NONE;\n\n  // Start asynchronous resolution\n  int start_result = getaddrinfo_a(GAI_NOWAIT, requests, 1, &sevp);\n  if (start_result != 0) { return start_result; }\n\n  // Wait for completion with timeout\n  int wait_result =\n      gai_suspend((const struct gaicb *const *)requests, 1, &timeout);\n\n  if (wait_result == 0 || wait_result == EAI_ALLDONE) {\n    // Completed successfully, get the result\n    int gai_result = gai_error(&request);\n    if (gai_result == 0) {\n      *res = request.ar_result;\n      return 0;\n    } else {\n      // Clean up on error\n      if (request.ar_result) { freeaddrinfo(request.ar_result); }\n      return gai_result;\n    }\n  } else if (wait_result == EAI_AGAIN) {\n    // Timeout occurred, cancel the request\n    gai_cancel(&request);\n    return EAI_AGAIN;\n  } else {\n    // Other error occurred\n    gai_cancel(&request);\n    return wait_result;\n  }\n#else\n  // Fallback implementation using thread-based timeout for other Unix systems\n\n  struct GetAddrInfoState {\n    ~GetAddrInfoState() {\n      if (info) { freeaddrinfo(info); }\n    }\n\n    std::mutex mutex;\n    std::condition_variable result_cv;\n    bool completed = false;\n    int result = EAI_SYSTEM;\n    std::string node;\n    std::string service;\n    struct addrinfo hints;\n    struct addrinfo *info = nullptr;\n  };\n\n  // Allocate on the heap, so the resolver thread can keep using the data.\n  auto state = std::make_shared<GetAddrInfoState>();\n  if (node) { state->node = node; }\n  state->service = service;\n  state->hints = *hints;\n\n  std::thread resolve_thread([state]() {\n    auto thread_result =\n        getaddrinfo(state->node.c_str(), state->service.c_str(), &state->hints,\n                    &state->info);\n\n    std::lock_guard<std::mutex> lock(state->mutex);\n    state->result = thread_result;\n    state->completed = true;\n    state->result_cv.notify_one();\n  });\n\n  // Wait for completion or timeout\n  std::unique_lock<std::mutex> lock(state->mutex);\n  auto finished =\n      state->result_cv.wait_for(lock, std::chrono::seconds(timeout_sec),\n                                [&] { return state->completed; });\n\n  if (finished) {\n    // Operation completed within timeout\n    resolve_thread.join();\n    *res = state->info;\n    state->info = nullptr; // Pass ownership to caller\n    return state->result;\n  } else {\n    // Timeout occurred\n    resolve_thread.detach(); // Let the thread finish in background\n    return EAI_AGAIN;        // Return timeout error\n  }\n#endif\n#else\n  (void)(timeout_sec); // Unused parameter for non-blocking getaddrinfo\n  return getaddrinfo(node, service, hints, res);\n#endif\n}\n\ntemplate <typename BindOrConnect>\nsocket_t create_socket(const std::string &host, const std::string &ip, int port,\n                       int address_family, int socket_flags, bool tcp_nodelay,\n                       bool ipv6_v6only, SocketOptions socket_options,\n                       BindOrConnect bind_or_connect, time_t timeout_sec = 0) {\n  // Get address info\n  const char *node = nullptr;\n  struct addrinfo hints;\n  struct addrinfo *result;\n\n  memset(&hints, 0, sizeof(struct addrinfo));\n  hints.ai_socktype = SOCK_STREAM;\n  hints.ai_protocol = IPPROTO_IP;\n\n  if (!ip.empty()) {\n    node = ip.c_str();\n    // Ask getaddrinfo to convert IP in c-string to address\n    hints.ai_family = AF_UNSPEC;\n    hints.ai_flags = AI_NUMERICHOST;\n  } else {\n    if (!host.empty()) { node = host.c_str(); }\n    hints.ai_family = address_family;\n    hints.ai_flags = socket_flags;\n  }\n\n#if !defined(_WIN32) || defined(CPPHTTPLIB_HAVE_AFUNIX_H)\n  if (hints.ai_family == AF_UNIX) {\n    const auto addrlen = host.length();\n    if (addrlen > sizeof(sockaddr_un::sun_path)) { return INVALID_SOCKET; }\n\n#ifdef SOCK_CLOEXEC\n    auto sock = socket(hints.ai_family, hints.ai_socktype | SOCK_CLOEXEC,\n                       hints.ai_protocol);\n#else\n    auto sock = socket(hints.ai_family, hints.ai_socktype, hints.ai_protocol);\n#endif\n\n    if (sock != INVALID_SOCKET) {\n      sockaddr_un addr{};\n      addr.sun_family = AF_UNIX;\n\n      auto unescaped_host = unescape_abstract_namespace_unix_domain(host);\n      std::copy(unescaped_host.begin(), unescaped_host.end(), addr.sun_path);\n\n      hints.ai_addr = reinterpret_cast<sockaddr *>(&addr);\n      hints.ai_addrlen = static_cast<socklen_t>(\n          sizeof(addr) - sizeof(addr.sun_path) + addrlen);\n\n#ifndef SOCK_CLOEXEC\n#ifndef _WIN32\n      fcntl(sock, F_SETFD, FD_CLOEXEC);\n#endif\n#endif\n\n      if (socket_options) { socket_options(sock); }\n\n#ifdef _WIN32\n      // Setting SO_REUSEADDR seems not to work well with AF_UNIX on windows, so\n      // remove the option.\n      detail::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 0);\n#endif\n\n      bool dummy;\n      if (!bind_or_connect(sock, hints, dummy)) {\n        close_socket(sock);\n        sock = INVALID_SOCKET;\n      }\n    }\n    return sock;\n  }\n#endif\n\n  auto service = std::to_string(port);\n\n  if (getaddrinfo_with_timeout(node, service.c_str(), &hints, &result,\n                               timeout_sec)) {\n#if defined __linux__ && !defined __ANDROID__\n    res_init();\n#endif\n    return INVALID_SOCKET;\n  }\n  auto se = detail::scope_exit([&] { freeaddrinfo(result); });\n\n  for (auto rp = result; rp; rp = rp->ai_next) {\n    // Create a socket\n#ifdef _WIN32\n    auto sock =\n        WSASocketW(rp->ai_family, rp->ai_socktype, rp->ai_protocol, nullptr, 0,\n                   WSA_FLAG_NO_HANDLE_INHERIT | WSA_FLAG_OVERLAPPED);\n    /**\n     * Since the WSA_FLAG_NO_HANDLE_INHERIT is only supported on Windows 7 SP1\n     * and above the socket creation fails on older Windows Systems.\n     *\n     * Let's try to create a socket the old way in this case.\n     *\n     * Reference:\n     * https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketa\n     *\n     * WSA_FLAG_NO_HANDLE_INHERIT:\n     * This flag is supported on Windows 7 with SP1, Windows Server 2008 R2 with\n     * SP1, and later\n     *\n     */\n    if (sock == INVALID_SOCKET) {\n      sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);\n    }\n#else\n\n#ifdef SOCK_CLOEXEC\n    auto sock =\n        socket(rp->ai_family, rp->ai_socktype | SOCK_CLOEXEC, rp->ai_protocol);\n#else\n    auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);\n#endif\n\n#endif\n    if (sock == INVALID_SOCKET) { continue; }\n\n#if !defined _WIN32 && !defined SOCK_CLOEXEC\n    if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) {\n      close_socket(sock);\n      continue;\n    }\n#endif\n\n    if (tcp_nodelay) { set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1); }\n\n    if (rp->ai_family == AF_INET6) {\n      set_socket_opt(sock, IPPROTO_IPV6, IPV6_V6ONLY, ipv6_v6only ? 1 : 0);\n    }\n\n    if (socket_options) { socket_options(sock); }\n\n    // bind or connect\n    auto quit = false;\n    if (bind_or_connect(sock, *rp, quit)) { return sock; }\n\n    close_socket(sock);\n\n    if (quit) { break; }\n  }\n\n  return INVALID_SOCKET;\n}\n\ninline void set_nonblocking(socket_t sock, bool nonblocking) {\n#ifdef _WIN32\n  auto flags = nonblocking ? 1UL : 0UL;\n  ioctlsocket(sock, FIONBIO, &flags);\n#else\n  auto flags = fcntl(sock, F_GETFL, 0);\n  fcntl(sock, F_SETFL,\n        nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK)));\n#endif\n}\n\ninline bool is_connection_error() {\n#ifdef _WIN32\n  return WSAGetLastError() != WSAEWOULDBLOCK;\n#else\n  return errno != EINPROGRESS;\n#endif\n}\n\ninline bool bind_ip_address(socket_t sock, const std::string &host) {\n  struct addrinfo hints;\n  struct addrinfo *result;\n\n  memset(&hints, 0, sizeof(struct addrinfo));\n  hints.ai_family = AF_UNSPEC;\n  hints.ai_socktype = SOCK_STREAM;\n  hints.ai_protocol = 0;\n\n  if (getaddrinfo_with_timeout(host.c_str(), \"0\", &hints, &result, 0)) {\n    return false;\n  }\n\n  auto se = detail::scope_exit([&] { freeaddrinfo(result); });\n\n  auto ret = false;\n  for (auto rp = result; rp; rp = rp->ai_next) {\n    const auto &ai = *rp;\n    if (!::bind(sock, ai.ai_addr, static_cast<socklen_t>(ai.ai_addrlen))) {\n      ret = true;\n      break;\n    }\n  }\n\n  return ret;\n}\n\n#if !defined _WIN32 && !defined ANDROID && !defined _AIX && !defined __MVS__\n#define USE_IF2IP\n#endif\n\n#ifdef USE_IF2IP\ninline std::string if2ip(int address_family, const std::string &ifn) {\n  struct ifaddrs *ifap;\n  getifaddrs(&ifap);\n  auto se = detail::scope_exit([&] { freeifaddrs(ifap); });\n\n  std::string addr_candidate;\n  for (auto ifa = ifap; ifa; ifa = ifa->ifa_next) {\n    if (ifa->ifa_addr && ifn == ifa->ifa_name &&\n        (AF_UNSPEC == address_family ||\n         ifa->ifa_addr->sa_family == address_family)) {\n      if (ifa->ifa_addr->sa_family == AF_INET) {\n        auto sa = reinterpret_cast<struct sockaddr_in *>(ifa->ifa_addr);\n        char buf[INET_ADDRSTRLEN];\n        if (inet_ntop(AF_INET, &sa->sin_addr, buf, INET_ADDRSTRLEN)) {\n          return std::string(buf, INET_ADDRSTRLEN);\n        }\n      } else if (ifa->ifa_addr->sa_family == AF_INET6) {\n        auto sa = reinterpret_cast<struct sockaddr_in6 *>(ifa->ifa_addr);\n        if (!IN6_IS_ADDR_LINKLOCAL(&sa->sin6_addr)) {\n          char buf[INET6_ADDRSTRLEN] = {};\n          if (inet_ntop(AF_INET6, &sa->sin6_addr, buf, INET6_ADDRSTRLEN)) {\n            // equivalent to mac's IN6_IS_ADDR_UNIQUE_LOCAL\n            auto s6_addr_head = sa->sin6_addr.s6_addr[0];\n            if (s6_addr_head == 0xfc || s6_addr_head == 0xfd) {\n              addr_candidate = std::string(buf, INET6_ADDRSTRLEN);\n            } else {\n              return std::string(buf, INET6_ADDRSTRLEN);\n            }\n          }\n        }\n      }\n    }\n  }\n  return addr_candidate;\n}\n#endif\n\ninline socket_t create_client_socket(\n    const std::string &host, const std::string &ip, int port,\n    int address_family, bool tcp_nodelay, bool ipv6_v6only,\n    SocketOptions socket_options, time_t connection_timeout_sec,\n    time_t connection_timeout_usec, time_t read_timeout_sec,\n    time_t read_timeout_usec, time_t write_timeout_sec,\n    time_t write_timeout_usec, const std::string &intf, Error &error) {\n  auto sock = create_socket(\n      host, ip, port, address_family, 0, tcp_nodelay, ipv6_v6only,\n      std::move(socket_options),\n      [&](socket_t sock2, struct addrinfo &ai, bool &quit) -> bool {\n        if (!intf.empty()) {\n#ifdef USE_IF2IP\n          auto ip_from_if = if2ip(address_family, intf);\n          if (ip_from_if.empty()) { ip_from_if = intf; }\n          if (!bind_ip_address(sock2, ip_from_if)) {\n            error = Error::BindIPAddress;\n            return false;\n          }\n#endif\n        }\n\n        set_nonblocking(sock2, true);\n\n        auto ret =\n            ::connect(sock2, ai.ai_addr, static_cast<socklen_t>(ai.ai_addrlen));\n\n        if (ret < 0) {\n          if (is_connection_error()) {\n            error = Error::Connection;\n            return false;\n          }\n          error = wait_until_socket_is_ready(sock2, connection_timeout_sec,\n                                             connection_timeout_usec);\n          if (error != Error::Success) {\n            if (error == Error::ConnectionTimeout) { quit = true; }\n            return false;\n          }\n        }\n\n        set_nonblocking(sock2, false);\n        set_socket_opt_time(sock2, SOL_SOCKET, SO_RCVTIMEO, read_timeout_sec,\n                            read_timeout_usec);\n        set_socket_opt_time(sock2, SOL_SOCKET, SO_SNDTIMEO, write_timeout_sec,\n                            write_timeout_usec);\n\n        error = Error::Success;\n        return true;\n      },\n      connection_timeout_sec); // Pass DNS timeout\n\n  if (sock != INVALID_SOCKET) {\n    error = Error::Success;\n  } else {\n    if (error == Error::Success) { error = Error::Connection; }\n  }\n\n  return sock;\n}\n\ninline bool get_ip_and_port(const struct sockaddr_storage &addr,\n                            socklen_t addr_len, std::string &ip, int &port) {\n  if (addr.ss_family == AF_INET) {\n    port = ntohs(reinterpret_cast<const struct sockaddr_in *>(&addr)->sin_port);\n  } else if (addr.ss_family == AF_INET6) {\n    port =\n        ntohs(reinterpret_cast<const struct sockaddr_in6 *>(&addr)->sin6_port);\n  } else {\n    return false;\n  }\n\n  std::array<char, NI_MAXHOST> ipstr{};\n  if (getnameinfo(reinterpret_cast<const struct sockaddr *>(&addr), addr_len,\n                  ipstr.data(), static_cast<socklen_t>(ipstr.size()), nullptr,\n                  0, NI_NUMERICHOST)) {\n    return false;\n  }\n\n  ip = ipstr.data();\n  return true;\n}\n\ninline void get_local_ip_and_port(socket_t sock, std::string &ip, int &port) {\n  struct sockaddr_storage addr;\n  socklen_t addr_len = sizeof(addr);\n  if (!getsockname(sock, reinterpret_cast<struct sockaddr *>(&addr),\n                   &addr_len)) {\n    get_ip_and_port(addr, addr_len, ip, port);\n  }\n}\n\ninline void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) {\n  struct sockaddr_storage addr;\n  socklen_t addr_len = sizeof(addr);\n\n  if (!getpeername(sock, reinterpret_cast<struct sockaddr *>(&addr),\n                   &addr_len)) {\n#ifndef _WIN32\n    if (addr.ss_family == AF_UNIX) {\n#if defined(__linux__)\n      struct ucred ucred;\n      socklen_t len = sizeof(ucred);\n      if (getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &ucred, &len) == 0) {\n        port = ucred.pid;\n      }\n#elif defined(SOL_LOCAL) && defined(SO_PEERPID)\n      pid_t pid;\n      socklen_t len = sizeof(pid);\n      if (getsockopt(sock, SOL_LOCAL, SO_PEERPID, &pid, &len) == 0) {\n        port = pid;\n      }\n#endif\n      return;\n    }\n#endif\n    get_ip_and_port(addr, addr_len, ip, port);\n  }\n}\n\ninline constexpr unsigned int str2tag_core(const char *s, size_t l,\n                                           unsigned int h) {\n  return (l == 0)\n             ? h\n             : str2tag_core(\n                   s + 1, l - 1,\n                   // Unsets the 6 high bits of h, therefore no overflow happens\n                   (((std::numeric_limits<unsigned int>::max)() >> 6) &\n                    h * 33) ^\n                       static_cast<unsigned char>(*s));\n}\n\ninline unsigned int str2tag(const std::string &s) {\n  return str2tag_core(s.data(), s.size(), 0);\n}\n\nnamespace udl {\n\ninline constexpr unsigned int operator\"\"_t(const char *s, size_t l) {\n  return str2tag_core(s, l, 0);\n}\n\n} // namespace udl\n\ninline std::string\nfind_content_type(const std::string &path,\n                  const std::map<std::string, std::string> &user_data,\n                  const std::string &default_content_type) {\n  auto ext = file_extension(path);\n\n  auto it = user_data.find(ext);\n  if (it != user_data.end()) { return it->second; }\n\n  using udl::operator\"\"_t;\n\n  switch (str2tag(ext)) {\n  default: return default_content_type;\n\n  case \"css\"_t: return \"text/css\";\n  case \"csv\"_t: return \"text/csv\";\n  case \"htm\"_t:\n  case \"html\"_t: return \"text/html\";\n  case \"js\"_t:\n  case \"mjs\"_t: return \"text/javascript\";\n  case \"txt\"_t: return \"text/plain\";\n  case \"vtt\"_t: return \"text/vtt\";\n\n  case \"apng\"_t: return \"image/apng\";\n  case \"avif\"_t: return \"image/avif\";\n  case \"bmp\"_t: return \"image/bmp\";\n  case \"gif\"_t: return \"image/gif\";\n  case \"png\"_t: return \"image/png\";\n  case \"svg\"_t: return \"image/svg+xml\";\n  case \"webp\"_t: return \"image/webp\";\n  case \"ico\"_t: return \"image/x-icon\";\n  case \"tif\"_t: return \"image/tiff\";\n  case \"tiff\"_t: return \"image/tiff\";\n  case \"jpg\"_t:\n  case \"jpeg\"_t: return \"image/jpeg\";\n\n  case \"mp4\"_t: return \"video/mp4\";\n  case \"mpeg\"_t: return \"video/mpeg\";\n  case \"webm\"_t: return \"video/webm\";\n\n  case \"mp3\"_t: return \"audio/mp3\";\n  case \"mpga\"_t: return \"audio/mpeg\";\n  case \"weba\"_t: return \"audio/webm\";\n  case \"wav\"_t: return \"audio/wave\";\n\n  case \"otf\"_t: return \"font/otf\";\n  case \"ttf\"_t: return \"font/ttf\";\n  case \"woff\"_t: return \"font/woff\";\n  case \"woff2\"_t: return \"font/woff2\";\n\n  case \"7z\"_t: return \"application/x-7z-compressed\";\n  case \"atom\"_t: return \"application/atom+xml\";\n  case \"pdf\"_t: return \"application/pdf\";\n  case \"json\"_t: return \"application/json\";\n  case \"rss\"_t: return \"application/rss+xml\";\n  case \"tar\"_t: return \"application/x-tar\";\n  case \"xht\"_t:\n  case \"xhtml\"_t: return \"application/xhtml+xml\";\n  case \"xslt\"_t: return \"application/xslt+xml\";\n  case \"xml\"_t: return \"application/xml\";\n  case \"gz\"_t: return \"application/gzip\";\n  case \"zip\"_t: return \"application/zip\";\n  case \"wasm\"_t: return \"application/wasm\";\n  }\n}\n\ninline bool can_compress_content_type(const std::string &content_type) {\n  using udl::operator\"\"_t;\n\n  auto tag = str2tag(content_type);\n\n  switch (tag) {\n  case \"image/svg+xml\"_t:\n  case \"application/javascript\"_t:\n  case \"application/json\"_t:\n  case \"application/xml\"_t:\n  case \"application/protobuf\"_t:\n  case \"application/xhtml+xml\"_t: return true;\n\n  case \"text/event-stream\"_t: return false;\n\n  default: return !content_type.rfind(\"text/\", 0);\n  }\n}\n\ninline EncodingType encoding_type(const Request &req, const Response &res) {\n  auto ret =\n      detail::can_compress_content_type(res.get_header_value(\"Content-Type\"));\n  if (!ret) { return EncodingType::None; }\n\n  const auto &s = req.get_header_value(\"Accept-Encoding\");\n  (void)(s);\n\n#ifdef CPPHTTPLIB_BROTLI_SUPPORT\n  // TODO: 'Accept-Encoding' has br, not br;q=0\n  ret = s.find(\"br\") != std::string::npos;\n  if (ret) { return EncodingType::Brotli; }\n#endif\n\n#ifdef CPPHTTPLIB_ZLIB_SUPPORT\n  // TODO: 'Accept-Encoding' has gzip, not gzip;q=0\n  ret = s.find(\"gzip\") != std::string::npos;\n  if (ret) { return EncodingType::Gzip; }\n#endif\n\n#ifdef CPPHTTPLIB_ZSTD_SUPPORT\n  // TODO: 'Accept-Encoding' has zstd, not zstd;q=0\n  ret = s.find(\"zstd\") != std::string::npos;\n  if (ret) { return EncodingType::Zstd; }\n#endif\n\n  return EncodingType::None;\n}\n\ninline bool nocompressor::compress(const char *data, size_t data_length,\n                                   bool /*last*/, Callback callback) {\n  if (!data_length) { return true; }\n  return callback(data, data_length);\n}\n\n#ifdef CPPHTTPLIB_ZLIB_SUPPORT\ninline gzip_compressor::gzip_compressor() {\n  std::memset(&strm_, 0, sizeof(strm_));\n  strm_.zalloc = Z_NULL;\n  strm_.zfree = Z_NULL;\n  strm_.opaque = Z_NULL;\n\n  is_valid_ = deflateInit2(&strm_, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8,\n                           Z_DEFAULT_STRATEGY) == Z_OK;\n}\n\ninline gzip_compressor::~gzip_compressor() { deflateEnd(&strm_); }\n\ninline bool gzip_compressor::compress(const char *data, size_t data_length,\n                                      bool last, Callback callback) {\n  assert(is_valid_);\n\n  do {\n    constexpr size_t max_avail_in =\n        (std::numeric_limits<decltype(strm_.avail_in)>::max)();\n\n    strm_.avail_in = static_cast<decltype(strm_.avail_in)>(\n        (std::min)(data_length, max_avail_in));\n    strm_.next_in = const_cast<Bytef *>(reinterpret_cast<const Bytef *>(data));\n\n    data_length -= strm_.avail_in;\n    data += strm_.avail_in;\n\n    auto flush = (last && data_length == 0) ? Z_FINISH : Z_NO_FLUSH;\n    auto ret = Z_OK;\n\n    std::array<char, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};\n    do {\n      strm_.avail_out = static_cast<uInt>(buff.size());\n      strm_.next_out = reinterpret_cast<Bytef *>(buff.data());\n\n      ret = deflate(&strm_, flush);\n      if (ret == Z_STREAM_ERROR) { return false; }\n\n      if (!callback(buff.data(), buff.size() - strm_.avail_out)) {\n        return false;\n      }\n    } while (strm_.avail_out == 0);\n\n    assert((flush == Z_FINISH && ret == Z_STREAM_END) ||\n           (flush == Z_NO_FLUSH && ret == Z_OK));\n    assert(strm_.avail_in == 0);\n  } while (data_length > 0);\n\n  return true;\n}\n\ninline gzip_decompressor::gzip_decompressor() {\n  std::memset(&strm_, 0, sizeof(strm_));\n  strm_.zalloc = Z_NULL;\n  strm_.zfree = Z_NULL;\n  strm_.opaque = Z_NULL;\n\n  // 15 is the value of wbits, which should be at the maximum possible value\n  // to ensure that any gzip stream can be decoded. The offset of 32 specifies\n  // that the stream type should be automatically detected either gzip or\n  // deflate.\n  is_valid_ = inflateInit2(&strm_, 32 + 15) == Z_OK;\n}\n\ninline gzip_decompressor::~gzip_decompressor() { inflateEnd(&strm_); }\n\ninline bool gzip_decompressor::is_valid() const { return is_valid_; }\n\ninline bool gzip_decompressor::decompress(const char *data, size_t data_length,\n                                          Callback callback) {\n  assert(is_valid_);\n\n  auto ret = Z_OK;\n\n  do {\n    constexpr size_t max_avail_in =\n        (std::numeric_limits<decltype(strm_.avail_in)>::max)();\n\n    strm_.avail_in = static_cast<decltype(strm_.avail_in)>(\n        (std::min)(data_length, max_avail_in));\n    strm_.next_in = const_cast<Bytef *>(reinterpret_cast<const Bytef *>(data));\n\n    data_length -= strm_.avail_in;\n    data += strm_.avail_in;\n\n    std::array<char, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};\n    while (strm_.avail_in > 0 && ret == Z_OK) {\n      strm_.avail_out = static_cast<uInt>(buff.size());\n      strm_.next_out = reinterpret_cast<Bytef *>(buff.data());\n\n      ret = inflate(&strm_, Z_NO_FLUSH);\n\n      assert(ret != Z_STREAM_ERROR);\n      switch (ret) {\n      case Z_NEED_DICT:\n      case Z_DATA_ERROR:\n      case Z_MEM_ERROR: inflateEnd(&strm_); return false;\n      }\n\n      if (!callback(buff.data(), buff.size() - strm_.avail_out)) {\n        return false;\n      }\n    }\n\n    if (ret != Z_OK && ret != Z_STREAM_END) { return false; }\n\n  } while (data_length > 0);\n\n  return true;\n}\n#endif\n\n#ifdef CPPHTTPLIB_BROTLI_SUPPORT\ninline brotli_compressor::brotli_compressor() {\n  state_ = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr);\n}\n\ninline brotli_compressor::~brotli_compressor() {\n  BrotliEncoderDestroyInstance(state_);\n}\n\ninline bool brotli_compressor::compress(const char *data, size_t data_length,\n                                        bool last, Callback callback) {\n  std::array<uint8_t, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};\n\n  auto operation = last ? BROTLI_OPERATION_FINISH : BROTLI_OPERATION_PROCESS;\n  auto available_in = data_length;\n  auto next_in = reinterpret_cast<const uint8_t *>(data);\n\n  for (;;) {\n    if (last) {\n      if (BrotliEncoderIsFinished(state_)) { break; }\n    } else {\n      if (!available_in) { break; }\n    }\n\n    auto available_out = buff.size();\n    auto next_out = buff.data();\n\n    if (!BrotliEncoderCompressStream(state_, operation, &available_in, &next_in,\n                                     &available_out, &next_out, nullptr)) {\n      return false;\n    }\n\n    auto output_bytes = buff.size() - available_out;\n    if (output_bytes) {\n      callback(reinterpret_cast<const char *>(buff.data()), output_bytes);\n    }\n  }\n\n  return true;\n}\n\ninline brotli_decompressor::brotli_decompressor() {\n  decoder_s = BrotliDecoderCreateInstance(0, 0, 0);\n  decoder_r = decoder_s ? BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT\n                        : BROTLI_DECODER_RESULT_ERROR;\n}\n\ninline brotli_decompressor::~brotli_decompressor() {\n  if (decoder_s) { BrotliDecoderDestroyInstance(decoder_s); }\n}\n\ninline bool brotli_decompressor::is_valid() const { return decoder_s; }\n\ninline bool brotli_decompressor::decompress(const char *data,\n                                            size_t data_length,\n                                            Callback callback) {\n  if (decoder_r == BROTLI_DECODER_RESULT_SUCCESS ||\n      decoder_r == BROTLI_DECODER_RESULT_ERROR) {\n    return 0;\n  }\n\n  auto next_in = reinterpret_cast<const uint8_t *>(data);\n  size_t avail_in = data_length;\n  size_t total_out;\n\n  decoder_r = BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT;\n\n  std::array<char, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};\n  while (decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) {\n    char *next_out = buff.data();\n    size_t avail_out = buff.size();\n\n    decoder_r = BrotliDecoderDecompressStream(\n        decoder_s, &avail_in, &next_in, &avail_out,\n        reinterpret_cast<uint8_t **>(&next_out), &total_out);\n\n    if (decoder_r == BROTLI_DECODER_RESULT_ERROR) { return false; }\n\n    if (!callback(buff.data(), buff.size() - avail_out)) { return false; }\n  }\n\n  return decoder_r == BROTLI_DECODER_RESULT_SUCCESS ||\n         decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT;\n}\n#endif\n\n#ifdef CPPHTTPLIB_ZSTD_SUPPORT\ninline zstd_compressor::zstd_compressor() {\n  ctx_ = ZSTD_createCCtx();\n  ZSTD_CCtx_setParameter(ctx_, ZSTD_c_compressionLevel, ZSTD_fast);\n}\n\ninline zstd_compressor::~zstd_compressor() { ZSTD_freeCCtx(ctx_); }\n\ninline bool zstd_compressor::compress(const char *data, size_t data_length,\n                                      bool last, Callback callback) {\n  std::array<char, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};\n\n  ZSTD_EndDirective mode = last ? ZSTD_e_end : ZSTD_e_continue;\n  ZSTD_inBuffer input = {data, data_length, 0};\n\n  bool finished;\n  do {\n    ZSTD_outBuffer output = {buff.data(), CPPHTTPLIB_COMPRESSION_BUFSIZ, 0};\n    size_t const remaining = ZSTD_compressStream2(ctx_, &output, &input, mode);\n\n    if (ZSTD_isError(remaining)) { return false; }\n\n    if (!callback(buff.data(), output.pos)) { return false; }\n\n    finished = last ? (remaining == 0) : (input.pos == input.size);\n\n  } while (!finished);\n\n  return true;\n}\n\ninline zstd_decompressor::zstd_decompressor() { ctx_ = ZSTD_createDCtx(); }\n\ninline zstd_decompressor::~zstd_decompressor() { ZSTD_freeDCtx(ctx_); }\n\ninline bool zstd_decompressor::is_valid() const { return ctx_ != nullptr; }\n\ninline bool zstd_decompressor::decompress(const char *data, size_t data_length,\n                                          Callback callback) {\n  std::array<char, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};\n  ZSTD_inBuffer input = {data, data_length, 0};\n\n  while (input.pos < input.size) {\n    ZSTD_outBuffer output = {buff.data(), CPPHTTPLIB_COMPRESSION_BUFSIZ, 0};\n    size_t const remaining = ZSTD_decompressStream(ctx_, &output, &input);\n\n    if (ZSTD_isError(remaining)) { return false; }\n\n    if (!callback(buff.data(), output.pos)) { return false; }\n  }\n\n  return true;\n}\n#endif\n\ninline std::unique_ptr<decompressor>\ncreate_decompressor(const std::string &encoding) {\n  std::unique_ptr<decompressor> decompressor;\n\n  if (encoding == \"gzip\" || encoding == \"deflate\") {\n#ifdef CPPHTTPLIB_ZLIB_SUPPORT\n    decompressor = detail::make_unique<gzip_decompressor>();\n#endif\n  } else if (encoding.find(\"br\") != std::string::npos) {\n#ifdef CPPHTTPLIB_BROTLI_SUPPORT\n    decompressor = detail::make_unique<brotli_decompressor>();\n#endif\n  } else if (encoding == \"zstd\" || encoding.find(\"zstd\") != std::string::npos) {\n#ifdef CPPHTTPLIB_ZSTD_SUPPORT\n    decompressor = detail::make_unique<zstd_decompressor>();\n#endif\n  }\n\n  return decompressor;\n}\n\ninline bool is_prohibited_header_name(const std::string &name) {\n  using udl::operator\"\"_t;\n\n  switch (str2tag(name)) {\n  case \"REMOTE_ADDR\"_t:\n  case \"REMOTE_PORT\"_t:\n  case \"LOCAL_ADDR\"_t:\n  case \"LOCAL_PORT\"_t: return true;\n  default: return false;\n  }\n}\n\ninline bool has_header(const Headers &headers, const std::string &key) {\n  if (is_prohibited_header_name(key)) { return false; }\n  return headers.find(key) != headers.end();\n}\n\ninline const char *get_header_value(const Headers &headers,\n                                    const std::string &key, const char *def,\n                                    size_t id) {\n  if (is_prohibited_header_name(key)) {\n#ifndef CPPHTTPLIB_NO_EXCEPTIONS\n    std::string msg = \"Prohibited header name '\" + key + \"' is specified.\";\n    throw std::invalid_argument(msg);\n#else\n    return \"\";\n#endif\n  }\n\n  auto rng = headers.equal_range(key);\n  auto it = rng.first;\n  std::advance(it, static_cast<ssize_t>(id));\n  if (it != rng.second) { return it->second.c_str(); }\n  return def;\n}\n\ninline bool read_headers(Stream &strm, Headers &headers) {\n  const auto bufsiz = 2048;\n  char buf[bufsiz];\n  stream_line_reader line_reader(strm, buf, bufsiz);\n\n  size_t header_count = 0;\n\n  for (;;) {\n    if (!line_reader.getline()) { return false; }\n\n    // Check if the line ends with CRLF.\n    auto line_terminator_len = 2;\n    if (line_reader.end_with_crlf()) {\n      // Blank line indicates end of headers.\n      if (line_reader.size() == 2) { break; }\n    } else {\n#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR\n      // Blank line indicates end of headers.\n      if (line_reader.size() == 1) { break; }\n      line_terminator_len = 1;\n#else\n      continue; // Skip invalid line.\n#endif\n    }\n\n    if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }\n\n    // Check header count limit\n    if (header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; }\n\n    // Exclude line terminator\n    auto end = line_reader.ptr() + line_reader.size() - line_terminator_len;\n\n    if (!parse_header(line_reader.ptr(), end,\n                      [&](const std::string &key, const std::string &val) {\n                        headers.emplace(key, val);\n                      })) {\n      return false;\n    }\n\n    header_count++;\n  }\n\n  return true;\n}\n\nenum class ReadContentResult {\n  Success,         // Successfully read the content\n  PayloadTooLarge, // The content exceeds the specified payload limit\n  Error            // An error occurred while reading the content\n};\n\ninline ReadContentResult read_content_with_length(\n    Stream &strm, size_t len, DownloadProgress progress,\n    ContentReceiverWithProgress out,\n    size_t payload_max_length = (std::numeric_limits<size_t>::max)()) {\n  char buf[CPPHTTPLIB_RECV_BUFSIZ];\n\n  detail::BodyReader br;\n  br.stream = &strm;\n  br.has_content_length = true;\n  br.content_length = len;\n  br.payload_max_length = payload_max_length;\n  br.chunked = false;\n  br.bytes_read = 0;\n  br.last_error = Error::Success;\n\n  size_t r = 0;\n  while (r < len) {\n    auto read_len = static_cast<size_t>(len - r);\n    auto to_read = (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ);\n    auto n = detail::read_body_content(&strm, br, buf, to_read);\n    if (n <= 0) {\n      // Check if it was a payload size error\n      if (br.last_error == Error::ExceedMaxPayloadSize) {\n        return ReadContentResult::PayloadTooLarge;\n      }\n      return ReadContentResult::Error;\n    }\n\n    if (!out(buf, static_cast<size_t>(n), r, len)) {\n      return ReadContentResult::Error;\n    }\n    r += static_cast<size_t>(n);\n\n    if (progress) {\n      if (!progress(r, len)) { return ReadContentResult::Error; }\n    }\n  }\n\n  return ReadContentResult::Success;\n}\n\ninline ReadContentResult\nread_content_without_length(Stream &strm, size_t payload_max_length,\n                            ContentReceiverWithProgress out) {\n  char buf[CPPHTTPLIB_RECV_BUFSIZ];\n  size_t r = 0;\n  for (;;) {\n    auto n = strm.read(buf, CPPHTTPLIB_RECV_BUFSIZ);\n    if (n == 0) { return ReadContentResult::Success; }\n    if (n < 0) { return ReadContentResult::Error; }\n\n    // Check if adding this data would exceed the payload limit\n    if (r > payload_max_length ||\n        payload_max_length - r < static_cast<size_t>(n)) {\n      return ReadContentResult::PayloadTooLarge;\n    }\n\n    if (!out(buf, static_cast<size_t>(n), r, 0)) {\n      return ReadContentResult::Error;\n    }\n    r += static_cast<size_t>(n);\n  }\n\n  return ReadContentResult::Success;\n}\n\ntemplate <typename T>\ninline ReadContentResult read_content_chunked(Stream &strm, T &x,\n                                              size_t payload_max_length,\n                                              ContentReceiverWithProgress out) {\n  detail::ChunkedDecoder dec(strm);\n\n  char buf[CPPHTTPLIB_RECV_BUFSIZ];\n  size_t total_len = 0;\n\n  for (;;) {\n    size_t chunk_offset = 0;\n    size_t chunk_total = 0;\n    auto n = dec.read_payload(buf, sizeof(buf), chunk_offset, chunk_total);\n    if (n < 0) { return ReadContentResult::Error; }\n\n    if (n == 0) {\n      if (!dec.parse_trailers_into(x.trailers, x.headers)) {\n        return ReadContentResult::Error;\n      }\n      return ReadContentResult::Success;\n    }\n\n    if (total_len > payload_max_length ||\n        payload_max_length - total_len < static_cast<size_t>(n)) {\n      return ReadContentResult::PayloadTooLarge;\n    }\n\n    if (!out(buf, static_cast<size_t>(n), chunk_offset, chunk_total)) {\n      return ReadContentResult::Error;\n    }\n\n    total_len += static_cast<size_t>(n);\n  }\n}\n\ninline bool is_chunked_transfer_encoding(const Headers &headers) {\n  return case_ignore::equal(\n      get_header_value(headers, \"Transfer-Encoding\", \"\", 0), \"chunked\");\n}\n\ntemplate <typename T, typename U>\nbool prepare_content_receiver(T &x, int &status,\n                              ContentReceiverWithProgress receiver,\n                              bool decompress, U callback) {\n  if (decompress) {\n    std::string encoding = x.get_header_value(\"Content-Encoding\");\n    std::unique_ptr<decompressor> decompressor;\n\n    if (!encoding.empty()) {\n      decompressor = detail::create_decompressor(encoding);\n      if (!decompressor) {\n        // Unsupported encoding or no support compiled in\n        status = StatusCode::UnsupportedMediaType_415;\n        return false;\n      }\n    }\n\n    if (decompressor) {\n      if (decompressor->is_valid()) {\n        ContentReceiverWithProgress out = [&](const char *buf, size_t n,\n                                              size_t off, size_t len) {\n          return decompressor->decompress(buf, n,\n                                          [&](const char *buf2, size_t n2) {\n                                            return receiver(buf2, n2, off, len);\n                                          });\n        };\n        return callback(std::move(out));\n      } else {\n        status = StatusCode::InternalServerError_500;\n        return false;\n      }\n    }\n  }\n\n  ContentReceiverWithProgress out = [&](const char *buf, size_t n, size_t off,\n                                        size_t len) {\n    return receiver(buf, n, off, len);\n  };\n  return callback(std::move(out));\n}\n\ntemplate <typename T>\nbool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,\n                  DownloadProgress progress,\n                  ContentReceiverWithProgress receiver, bool decompress) {\n  return prepare_content_receiver(\n      x, status, std::move(receiver), decompress,\n      [&](const ContentReceiverWithProgress &out) {\n        auto ret = true;\n        auto exceed_payload_max_length = false;\n\n        if (is_chunked_transfer_encoding(x.headers)) {\n          auto result = read_content_chunked(strm, x, payload_max_length, out);\n          if (result == ReadContentResult::Success) {\n            ret = true;\n          } else if (result == ReadContentResult::PayloadTooLarge) {\n            exceed_payload_max_length = true;\n            ret = false;\n          } else {\n            ret = false;\n          }\n        } else if (!has_header(x.headers, \"Content-Length\")) {\n          auto result =\n              read_content_without_length(strm, payload_max_length, out);\n          if (result == ReadContentResult::Success) {\n            ret = true;\n          } else if (result == ReadContentResult::PayloadTooLarge) {\n            exceed_payload_max_length = true;\n            ret = false;\n          } else {\n            ret = false;\n          }\n        } else {\n          auto is_invalid_value = false;\n          auto len = get_header_value_u64(x.headers, \"Content-Length\",\n                                          (std::numeric_limits<size_t>::max)(),\n                                          0, is_invalid_value);\n\n          if (is_invalid_value) {\n            ret = false;\n          } else if (len > 0) {\n            auto result = read_content_with_length(\n                strm, len, std::move(progress), out, payload_max_length);\n            ret = (result == ReadContentResult::Success);\n            if (result == ReadContentResult::PayloadTooLarge) {\n              exceed_payload_max_length = true;\n            }\n          }\n        }\n\n        if (!ret) {\n          status = exceed_payload_max_length ? StatusCode::PayloadTooLarge_413\n                                             : StatusCode::BadRequest_400;\n        }\n        return ret;\n      });\n}\n\ninline ssize_t write_request_line(Stream &strm, const std::string &method,\n                                  const std::string &path) {\n  std::string s = method;\n  s += ' ';\n  s += path;\n  s += \" HTTP/1.1\\r\\n\";\n  return strm.write(s.data(), s.size());\n}\n\ninline ssize_t write_response_line(Stream &strm, int status) {\n  std::string s = \"HTTP/1.1 \";\n  s += std::to_string(status);\n  s += ' ';\n  s += httplib::status_message(status);\n  s += \"\\r\\n\";\n  return strm.write(s.data(), s.size());\n}\n\ninline ssize_t write_headers(Stream &strm, const Headers &headers) {\n  ssize_t write_len = 0;\n  for (const auto &x : headers) {\n    std::string s;\n    s = x.first;\n    s += \": \";\n    s += x.second;\n    s += \"\\r\\n\";\n\n    auto len = strm.write(s.data(), s.size());\n    if (len < 0) { return len; }\n    write_len += len;\n  }\n  auto len = strm.write(\"\\r\\n\");\n  if (len < 0) { return len; }\n  write_len += len;\n  return write_len;\n}\n\ninline bool write_data(Stream &strm, const char *d, size_t l) {\n  size_t offset = 0;\n  while (offset < l) {\n    auto length = strm.write(d + offset, l - offset);\n    if (length < 0) { return false; }\n    offset += static_cast<size_t>(length);\n  }\n  return true;\n}\n\ntemplate <typename T>\ninline bool write_content_with_progress(Stream &strm,\n                                        const ContentProvider &content_provider,\n                                        size_t offset, size_t length,\n                                        T is_shutting_down,\n                                        const UploadProgress &upload_progress,\n                                        Error &error) {\n  size_t end_offset = offset + length;\n  size_t start_offset = offset;\n  auto ok = true;\n  DataSink data_sink;\n\n  data_sink.write = [&](const char *d, size_t l) -> bool {\n    if (ok) {\n      if (write_data(strm, d, l)) {\n        offset += l;\n\n        if (upload_progress && length > 0) {\n          size_t current_written = offset - start_offset;\n          if (!upload_progress(current_written, length)) {\n            ok = false;\n            return false;\n          }\n        }\n      } else {\n        ok = false;\n      }\n    }\n    return ok;\n  };\n\n  data_sink.is_writable = [&]() -> bool { return strm.wait_writable(); };\n\n  while (offset < end_offset && !is_shutting_down()) {\n    if (!strm.wait_writable()) {\n      error = Error::Write;\n      return false;\n    } else if (!content_provider(offset, end_offset - offset, data_sink)) {\n      error = Error::Canceled;\n      return false;\n    } else if (!ok) {\n      error = Error::Write;\n      return false;\n    }\n  }\n\n  error = Error::Success;\n  return true;\n}\n\ntemplate <typename T>\ninline bool write_content(Stream &strm, const ContentProvider &content_provider,\n                          size_t offset, size_t length, T is_shutting_down,\n                          Error &error) {\n  return write_content_with_progress<T>(strm, content_provider, offset, length,\n                                        is_shutting_down, nullptr, error);\n}\n\ntemplate <typename T>\ninline bool write_content(Stream &strm, const ContentProvider &content_provider,\n                          size_t offset, size_t length,\n                          const T &is_shutting_down) {\n  auto error = Error::Success;\n  return write_content(strm, content_provider, offset, length, is_shutting_down,\n                       error);\n}\n\ntemplate <typename T>\ninline bool\nwrite_content_without_length(Stream &strm,\n                             const ContentProvider &content_provider,\n                             const T &is_shutting_down) {\n  size_t offset = 0;\n  auto data_available = true;\n  auto ok = true;\n  DataSink data_sink;\n\n  data_sink.write = [&](const char *d, size_t l) -> bool {\n    if (ok) {\n      offset += l;\n      if (!write_data(strm, d, l)) { ok = false; }\n    }\n    return ok;\n  };\n\n  data_sink.is_writable = [&]() -> bool { return strm.wait_writable(); };\n\n  data_sink.done = [&](void) { data_available = false; };\n\n  while (data_available && !is_shutting_down()) {\n    if (!strm.wait_writable()) {\n      return false;\n    } else if (!content_provider(offset, 0, data_sink)) {\n      return false;\n    } else if (!ok) {\n      return false;\n    }\n  }\n  return true;\n}\n\ntemplate <typename T, typename U>\ninline bool\nwrite_content_chunked(Stream &strm, const ContentProvider &content_provider,\n                      const T &is_shutting_down, U &compressor, Error &error) {\n  size_t offset = 0;\n  auto data_available = true;\n  auto ok = true;\n  DataSink data_sink;\n\n  data_sink.write = [&](const char *d, size_t l) -> bool {\n    if (ok) {\n      data_available = l > 0;\n      offset += l;\n\n      std::string payload;\n      if (compressor.compress(d, l, false,\n                              [&](const char *data, size_t data_len) {\n                                payload.append(data, data_len);\n                                return true;\n                              })) {\n        if (!payload.empty()) {\n          // Emit chunked response header and footer for each chunk\n          auto chunk =\n              from_i_to_hex(payload.size()) + \"\\r\\n\" + payload + \"\\r\\n\";\n          if (!write_data(strm, chunk.data(), chunk.size())) { ok = false; }\n        }\n      } else {\n        ok = false;\n      }\n    }\n    return ok;\n  };\n\n  data_sink.is_writable = [&]() -> bool { return strm.wait_writable(); };\n\n  auto done_with_trailer = [&](const Headers *trailer) {\n    if (!ok) { return; }\n\n    data_available = false;\n\n    std::string payload;\n    if (!compressor.compress(nullptr, 0, true,\n                             [&](const char *data, size_t data_len) {\n                               payload.append(data, data_len);\n                               return true;\n                             })) {\n      ok = false;\n      return;\n    }\n\n    if (!payload.empty()) {\n      // Emit chunked response header and footer for each chunk\n      auto chunk = from_i_to_hex(payload.size()) + \"\\r\\n\" + payload + \"\\r\\n\";\n      if (!write_data(strm, chunk.data(), chunk.size())) {\n        ok = false;\n        return;\n      }\n    }\n\n    constexpr const char done_marker[] = \"0\\r\\n\";\n    if (!write_data(strm, done_marker, str_len(done_marker))) { ok = false; }\n\n    // Trailer\n    if (trailer) {\n      for (const auto &kv : *trailer) {\n        std::string field_line = kv.first + \": \" + kv.second + \"\\r\\n\";\n        if (!write_data(strm, field_line.data(), field_line.size())) {\n          ok = false;\n        }\n      }\n    }\n\n    constexpr const char crlf[] = \"\\r\\n\";\n    if (!write_data(strm, crlf, str_len(crlf))) { ok = false; }\n  };\n\n  data_sink.done = [&](void) { done_with_trailer(nullptr); };\n\n  data_sink.done_with_trailer = [&](const Headers &trailer) {\n    done_with_trailer(&trailer);\n  };\n\n  while (data_available && !is_shutting_down()) {\n    if (!strm.wait_writable()) {\n      error = Error::Write;\n      return false;\n    } else if (!content_provider(offset, 0, data_sink)) {\n      error = Error::Canceled;\n      return false;\n    } else if (!ok) {\n      error = Error::Write;\n      return false;\n    }\n  }\n\n  error = Error::Success;\n  return true;\n}\n\ntemplate <typename T, typename U>\ninline bool write_content_chunked(Stream &strm,\n                                  const ContentProvider &content_provider,\n                                  const T &is_shutting_down, U &compressor) {\n  auto error = Error::Success;\n  return write_content_chunked(strm, content_provider, is_shutting_down,\n                               compressor, error);\n}\n\ntemplate <typename T>\ninline bool redirect(T &cli, Request &req, Response &res,\n                     const std::string &path, const std::string &location,\n                     Error &error) {\n  Request new_req = req;\n  new_req.path = path;\n  new_req.redirect_count_ -= 1;\n\n  if (res.status == StatusCode::SeeOther_303 &&\n      (req.method != \"GET\" && req.method != \"HEAD\")) {\n    new_req.method = \"GET\";\n    new_req.body.clear();\n    new_req.headers.clear();\n  }\n\n  Response new_res;\n\n  auto ret = cli.send(new_req, new_res, error);\n  if (ret) {\n    req = std::move(new_req);\n    res = std::move(new_res);\n\n    if (res.location.empty()) { res.location = location; }\n  }\n  return ret;\n}\n\ninline std::string params_to_query_str(const Params &params) {\n  std::string query;\n\n  for (auto it = params.begin(); it != params.end(); ++it) {\n    if (it != params.begin()) { query += '&'; }\n    query += encode_query_component(it->first);\n    query += '=';\n    query += encode_query_component(it->second);\n  }\n  return query;\n}\n\ninline void parse_query_text(const char *data, std::size_t size,\n                             Params &params) {\n  std::set<std::string> cache;\n  split(data, data + size, '&', [&](const char *b, const char *e) {\n    std::string kv(b, e);\n    if (cache.find(kv) != cache.end()) { return; }\n    cache.insert(std::move(kv));\n\n    std::string key;\n    std::string val;\n    divide(b, static_cast<std::size_t>(e - b), '=',\n           [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data,\n               std::size_t rhs_size) {\n             key.assign(lhs_data, lhs_size);\n             val.assign(rhs_data, rhs_size);\n           });\n\n    if (!key.empty()) {\n      params.emplace(decode_query_component(key), decode_query_component(val));\n    }\n  });\n}\n\ninline void parse_query_text(const std::string &s, Params &params) {\n  parse_query_text(s.data(), s.size(), params);\n}\n\n// Normalize a query string by decoding and re-encoding each key/value pair\n// while preserving the original parameter order. This avoids double-encoding\n// and ensures consistent encoding without reordering (unlike Params which\n// uses std::multimap and sorts keys).\ninline std::string normalize_query_string(const std::string &query) {\n  std::string result;\n  split(query.data(), query.data() + query.size(), '&',\n        [&](const char *b, const char *e) {\n          std::string key;\n          std::string val;\n          divide(b, static_cast<std::size_t>(e - b), '=',\n                 [&](const char *lhs_data, std::size_t lhs_size,\n                     const char *rhs_data, std::size_t rhs_size) {\n                   key.assign(lhs_data, lhs_size);\n                   val.assign(rhs_data, rhs_size);\n                 });\n\n          if (!key.empty()) {\n            auto dec_key = decode_query_component(key);\n            auto dec_val = decode_query_component(val);\n\n            if (!result.empty()) { result += '&'; }\n            result += encode_query_component(dec_key);\n            if (!val.empty() || std::find(b, e, '=') != e) {\n              result += '=';\n              result += encode_query_component(dec_val);\n            }\n          }\n        });\n  return result;\n}\n\ninline bool parse_multipart_boundary(const std::string &content_type,\n                                     std::string &boundary) {\n  auto boundary_keyword = \"boundary=\";\n  auto pos = content_type.find(boundary_keyword);\n  if (pos == std::string::npos) { return false; }\n  auto end = content_type.find(';', pos);\n  auto beg = pos + strlen(boundary_keyword);\n  boundary = trim_double_quotes_copy(content_type.substr(beg, end - beg));\n  return !boundary.empty();\n}\n\ninline void parse_disposition_params(const std::string &s, Params &params) {\n  std::set<std::string> cache;\n  split(s.data(), s.data() + s.size(), ';', [&](const char *b, const char *e) {\n    std::string kv(b, e);\n    if (cache.find(kv) != cache.end()) { return; }\n    cache.insert(kv);\n\n    std::string key;\n    std::string val;\n    split(b, e, '=', [&](const char *b2, const char *e2) {\n      if (key.empty()) {\n        key.assign(b2, e2);\n      } else {\n        val.assign(b2, e2);\n      }\n    });\n\n    if (!key.empty()) {\n      params.emplace(trim_double_quotes_copy((key)),\n                     trim_double_quotes_copy((val)));\n    }\n  });\n}\n\n#ifdef CPPHTTPLIB_NO_EXCEPTIONS\ninline bool parse_range_header(const std::string &s, Ranges &ranges) {\n#else\ninline bool parse_range_header(const std::string &s, Ranges &ranges) try {\n#endif\n  auto is_valid = [](const std::string &str) {\n    return std::all_of(str.cbegin(), str.cend(),\n                       [](unsigned char c) { return std::isdigit(c); });\n  };\n\n  if (s.size() > 7 && s.compare(0, 6, \"bytes=\") == 0) {\n    const auto pos = static_cast<size_t>(6);\n    const auto len = static_cast<size_t>(s.size() - 6);\n    auto all_valid_ranges = true;\n    split(&s[pos], &s[pos + len], ',', [&](const char *b, const char *e) {\n      if (!all_valid_ranges) { return; }\n\n      const auto it = std::find(b, e, '-');\n      if (it == e) {\n        all_valid_ranges = false;\n        return;\n      }\n\n      const auto lhs = std::string(b, it);\n      const auto rhs = std::string(it + 1, e);\n      if (!is_valid(lhs) || !is_valid(rhs)) {\n        all_valid_ranges = false;\n        return;\n      }\n\n      ssize_t first = -1;\n      if (!lhs.empty()) {\n        ssize_t v;\n        auto res = detail::from_chars(lhs.data(), lhs.data() + lhs.size(), v);\n        if (res.ec == std::errc{}) { first = v; }\n      }\n\n      ssize_t last = -1;\n      if (!rhs.empty()) {\n        ssize_t v;\n        auto res = detail::from_chars(rhs.data(), rhs.data() + rhs.size(), v);\n        if (res.ec == std::errc{}) { last = v; }\n      }\n\n      if ((first == -1 && last == -1) ||\n          (first != -1 && last != -1 && first > last)) {\n        all_valid_ranges = false;\n        return;\n      }\n\n      ranges.emplace_back(first, last);\n    });\n    return all_valid_ranges && !ranges.empty();\n  }\n  return false;\n#ifdef CPPHTTPLIB_NO_EXCEPTIONS\n}\n#else\n} catch (...) { return false; }\n#endif\n\ninline bool parse_accept_header(const std::string &s,\n                                std::vector<std::string> &content_types) {\n  content_types.clear();\n\n  // Empty string is considered valid (no preference)\n  if (s.empty()) { return true; }\n\n  // Check for invalid patterns: leading/trailing commas or consecutive commas\n  if (s.front() == ',' || s.back() == ',' ||\n      s.find(\",,\") != std::string::npos) {\n    return false;\n  }\n\n  struct AcceptEntry {\n    std::string media_type;\n    double quality;\n    int order; // Original order in header\n  };\n\n  std::vector<AcceptEntry> entries;\n  int order = 0;\n  bool has_invalid_entry = false;\n\n  // Split by comma and parse each entry\n  split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) {\n    std::string entry(b, e);\n    entry = trim_copy(entry);\n\n    if (entry.empty()) {\n      has_invalid_entry = true;\n      return;\n    }\n\n    AcceptEntry accept_entry;\n    accept_entry.quality = 1.0; // Default quality\n    accept_entry.order = order++;\n\n    // Find q= parameter\n    auto q_pos = entry.find(\";q=\");\n    if (q_pos == std::string::npos) { q_pos = entry.find(\"; q=\"); }\n\n    if (q_pos != std::string::npos) {\n      // Extract media type (before q parameter)\n      accept_entry.media_type = trim_copy(entry.substr(0, q_pos));\n\n      // Extract quality value\n      auto q_start = entry.find('=', q_pos) + 1;\n      auto q_end = entry.find(';', q_start);\n      if (q_end == std::string::npos) { q_end = entry.length(); }\n\n      std::string quality_str =\n          trim_copy(entry.substr(q_start, q_end - q_start));\n      if (quality_str.empty()) {\n        has_invalid_entry = true;\n        return;\n      }\n\n      {\n        double v = 0.0;\n        auto res = detail::from_chars(\n            quality_str.data(), quality_str.data() + quality_str.size(), v);\n        if (res.ec == std::errc{}) {\n          accept_entry.quality = v;\n        } else {\n          has_invalid_entry = true;\n          return;\n        }\n      }\n      // Check if quality is in valid range [0.0, 1.0]\n      if (accept_entry.quality < 0.0 || accept_entry.quality > 1.0) {\n        has_invalid_entry = true;\n        return;\n      }\n    } else {\n      // No quality parameter, use entire entry as media type\n      accept_entry.media_type = entry;\n    }\n\n    // Remove additional parameters from media type\n    auto param_pos = accept_entry.media_type.find(';');\n    if (param_pos != std::string::npos) {\n      accept_entry.media_type =\n          trim_copy(accept_entry.media_type.substr(0, param_pos));\n    }\n\n    // Basic validation of media type format\n    if (accept_entry.media_type.empty()) {\n      has_invalid_entry = true;\n      return;\n    }\n\n    // Check for basic media type format (should contain '/' or be '*')\n    if (accept_entry.media_type != \"*\" &&\n        accept_entry.media_type.find('/') == std::string::npos) {\n      has_invalid_entry = true;\n      return;\n    }\n\n    entries.push_back(std::move(accept_entry));\n  });\n\n  // Return false if any invalid entry was found\n  if (has_invalid_entry) { return false; }\n\n  // Sort by quality (descending), then by original order (ascending)\n  std::sort(entries.begin(), entries.end(),\n            [](const AcceptEntry &a, const AcceptEntry &b) {\n              if (a.quality != b.quality) {\n                return a.quality > b.quality; // Higher quality first\n              }\n              return a.order < b.order; // Earlier order first for same quality\n            });\n\n  // Extract sorted media types\n  content_types.reserve(entries.size());\n  for (auto &entry : entries) {\n    content_types.push_back(std::move(entry.media_type));\n  }\n\n  return true;\n}\n\nclass FormDataParser {\npublic:\n  FormDataParser() = default;\n\n  void set_boundary(std::string &&boundary) {\n    boundary_ = std::move(boundary);\n    dash_boundary_crlf_ = dash_ + boundary_ + crlf_;\n    crlf_dash_boundary_ = crlf_ + dash_ + boundary_;\n  }\n\n  bool is_valid() const { return is_valid_; }\n\n  bool parse(const char *buf, size_t n, const FormDataHeader &header_callback,\n             const ContentReceiver &content_callback) {\n\n    buf_append(buf, n);\n\n    while (buf_size() > 0) {\n      switch (state_) {\n      case 0: { // Initial boundary\n        auto pos = buf_find(dash_boundary_crlf_);\n        if (pos == buf_size()) { return true; }\n        buf_erase(pos + dash_boundary_crlf_.size());\n        state_ = 1;\n        break;\n      }\n      case 1: { // New entry\n        clear_file_info();\n        state_ = 2;\n        break;\n      }\n      case 2: { // Headers\n        auto pos = buf_find(crlf_);\n        if (pos > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }\n        while (pos < buf_size()) {\n          // Empty line\n          if (pos == 0) {\n            if (!header_callback(file_)) {\n              is_valid_ = false;\n              return false;\n            }\n            buf_erase(crlf_.size());\n            state_ = 3;\n            break;\n          }\n\n          const auto header = buf_head(pos);\n\n          if (!parse_header(header.data(), header.data() + header.size(),\n                            [&](const std::string &, const std::string &) {})) {\n            is_valid_ = false;\n            return false;\n          }\n\n          // Parse and emplace space trimmed headers into a map\n          if (!parse_header(\n                  header.data(), header.data() + header.size(),\n                  [&](const std::string &key, const std::string &val) {\n                    file_.headers.emplace(key, val);\n                  })) {\n            is_valid_ = false;\n            return false;\n          }\n\n          constexpr const char header_content_type[] = \"Content-Type:\";\n\n          if (start_with_case_ignore(header, header_content_type)) {\n            file_.content_type =\n                trim_copy(header.substr(str_len(header_content_type)));\n          } else {\n            thread_local const std::regex re_content_disposition(\n                R\"~(^Content-Disposition:\\s*form-data;\\s*(.*)$)~\",\n                std::regex_constants::icase);\n\n            std::smatch m;\n            if (std::regex_match(header, m, re_content_disposition)) {\n              Params params;\n              parse_disposition_params(m[1], params);\n\n              auto it = params.find(\"name\");\n              if (it != params.end()) {\n                file_.name = it->second;\n              } else {\n                is_valid_ = false;\n                return false;\n              }\n\n              it = params.find(\"filename\");\n              if (it != params.end()) { file_.filename = it->second; }\n\n              it = params.find(\"filename*\");\n              if (it != params.end()) {\n                // Only allow UTF-8 encoding...\n                thread_local const std::regex re_rfc5987_encoding(\n                    R\"~(^UTF-8''(.+?)$)~\", std::regex_constants::icase);\n\n                std::smatch m2;\n                if (std::regex_match(it->second, m2, re_rfc5987_encoding)) {\n                  file_.filename = decode_path_component(m2[1]); // override...\n                } else {\n                  is_valid_ = false;\n                  return false;\n                }\n              }\n            }\n          }\n          buf_erase(pos + crlf_.size());\n          pos = buf_find(crlf_);\n        }\n        if (state_ != 3) { return true; }\n        break;\n      }\n      case 3: { // Body\n        if (crlf_dash_boundary_.size() > buf_size()) { return true; }\n        auto pos = buf_find(crlf_dash_boundary_);\n        if (pos < buf_size()) {\n          if (!content_callback(buf_data(), pos)) {\n            is_valid_ = false;\n            return false;\n          }\n          buf_erase(pos + crlf_dash_boundary_.size());\n          state_ = 4;\n        } else {\n          auto len = buf_size() - crlf_dash_boundary_.size();\n          if (len > 0) {\n            if (!content_callback(buf_data(), len)) {\n              is_valid_ = false;\n              return false;\n            }\n            buf_erase(len);\n          }\n          return true;\n        }\n        break;\n      }\n      case 4: { // Boundary\n        if (crlf_.size() > buf_size()) { return true; }\n        if (buf_start_with(crlf_)) {\n          buf_erase(crlf_.size());\n          state_ = 1;\n        } else {\n          if (dash_.size() > buf_size()) { return true; }\n          if (buf_start_with(dash_)) {\n            buf_erase(dash_.size());\n            is_valid_ = true;\n            buf_erase(buf_size()); // Remove epilogue\n          } else {\n            return true;\n          }\n        }\n        break;\n      }\n      }\n    }\n\n    return true;\n  }\n\nprivate:\n  void clear_file_info() {\n    file_.name.clear();\n    file_.filename.clear();\n    file_.content_type.clear();\n    file_.headers.clear();\n  }\n\n  bool start_with_case_ignore(const std::string &a, const char *b) const {\n    const auto b_len = strlen(b);\n    if (a.size() < b_len) { return false; }\n    for (size_t i = 0; i < b_len; i++) {\n      if (case_ignore::to_lower(a[i]) != case_ignore::to_lower(b[i])) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  const std::string dash_ = \"--\";\n  const std::string crlf_ = \"\\r\\n\";\n  std::string boundary_;\n  std::string dash_boundary_crlf_;\n  std::string crlf_dash_boundary_;\n\n  size_t state_ = 0;\n  bool is_valid_ = false;\n  FormData file_;\n\n  // Buffer\n  bool start_with(const std::string &a, size_t spos, size_t epos,\n                  const std::string &b) const {\n    if (epos - spos < b.size()) { return false; }\n    for (size_t i = 0; i < b.size(); i++) {\n      if (a[i + spos] != b[i]) { return false; }\n    }\n    return true;\n  }\n\n  size_t buf_size() const { return buf_epos_ - buf_spos_; }\n\n  const char *buf_data() const { return &buf_[buf_spos_]; }\n\n  std::string buf_head(size_t l) const { return buf_.substr(buf_spos_, l); }\n\n  bool buf_start_with(const std::string &s) const {\n    return start_with(buf_, buf_spos_, buf_epos_, s);\n  }\n\n  size_t buf_find(const std::string &s) const {\n    auto c = s.front();\n\n    size_t off = buf_spos_;\n    while (off < buf_epos_) {\n      auto pos = off;\n      while (true) {\n        if (pos == buf_epos_) { return buf_size(); }\n        if (buf_[pos] == c) { break; }\n        pos++;\n      }\n\n      auto remaining_size = buf_epos_ - pos;\n      if (s.size() > remaining_size) { return buf_size(); }\n\n      if (start_with(buf_, pos, buf_epos_, s)) { return pos - buf_spos_; }\n\n      off = pos + 1;\n    }\n\n    return buf_size();\n  }\n\n  void buf_append(const char *data, size_t n) {\n    auto remaining_size = buf_size();\n    if (remaining_size > 0 && buf_spos_ > 0) {\n      for (size_t i = 0; i < remaining_size; i++) {\n        buf_[i] = buf_[buf_spos_ + i];\n      }\n    }\n    buf_spos_ = 0;\n    buf_epos_ = remaining_size;\n\n    if (remaining_size + n > buf_.size()) { buf_.resize(remaining_size + n); }\n\n    for (size_t i = 0; i < n; i++) {\n      buf_[buf_epos_ + i] = data[i];\n    }\n    buf_epos_ += n;\n  }\n\n  void buf_erase(size_t size) { buf_spos_ += size; }\n\n  std::string buf_;\n  size_t buf_spos_ = 0;\n  size_t buf_epos_ = 0;\n};\n\ninline std::string random_string(size_t length) {\n  constexpr const char data[] =\n      \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n\n  thread_local auto engine([]() {\n    // std::random_device might actually be deterministic on some\n    // platforms, but due to lack of support in the c++ standard library,\n    // doing better requires either some ugly hacks or breaking portability.\n    std::random_device seed_gen;\n    // Request 128 bits of entropy for initialization\n    std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(), seed_gen()};\n    return std::mt19937(seed_sequence);\n  }());\n\n  std::string result;\n  for (size_t i = 0; i < length; i++) {\n    result += data[engine() % (sizeof(data) - 1)];\n  }\n  return result;\n}\n\ninline std::string make_multipart_data_boundary() {\n  return \"--cpp-httplib-multipart-data-\" + detail::random_string(16);\n}\n\ninline bool is_multipart_boundary_chars_valid(const std::string &boundary) {\n  auto valid = true;\n  for (size_t i = 0; i < boundary.size(); i++) {\n    auto c = boundary[i];\n    if (!std::isalnum(c) && c != '-' && c != '_') {\n      valid = false;\n      break;\n    }\n  }\n  return valid;\n}\n\ntemplate <typename T>\ninline std::string\nserialize_multipart_formdata_item_begin(const T &item,\n                                        const std::string &boundary) {\n  std::string body = \"--\" + boundary + \"\\r\\n\";\n  body += \"Content-Disposition: form-data; name=\\\"\" + item.name + \"\\\"\";\n  if (!item.filename.empty()) {\n    body += \"; filename=\\\"\" + item.filename + \"\\\"\";\n  }\n  body += \"\\r\\n\";\n  if (!item.content_type.empty()) {\n    body += \"Content-Type: \" + item.content_type + \"\\r\\n\";\n  }\n  body += \"\\r\\n\";\n\n  return body;\n}\n\ninline std::string serialize_multipart_formdata_item_end() { return \"\\r\\n\"; }\n\ninline std::string\nserialize_multipart_formdata_finish(const std::string &boundary) {\n  return \"--\" + boundary + \"--\\r\\n\";\n}\n\ninline std::string\nserialize_multipart_formdata_get_content_type(const std::string &boundary) {\n  return \"multipart/form-data; boundary=\" + boundary;\n}\n\ninline std::string\nserialize_multipart_formdata(const UploadFormDataItems &items,\n                             const std::string &boundary, bool finish = true) {\n  std::string body;\n\n  for (const auto &item : items) {\n    body += serialize_multipart_formdata_item_begin(item, boundary);\n    body += item.content + serialize_multipart_formdata_item_end();\n  }\n\n  if (finish) { body += serialize_multipart_formdata_finish(boundary); }\n\n  return body;\n}\n\ninline void coalesce_ranges(Ranges &ranges, size_t content_length) {\n  if (ranges.size() <= 1) return;\n\n  // Sort ranges by start position\n  std::sort(ranges.begin(), ranges.end(),\n            [](const Range &a, const Range &b) { return a.first < b.first; });\n\n  Ranges coalesced;\n  coalesced.reserve(ranges.size());\n\n  for (auto &r : ranges) {\n    auto first_pos = r.first;\n    auto last_pos = r.second;\n\n    // Handle special cases like in range_error\n    if (first_pos == -1 && last_pos == -1) {\n      first_pos = 0;\n      last_pos = static_cast<ssize_t>(content_length);\n    }\n\n    if (first_pos == -1) {\n      first_pos = static_cast<ssize_t>(content_length) - last_pos;\n      last_pos = static_cast<ssize_t>(content_length) - 1;\n    }\n\n    if (last_pos == -1 || last_pos >= static_cast<ssize_t>(content_length)) {\n      last_pos = static_cast<ssize_t>(content_length) - 1;\n    }\n\n    // Skip invalid ranges\n    if (!(0 <= first_pos && first_pos <= last_pos &&\n          last_pos < static_cast<ssize_t>(content_length))) {\n      continue;\n    }\n\n    // Coalesce with previous range if overlapping or adjacent (but not\n    // identical)\n    if (!coalesced.empty()) {\n      auto &prev = coalesced.back();\n      // Check if current range overlaps or is adjacent to previous range\n      // but don't coalesce identical ranges (allow duplicates)\n      if (first_pos <= prev.second + 1 &&\n          !(first_pos == prev.first && last_pos == prev.second)) {\n        // Extend the previous range\n        prev.second = (std::max)(prev.second, last_pos);\n        continue;\n      }\n    }\n\n    // Add new range\n    coalesced.emplace_back(first_pos, last_pos);\n  }\n\n  ranges = std::move(coalesced);\n}\n\ninline bool range_error(Request &req, Response &res) {\n  if (!req.ranges.empty() && 200 <= res.status && res.status < 300) {\n    ssize_t content_len = static_cast<ssize_t>(\n        res.content_length_ ? res.content_length_ : res.body.size());\n\n    std::vector<std::pair<ssize_t, ssize_t>> processed_ranges;\n    size_t overwrapping_count = 0;\n\n    // NOTE: The following Range check is based on '14.2. Range' in RFC 9110\n    // 'HTTP Semantics' to avoid potential denial-of-service attacks.\n    // https://www.rfc-editor.org/rfc/rfc9110#section-14.2\n\n    // Too many ranges\n    if (req.ranges.size() > CPPHTTPLIB_RANGE_MAX_COUNT) { return true; }\n\n    for (auto &r : req.ranges) {\n      auto &first_pos = r.first;\n      auto &last_pos = r.second;\n\n      if (first_pos == -1 && last_pos == -1) {\n        first_pos = 0;\n        last_pos = content_len;\n      }\n\n      if (first_pos == -1) {\n        first_pos = content_len - last_pos;\n        last_pos = content_len - 1;\n      }\n\n      // NOTE: RFC-9110 '14.1.2. Byte Ranges':\n      // A client can limit the number of bytes requested without knowing the\n      // size of the selected representation. If the last-pos value is absent,\n      // or if the value is greater than or equal to the current length of the\n      // representation data, the byte range is interpreted as the remainder of\n      // the representation (i.e., the server replaces the value of last-pos\n      // with a value that is one less than the current length of the selected\n      // representation).\n      // https://www.rfc-editor.org/rfc/rfc9110.html#section-14.1.2-6\n      if (last_pos == -1 || last_pos >= content_len) {\n        last_pos = content_len - 1;\n      }\n\n      // Range must be within content length\n      if (!(0 <= first_pos && first_pos <= last_pos &&\n            last_pos <= content_len - 1)) {\n        return true;\n      }\n\n      // Request must not have more than two overlapping ranges\n      for (const auto &processed_range : processed_ranges) {\n        if (!(last_pos < processed_range.first ||\n              first_pos > processed_range.second)) {\n          overwrapping_count++;\n          if (overwrapping_count > 2) { return true; }\n          break; // Only count once per range\n        }\n      }\n\n      processed_ranges.emplace_back(first_pos, last_pos);\n    }\n\n    // After validation, coalesce overlapping ranges as per RFC 9110\n    coalesce_ranges(req.ranges, static_cast<size_t>(content_len));\n  }\n\n  return false;\n}\n\ninline std::pair<size_t, size_t>\nget_range_offset_and_length(Range r, size_t content_length) {\n  assert(r.first != -1 && r.second != -1);\n  assert(0 <= r.first && r.first < static_cast<ssize_t>(content_length));\n  assert(r.first <= r.second &&\n         r.second < static_cast<ssize_t>(content_length));\n  (void)(content_length);\n  return std::make_pair(r.first, static_cast<size_t>(r.second - r.first) + 1);\n}\n\ninline std::string make_content_range_header_field(\n    const std::pair<size_t, size_t> &offset_and_length, size_t content_length) {\n  auto st = offset_and_length.first;\n  auto ed = st + offset_and_length.second - 1;\n\n  std::string field = \"bytes \";\n  field += std::to_string(st);\n  field += '-';\n  field += std::to_string(ed);\n  field += '/';\n  field += std::to_string(content_length);\n  return field;\n}\n\ntemplate <typename SToken, typename CToken, typename Content>\nbool process_multipart_ranges_data(const Request &req,\n                                   const std::string &boundary,\n                                   const std::string &content_type,\n                                   size_t content_length, SToken stoken,\n                                   CToken ctoken, Content content) {\n  for (size_t i = 0; i < req.ranges.size(); i++) {\n    ctoken(\"--\");\n    stoken(boundary);\n    ctoken(\"\\r\\n\");\n    if (!content_type.empty()) {\n      ctoken(\"Content-Type: \");\n      stoken(content_type);\n      ctoken(\"\\r\\n\");\n    }\n\n    auto offset_and_length =\n        get_range_offset_and_length(req.ranges[i], content_length);\n\n    ctoken(\"Content-Range: \");\n    stoken(make_content_range_header_field(offset_and_length, content_length));\n    ctoken(\"\\r\\n\");\n    ctoken(\"\\r\\n\");\n\n    if (!content(offset_and_length.first, offset_and_length.second)) {\n      return false;\n    }\n    ctoken(\"\\r\\n\");\n  }\n\n  ctoken(\"--\");\n  stoken(boundary);\n  ctoken(\"--\");\n\n  return true;\n}\n\ninline void make_multipart_ranges_data(const Request &req, Response &res,\n                                       const std::string &boundary,\n                                       const std::string &content_type,\n                                       size_t content_length,\n                                       std::string &data) {\n  process_multipart_ranges_data(\n      req, boundary, content_type, content_length,\n      [&](const std::string &token) { data += token; },\n      [&](const std::string &token) { data += token; },\n      [&](size_t offset, size_t length) {\n        assert(offset + length <= content_length);\n        data += res.body.substr(offset, length);\n        return true;\n      });\n}\n\ninline size_t get_multipart_ranges_data_length(const Request &req,\n                                               const std::string &boundary,\n                                               const std::string &content_type,\n                                               size_t content_length) {\n  size_t data_length = 0;\n\n  process_multipart_ranges_data(\n      req, boundary, content_type, content_length,\n      [&](const std::string &token) { data_length += token.size(); },\n      [&](const std::string &token) { data_length += token.size(); },\n      [&](size_t /*offset*/, size_t length) {\n        data_length += length;\n        return true;\n      });\n\n  return data_length;\n}\n\ntemplate <typename T>\ninline bool\nwrite_multipart_ranges_data(Stream &strm, const Request &req, Response &res,\n                            const std::string &boundary,\n                            const std::string &content_type,\n                            size_t content_length, const T &is_shutting_down) {\n  return process_multipart_ranges_data(\n      req, boundary, content_type, content_length,\n      [&](const std::string &token) { strm.write(token); },\n      [&](const std::string &token) { strm.write(token); },\n      [&](size_t offset, size_t length) {\n        return write_content(strm, res.content_provider_, offset, length,\n                             is_shutting_down);\n      });\n}\n\ninline bool expect_content(const Request &req) {\n  if (req.method == \"POST\" || req.method == \"PUT\" || req.method == \"PATCH\" ||\n      req.method == \"DELETE\") {\n    return true;\n  }\n  if (req.has_header(\"Content-Length\") &&\n      req.get_header_value_u64(\"Content-Length\") > 0) {\n    return true;\n  }\n  if (is_chunked_transfer_encoding(req.headers)) { return true; }\n  return false;\n}\n\ninline bool has_crlf(const std::string &s) {\n  auto p = s.c_str();\n  while (*p) {\n    if (*p == '\\r' || *p == '\\n') { return true; }\n    p++;\n  }\n  return false;\n}\n\n#ifdef _WIN32\nclass WSInit {\npublic:\n  WSInit() {\n    WSADATA wsaData;\n    if (WSAStartup(0x0002, &wsaData) == 0) is_valid_ = true;\n  }\n\n  ~WSInit() {\n    if (is_valid_) WSACleanup();\n  }\n\n  bool is_valid_ = false;\n};\n\nstatic WSInit wsinit_;\n#endif\n\ninline bool parse_www_authenticate(const Response &res,\n                                   std::map<std::string, std::string> &auth,\n                                   bool is_proxy) {\n  auto auth_key = is_proxy ? \"Proxy-Authenticate\" : \"WWW-Authenticate\";\n  if (res.has_header(auth_key)) {\n    thread_local auto re =\n        std::regex(R\"~((?:(?:,\\s*)?(.+?)=(?:\"(.*?)\"|([^,]*))))~\");\n    auto s = res.get_header_value(auth_key);\n    auto pos = s.find(' ');\n    if (pos != std::string::npos) {\n      auto type = s.substr(0, pos);\n      if (type == \"Basic\") {\n        return false;\n      } else if (type == \"Digest\") {\n        s = s.substr(pos + 1);\n        auto beg = std::sregex_iterator(s.begin(), s.end(), re);\n        for (auto i = beg; i != std::sregex_iterator(); ++i) {\n          const auto &m = *i;\n          auto key = s.substr(static_cast<size_t>(m.position(1)),\n                              static_cast<size_t>(m.length(1)));\n          auto val = m.length(2) > 0\n                         ? s.substr(static_cast<size_t>(m.position(2)),\n                                    static_cast<size_t>(m.length(2)))\n                         : s.substr(static_cast<size_t>(m.position(3)),\n                                    static_cast<size_t>(m.length(3)));\n          auth[std::move(key)] = std::move(val);\n        }\n        return true;\n      }\n    }\n  }\n  return false;\n}\n\nclass ContentProviderAdapter {\npublic:\n  explicit ContentProviderAdapter(\n      ContentProviderWithoutLength &&content_provider)\n      : content_provider_(std::move(content_provider)) {}\n\n  bool operator()(size_t offset, size_t, DataSink &sink) {\n    return content_provider_(offset, sink);\n  }\n\nprivate:\n  ContentProviderWithoutLength content_provider_;\n};\n\n// NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5\nnamespace fields {\n\ninline bool is_token_char(char c) {\n  return std::isalnum(c) || c == '!' || c == '#' || c == '$' || c == '%' ||\n         c == '&' || c == '\\'' || c == '*' || c == '+' || c == '-' ||\n         c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';\n}\n\ninline bool is_token(const std::string &s) {\n  if (s.empty()) { return false; }\n  for (auto c : s) {\n    if (!is_token_char(c)) { return false; }\n  }\n  return true;\n}\n\ninline bool is_field_name(const std::string &s) { return is_token(s); }\n\ninline bool is_vchar(char c) { return c >= 33 && c <= 126; }\n\ninline bool is_obs_text(char c) { return 128 <= static_cast<unsigned char>(c); }\n\ninline bool is_field_vchar(char c) { return is_vchar(c) || is_obs_text(c); }\n\ninline bool is_field_content(const std::string &s) {\n  if (s.empty()) { return true; }\n\n  if (s.size() == 1) {\n    return is_field_vchar(s[0]);\n  } else if (s.size() == 2) {\n    return is_field_vchar(s[0]) && is_field_vchar(s[1]);\n  } else {\n    size_t i = 0;\n\n    if (!is_field_vchar(s[i])) { return false; }\n    i++;\n\n    while (i < s.size() - 1) {\n      auto c = s[i++];\n      if (c == ' ' || c == '\\t' || is_field_vchar(c)) {\n      } else {\n        return false;\n      }\n    }\n\n    return is_field_vchar(s[i]);\n  }\n}\n\ninline bool is_field_value(const std::string &s) { return is_field_content(s); }\n\n} // namespace fields\n} // namespace detail\n\n/*\n * Group 2: detail namespace - SSL common utilities\n */\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\nnamespace detail {\n\nclass SSLSocketStream final : public Stream {\npublic:\n  SSLSocketStream(\n      socket_t sock, tls::session_t session, time_t read_timeout_sec,\n      time_t read_timeout_usec, time_t write_timeout_sec,\n      time_t write_timeout_usec, time_t max_timeout_msec = 0,\n      std::chrono::time_point<std::chrono::steady_clock> start_time =\n          (std::chrono::steady_clock::time_point::min)());\n  ~SSLSocketStream() override;\n\n  bool is_readable() const override;\n  bool wait_readable() const override;\n  bool wait_writable() const override;\n  ssize_t read(char *ptr, size_t size) override;\n  ssize_t write(const char *ptr, size_t size) override;\n  void get_remote_ip_and_port(std::string &ip, int &port) const override;\n  void get_local_ip_and_port(std::string &ip, int &port) const override;\n  socket_t socket() const override;\n  time_t duration() const override;\n\nprivate:\n  socket_t sock_;\n  tls::session_t session_;\n  time_t read_timeout_sec_;\n  time_t read_timeout_usec_;\n  time_t write_timeout_sec_;\n  time_t write_timeout_usec_;\n  time_t max_timeout_msec_;\n  const std::chrono::time_point<std::chrono::steady_clock> start_time_;\n};\n\n#ifdef CPPHTTPLIB_OPENSSL_SUPPORT\ninline std::string message_digest(const std::string &s, const EVP_MD *algo) {\n  auto context = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(\n      EVP_MD_CTX_new(), EVP_MD_CTX_free);\n\n  unsigned int hash_length = 0;\n  unsigned char hash[EVP_MAX_MD_SIZE];\n\n  EVP_DigestInit_ex(context.get(), algo, nullptr);\n  EVP_DigestUpdate(context.get(), s.c_str(), s.size());\n  EVP_DigestFinal_ex(context.get(), hash, &hash_length);\n\n  std::stringstream ss;\n  for (auto i = 0u; i < hash_length; ++i) {\n    ss << std::hex << std::setw(2) << std::setfill('0')\n       << static_cast<unsigned int>(hash[i]);\n  }\n\n  return ss.str();\n}\n\ninline std::string MD5(const std::string &s) {\n  return message_digest(s, EVP_md5());\n}\n\ninline std::string SHA_256(const std::string &s) {\n  return message_digest(s, EVP_sha256());\n}\n\ninline std::string SHA_512(const std::string &s) {\n  return message_digest(s, EVP_sha512());\n}\n#elif defined(CPPHTTPLIB_MBEDTLS_SUPPORT)\nnamespace {\ntemplate <size_t N>\ninline std::string hash_to_hex(const unsigned char (&hash)[N]) {\n  std::stringstream ss;\n  for (size_t i = 0; i < N; ++i) {\n    ss << std::hex << std::setw(2) << std::setfill('0')\n       << static_cast<unsigned int>(hash[i]);\n  }\n  return ss.str();\n}\n} // namespace\n\ninline std::string MD5(const std::string &s) {\n  unsigned char hash[16];\n#ifdef CPPHTTPLIB_MBEDTLS_V3\n  mbedtls_md5(reinterpret_cast<const unsigned char *>(s.c_str()), s.size(),\n              hash);\n#else\n  mbedtls_md5_ret(reinterpret_cast<const unsigned char *>(s.c_str()), s.size(),\n                  hash);\n#endif\n  return hash_to_hex(hash);\n}\n\ninline std::string SHA_256(const std::string &s) {\n  unsigned char hash[32];\n#ifdef CPPHTTPLIB_MBEDTLS_V3\n  mbedtls_sha256(reinterpret_cast<const unsigned char *>(s.c_str()), s.size(),\n                 hash, 0);\n#else\n  mbedtls_sha256_ret(reinterpret_cast<const unsigned char *>(s.c_str()),\n                     s.size(), hash, 0);\n#endif\n  return hash_to_hex(hash);\n}\n\ninline std::string SHA_512(const std::string &s) {\n  unsigned char hash[64];\n#ifdef CPPHTTPLIB_MBEDTLS_V3\n  mbedtls_sha512(reinterpret_cast<const unsigned char *>(s.c_str()), s.size(),\n                 hash, 0);\n#else\n  mbedtls_sha512_ret(reinterpret_cast<const unsigned char *>(s.c_str()),\n                     s.size(), hash, 0);\n#endif\n  return hash_to_hex(hash);\n}\n#endif\n\ninline bool is_ip_address(const std::string &host) {\n  struct in_addr addr4;\n  struct in6_addr addr6;\n  return inet_pton(AF_INET, host.c_str(), &addr4) == 1 ||\n         inet_pton(AF_INET6, host.c_str(), &addr6) == 1;\n}\n\ntemplate <typename T>\ninline bool process_server_socket_ssl(\n    const std::atomic<socket_t> &svr_sock, tls::session_t session,\n    socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec,\n    time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec,\n    time_t write_timeout_usec, T callback) {\n  return process_server_socket_core(\n      svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec,\n      [&](bool close_connection, bool &connection_closed) {\n        SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec,\n                             write_timeout_sec, write_timeout_usec);\n        return callback(strm, close_connection, connection_closed);\n      });\n}\n\ntemplate <typename T>\ninline bool process_client_socket_ssl(\n    tls::session_t session, socket_t sock, time_t read_timeout_sec,\n    time_t read_timeout_usec, time_t write_timeout_sec,\n    time_t write_timeout_usec, time_t max_timeout_msec,\n    std::chrono::time_point<std::chrono::steady_clock> start_time, T callback) {\n  SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec,\n                       write_timeout_sec, write_timeout_usec, max_timeout_msec,\n                       start_time);\n  return callback(strm);\n}\n\ninline std::pair<std::string, std::string> make_digest_authentication_header(\n    const Request &req, const std::map<std::string, std::string> &auth,\n    size_t cnonce_count, const std::string &cnonce, const std::string &username,\n    const std::string &password, bool is_proxy = false) {\n  std::string nc;\n  {\n    std::stringstream ss;\n    ss << std::setfill('0') << std::setw(8) << std::hex << cnonce_count;\n    nc = ss.str();\n  }\n\n  std::string qop;\n  if (auth.find(\"qop\") != auth.end()) {\n    qop = auth.at(\"qop\");\n    if (qop.find(\"auth-int\") != std::string::npos) {\n      qop = \"auth-int\";\n    } else if (qop.find(\"auth\") != std::string::npos) {\n      qop = \"auth\";\n    } else {\n      qop.clear();\n    }\n  }\n\n  std::string algo = \"MD5\";\n  if (auth.find(\"algorithm\") != auth.end()) { algo = auth.at(\"algorithm\"); }\n\n  std::string response;\n  {\n    auto H = algo == \"SHA-256\"   ? detail::SHA_256\n             : algo == \"SHA-512\" ? detail::SHA_512\n                                 : detail::MD5;\n\n    auto A1 = username + \":\" + auth.at(\"realm\") + \":\" + password;\n\n    auto A2 = req.method + \":\" + req.path;\n    if (qop == \"auth-int\") { A2 += \":\" + H(req.body); }\n\n    if (qop.empty()) {\n      response = H(H(A1) + \":\" + auth.at(\"nonce\") + \":\" + H(A2));\n    } else {\n      response = H(H(A1) + \":\" + auth.at(\"nonce\") + \":\" + nc + \":\" + cnonce +\n                   \":\" + qop + \":\" + H(A2));\n    }\n  }\n\n  auto opaque = (auth.find(\"opaque\") != auth.end()) ? auth.at(\"opaque\") : \"\";\n\n  auto field = \"Digest username=\\\"\" + username + \"\\\", realm=\\\"\" +\n               auth.at(\"realm\") + \"\\\", nonce=\\\"\" + auth.at(\"nonce\") +\n               \"\\\", uri=\\\"\" + req.path + \"\\\", algorithm=\" + algo +\n               (qop.empty() ? \", response=\\\"\"\n                            : \", qop=\" + qop + \", nc=\" + nc + \", cnonce=\\\"\" +\n                                  cnonce + \"\\\", response=\\\"\") +\n               response + \"\\\"\" +\n               (opaque.empty() ? \"\" : \", opaque=\\\"\" + opaque + \"\\\"\");\n\n  auto key = is_proxy ? \"Proxy-Authorization\" : \"Authorization\";\n  return std::make_pair(key, field);\n}\n\ninline bool match_hostname(const std::string &pattern,\n                           const std::string &hostname) {\n  // Exact match (case-insensitive)\n  if (detail::case_ignore::equal(hostname, pattern)) { return true; }\n\n  // Split both pattern and hostname into components by '.'\n  std::vector<std::string> pattern_components;\n  if (!pattern.empty()) {\n    split(pattern.data(), pattern.data() + pattern.size(), '.',\n          [&](const char *b, const char *e) {\n            pattern_components.emplace_back(b, e);\n          });\n  }\n\n  std::vector<std::string> host_components;\n  if (!hostname.empty()) {\n    split(hostname.data(), hostname.data() + hostname.size(), '.',\n          [&](const char *b, const char *e) {\n            host_components.emplace_back(b, e);\n          });\n  }\n\n  // Component count must match\n  if (host_components.size() != pattern_components.size()) { return false; }\n\n  // Compare each component with wildcard support\n  // Supports: \"*\" (full wildcard), \"prefix*\" (partial wildcard)\n  // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484\n  auto itr = pattern_components.begin();\n  for (const auto &h : host_components) {\n    auto &p = *itr;\n    if (!detail::case_ignore::equal(p, h) && p != \"*\") {\n      bool partial_match = false;\n      if (!p.empty() && p[p.size() - 1] == '*') {\n        const auto prefix_length = p.size() - 1;\n        if (prefix_length == 0) {\n          partial_match = true;\n        } else if (h.size() >= prefix_length) {\n          partial_match =\n              std::equal(p.begin(),\n                         p.begin() + static_cast<std::string::difference_type>(\n                                         prefix_length),\n                         h.begin(), [](const char ca, const char cb) {\n                           return detail::case_ignore::to_lower(ca) ==\n                                  detail::case_ignore::to_lower(cb);\n                         });\n        }\n      }\n      if (!partial_match) { return false; }\n    }\n    ++itr;\n  }\n\n  return true;\n}\n\n#ifdef _WIN32\n// Verify certificate using Windows CertGetCertificateChain API.\n// This provides real-time certificate validation with Windows Update\n// integration, independent of the TLS backend (OpenSSL or MbedTLS).\ninline bool verify_cert_with_windows_schannel(\n    const std::vector<unsigned char> &der_cert, const std::string &hostname,\n    bool verify_hostname, unsigned long &out_error) {\n  if (der_cert.empty()) { return false; }\n\n  out_error = 0;\n\n  // Create Windows certificate context from DER data\n  auto cert_context = CertCreateCertificateContext(\n      X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, der_cert.data(),\n      static_cast<DWORD>(der_cert.size()));\n\n  if (!cert_context) {\n    out_error = GetLastError();\n    return false;\n  }\n\n  auto cert_guard =\n      scope_exit([&] { CertFreeCertificateContext(cert_context); });\n\n  // Setup chain parameters\n  CERT_CHAIN_PARA chain_para = {};\n  chain_para.cbSize = sizeof(chain_para);\n\n  // Build certificate chain with revocation checking\n  PCCERT_CHAIN_CONTEXT chain_context = nullptr;\n  auto chain_result = CertGetCertificateChain(\n      nullptr, cert_context, nullptr, cert_context->hCertStore, &chain_para,\n      CERT_CHAIN_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_END_CERT |\n          CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT,\n      nullptr, &chain_context);\n\n  if (!chain_result || !chain_context) {\n    out_error = GetLastError();\n    return false;\n  }\n\n  auto chain_guard =\n      scope_exit([&] { CertFreeCertificateChain(chain_context); });\n\n  // Check if chain has errors\n  if (chain_context->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) {\n    out_error = chain_context->TrustStatus.dwErrorStatus;\n    return false;\n  }\n\n  // Verify SSL policy\n  SSL_EXTRA_CERT_CHAIN_POLICY_PARA extra_policy_para = {};\n  extra_policy_para.cbSize = sizeof(extra_policy_para);\n#ifdef AUTHTYPE_SERVER\n  extra_policy_para.dwAuthType = AUTHTYPE_SERVER;\n#endif\n\n  std::wstring whost;\n  if (verify_hostname) {\n    whost = u8string_to_wstring(hostname.c_str());\n    extra_policy_para.pwszServerName = const_cast<wchar_t *>(whost.c_str());\n  }\n\n  CERT_CHAIN_POLICY_PARA policy_para = {};\n  policy_para.cbSize = sizeof(policy_para);\n#ifdef CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS\n  policy_para.dwFlags = CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS;\n#else\n  policy_para.dwFlags = 0;\n#endif\n  policy_para.pvExtraPolicyPara = &extra_policy_para;\n\n  CERT_CHAIN_POLICY_STATUS policy_status = {};\n  policy_status.cbSize = sizeof(policy_status);\n\n  if (!CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, chain_context,\n                                        &policy_para, &policy_status)) {\n    out_error = GetLastError();\n    return false;\n  }\n\n  if (policy_status.dwError != 0) {\n    out_error = policy_status.dwError;\n    return false;\n  }\n\n  return true;\n}\n#endif // _WIN32\n\n} // namespace detail\n#endif // CPPHTTPLIB_SSL_ENABLED\n\n/*\n * Group 3: httplib namespace - Non-SSL public API implementations\n */\n\ninline void default_socket_options(socket_t sock) {\n  detail::set_socket_opt(sock, SOL_SOCKET,\n#ifdef SO_REUSEPORT\n                         SO_REUSEPORT,\n#else\n                         SO_REUSEADDR,\n#endif\n                         1);\n}\n\ninline std::string get_bearer_token_auth(const Request &req) {\n  if (req.has_header(\"Authorization\")) {\n    constexpr auto bearer_header_prefix_len = detail::str_len(\"Bearer \");\n    return req.get_header_value(\"Authorization\")\n        .substr(bearer_header_prefix_len);\n  }\n  return \"\";\n}\n\ninline const char *status_message(int status) {\n  switch (status) {\n  case StatusCode::Continue_100: return \"Continue\";\n  case StatusCode::SwitchingProtocol_101: return \"Switching Protocol\";\n  case StatusCode::Processing_102: return \"Processing\";\n  case StatusCode::EarlyHints_103: return \"Early Hints\";\n  case StatusCode::OK_200: return \"OK\";\n  case StatusCode::Created_201: return \"Created\";\n  case StatusCode::Accepted_202: return \"Accepted\";\n  case StatusCode::NonAuthoritativeInformation_203:\n    return \"Non-Authoritative Information\";\n  case StatusCode::NoContent_204: return \"No Content\";\n  case StatusCode::ResetContent_205: return \"Reset Content\";\n  case StatusCode::PartialContent_206: return \"Partial Content\";\n  case StatusCode::MultiStatus_207: return \"Multi-Status\";\n  case StatusCode::AlreadyReported_208: return \"Already Reported\";\n  case StatusCode::IMUsed_226: return \"IM Used\";\n  case StatusCode::MultipleChoices_300: return \"Multiple Choices\";\n  case StatusCode::MovedPermanently_301: return \"Moved Permanently\";\n  case StatusCode::Found_302: return \"Found\";\n  case StatusCode::SeeOther_303: return \"See Other\";\n  case StatusCode::NotModified_304: return \"Not Modified\";\n  case StatusCode::UseProxy_305: return \"Use Proxy\";\n  case StatusCode::unused_306: return \"unused\";\n  case StatusCode::TemporaryRedirect_307: return \"Temporary Redirect\";\n  case StatusCode::PermanentRedirect_308: return \"Permanent Redirect\";\n  case StatusCode::BadRequest_400: return \"Bad Request\";\n  case StatusCode::Unauthorized_401: return \"Unauthorized\";\n  case StatusCode::PaymentRequired_402: return \"Payment Required\";\n  case StatusCode::Forbidden_403: return \"Forbidden\";\n  case StatusCode::NotFound_404: return \"Not Found\";\n  case StatusCode::MethodNotAllowed_405: return \"Method Not Allowed\";\n  case StatusCode::NotAcceptable_406: return \"Not Acceptable\";\n  case StatusCode::ProxyAuthenticationRequired_407:\n    return \"Proxy Authentication Required\";\n  case StatusCode::RequestTimeout_408: return \"Request Timeout\";\n  case StatusCode::Conflict_409: return \"Conflict\";\n  case StatusCode::Gone_410: return \"Gone\";\n  case StatusCode::LengthRequired_411: return \"Length Required\";\n  case StatusCode::PreconditionFailed_412: return \"Precondition Failed\";\n  case StatusCode::PayloadTooLarge_413: return \"Payload Too Large\";\n  case StatusCode::UriTooLong_414: return \"URI Too Long\";\n  case StatusCode::UnsupportedMediaType_415: return \"Unsupported Media Type\";\n  case StatusCode::RangeNotSatisfiable_416: return \"Range Not Satisfiable\";\n  case StatusCode::ExpectationFailed_417: return \"Expectation Failed\";\n  case StatusCode::ImATeapot_418: return \"I'm a teapot\";\n  case StatusCode::MisdirectedRequest_421: return \"Misdirected Request\";\n  case StatusCode::UnprocessableContent_422: return \"Unprocessable Content\";\n  case StatusCode::Locked_423: return \"Locked\";\n  case StatusCode::FailedDependency_424: return \"Failed Dependency\";\n  case StatusCode::TooEarly_425: return \"Too Early\";\n  case StatusCode::UpgradeRequired_426: return \"Upgrade Required\";\n  case StatusCode::PreconditionRequired_428: return \"Precondition Required\";\n  case StatusCode::TooManyRequests_429: return \"Too Many Requests\";\n  case StatusCode::RequestHeaderFieldsTooLarge_431:\n    return \"Request Header Fields Too Large\";\n  case StatusCode::UnavailableForLegalReasons_451:\n    return \"Unavailable For Legal Reasons\";\n  case StatusCode::NotImplemented_501: return \"Not Implemented\";\n  case StatusCode::BadGateway_502: return \"Bad Gateway\";\n  case StatusCode::ServiceUnavailable_503: return \"Service Unavailable\";\n  case StatusCode::GatewayTimeout_504: return \"Gateway Timeout\";\n  case StatusCode::HttpVersionNotSupported_505:\n    return \"HTTP Version Not Supported\";\n  case StatusCode::VariantAlsoNegotiates_506: return \"Variant Also Negotiates\";\n  case StatusCode::InsufficientStorage_507: return \"Insufficient Storage\";\n  case StatusCode::LoopDetected_508: return \"Loop Detected\";\n  case StatusCode::NotExtended_510: return \"Not Extended\";\n  case StatusCode::NetworkAuthenticationRequired_511:\n    return \"Network Authentication Required\";\n\n  default:\n  case StatusCode::InternalServerError_500: return \"Internal Server Error\";\n  }\n}\n\ninline std::string to_string(const Error error) {\n  switch (error) {\n  case Error::Success: return \"Success (no error)\";\n  case Error::Unknown: return \"Unknown\";\n  case Error::Connection: return \"Could not establish connection\";\n  case Error::BindIPAddress: return \"Failed to bind IP address\";\n  case Error::Read: return \"Failed to read connection\";\n  case Error::Write: return \"Failed to write connection\";\n  case Error::ExceedRedirectCount: return \"Maximum redirect count exceeded\";\n  case Error::Canceled: return \"Connection handling canceled\";\n  case Error::SSLConnection: return \"SSL connection failed\";\n  case Error::SSLLoadingCerts: return \"SSL certificate loading failed\";\n  case Error::SSLServerVerification: return \"SSL server verification failed\";\n  case Error::SSLServerHostnameVerification:\n    return \"SSL server hostname verification failed\";\n  case Error::UnsupportedMultipartBoundaryChars:\n    return \"Unsupported HTTP multipart boundary characters\";\n  case Error::Compression: return \"Compression failed\";\n  case Error::ConnectionTimeout: return \"Connection timed out\";\n  case Error::ProxyConnection: return \"Proxy connection failed\";\n  case Error::ConnectionClosed: return \"Connection closed by server\";\n  case Error::Timeout: return \"Read timeout\";\n  case Error::ResourceExhaustion: return \"Resource exhaustion\";\n  case Error::TooManyFormDataFiles: return \"Too many form data files\";\n  case Error::ExceedMaxPayloadSize: return \"Exceeded maximum payload size\";\n  case Error::ExceedUriMaxLength: return \"Exceeded maximum URI length\";\n  case Error::ExceedMaxSocketDescriptorCount:\n    return \"Exceeded maximum socket descriptor count\";\n  case Error::InvalidRequestLine: return \"Invalid request line\";\n  case Error::InvalidHTTPMethod: return \"Invalid HTTP method\";\n  case Error::InvalidHTTPVersion: return \"Invalid HTTP version\";\n  case Error::InvalidHeaders: return \"Invalid headers\";\n  case Error::MultipartParsing: return \"Multipart parsing failed\";\n  case Error::OpenFile: return \"Failed to open file\";\n  case Error::Listen: return \"Failed to listen on socket\";\n  case Error::GetSockName: return \"Failed to get socket name\";\n  case Error::UnsupportedAddressFamily: return \"Unsupported address family\";\n  case Error::HTTPParsing: return \"HTTP parsing failed\";\n  case Error::InvalidRangeHeader: return \"Invalid Range header\";\n  default: break;\n  }\n\n  return \"Invalid\";\n}\n\ninline std::ostream &operator<<(std::ostream &os, const Error &obj) {\n  os << to_string(obj);\n  os << \" (\" << static_cast<std::underlying_type<Error>::type>(obj) << ')';\n  return os;\n}\n\ninline std::string hosted_at(const std::string &hostname) {\n  std::vector<std::string> addrs;\n  hosted_at(hostname, addrs);\n  if (addrs.empty()) { return std::string(); }\n  return addrs[0];\n}\n\ninline void hosted_at(const std::string &hostname,\n                      std::vector<std::string> &addrs) {\n  struct addrinfo hints;\n  struct addrinfo *result;\n\n  memset(&hints, 0, sizeof(struct addrinfo));\n  hints.ai_family = AF_UNSPEC;\n  hints.ai_socktype = SOCK_STREAM;\n  hints.ai_protocol = 0;\n\n  if (detail::getaddrinfo_with_timeout(hostname.c_str(), nullptr, &hints,\n                                       &result, 0)) {\n#if defined __linux__ && !defined __ANDROID__\n    res_init();\n#endif\n    return;\n  }\n  auto se = detail::scope_exit([&] { freeaddrinfo(result); });\n\n  for (auto rp = result; rp; rp = rp->ai_next) {\n    const auto &addr =\n        *reinterpret_cast<struct sockaddr_storage *>(rp->ai_addr);\n    std::string ip;\n    auto dummy = -1;\n    if (detail::get_ip_and_port(addr, sizeof(struct sockaddr_storage), ip,\n                                dummy)) {\n      addrs.emplace_back(std::move(ip));\n    }\n  }\n}\n\ninline std::string encode_uri_component(const std::string &value) {\n  std::ostringstream escaped;\n  escaped.fill('0');\n  escaped << std::hex;\n\n  for (auto c : value) {\n    if (std::isalnum(static_cast<uint8_t>(c)) || c == '-' || c == '_' ||\n        c == '.' || c == '!' || c == '~' || c == '*' || c == '\\'' || c == '(' ||\n        c == ')') {\n      escaped << c;\n    } else {\n      escaped << std::uppercase;\n      escaped << '%' << std::setw(2)\n              << static_cast<int>(static_cast<unsigned char>(c));\n      escaped << std::nouppercase;\n    }\n  }\n\n  return escaped.str();\n}\n\ninline std::string encode_uri(const std::string &value) {\n  std::ostringstream escaped;\n  escaped.fill('0');\n  escaped << std::hex;\n\n  for (auto c : value) {\n    if (std::isalnum(static_cast<uint8_t>(c)) || c == '-' || c == '_' ||\n        c == '.' || c == '!' || c == '~' || c == '*' || c == '\\'' || c == '(' ||\n        c == ')' || c == ';' || c == '/' || c == '?' || c == ':' || c == '@' ||\n        c == '&' || c == '=' || c == '+' || c == '$' || c == ',' || c == '#') {\n      escaped << c;\n    } else {\n      escaped << std::uppercase;\n      escaped << '%' << std::setw(2)\n              << static_cast<int>(static_cast<unsigned char>(c));\n      escaped << std::nouppercase;\n    }\n  }\n\n  return escaped.str();\n}\n\ninline std::string decode_uri_component(const std::string &value) {\n  std::string result;\n\n  for (size_t i = 0; i < value.size(); i++) {\n    if (value[i] == '%' && i + 2 < value.size()) {\n      auto val = 0;\n      if (detail::from_hex_to_i(value, i + 1, 2, val)) {\n        result += static_cast<char>(val);\n        i += 2;\n      } else {\n        result += value[i];\n      }\n    } else {\n      result += value[i];\n    }\n  }\n\n  return result;\n}\n\ninline std::string decode_uri(const std::string &value) {\n  std::string result;\n\n  for (size_t i = 0; i < value.size(); i++) {\n    if (value[i] == '%' && i + 2 < value.size()) {\n      auto val = 0;\n      if (detail::from_hex_to_i(value, i + 1, 2, val)) {\n        result += static_cast<char>(val);\n        i += 2;\n      } else {\n        result += value[i];\n      }\n    } else {\n      result += value[i];\n    }\n  }\n\n  return result;\n}\n\ninline std::string encode_path_component(const std::string &component) {\n  std::string result;\n  result.reserve(component.size() * 3);\n\n  for (size_t i = 0; i < component.size(); i++) {\n    auto c = static_cast<unsigned char>(component[i]);\n\n    // Unreserved characters per RFC 3986: ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n    if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {\n      result += static_cast<char>(c);\n    }\n    // Path-safe sub-delimiters: \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" /\n    // \",\" / \";\" / \"=\"\n    else if (c == '!' || c == '$' || c == '&' || c == '\\'' || c == '(' ||\n             c == ')' || c == '*' || c == '+' || c == ',' || c == ';' ||\n             c == '=') {\n      result += static_cast<char>(c);\n    }\n    // Colon is allowed in path segments except first segment\n    else if (c == ':') {\n      result += static_cast<char>(c);\n    }\n    // @ is allowed in path\n    else if (c == '@') {\n      result += static_cast<char>(c);\n    } else {\n      result += '%';\n      char hex[3];\n      snprintf(hex, sizeof(hex), \"%02X\", c);\n      result.append(hex, 2);\n    }\n  }\n  return result;\n}\n\ninline std::string decode_path_component(const std::string &component) {\n  std::string result;\n  result.reserve(component.size());\n\n  for (size_t i = 0; i < component.size(); i++) {\n    if (component[i] == '%' && i + 1 < component.size()) {\n      if (component[i + 1] == 'u') {\n        // Unicode %uXXXX encoding\n        auto val = 0;\n        if (detail::from_hex_to_i(component, i + 2, 4, val)) {\n          // 4 digits Unicode codes\n          char buff[4];\n          size_t len = detail::to_utf8(val, buff);\n          if (len > 0) { result.append(buff, len); }\n          i += 5; // 'u0000'\n        } else {\n          result += component[i];\n        }\n      } else {\n        // Standard %XX encoding\n        auto val = 0;\n        if (detail::from_hex_to_i(component, i + 1, 2, val)) {\n          // 2 digits hex codes\n          result += static_cast<char>(val);\n          i += 2; // 'XX'\n        } else {\n          result += component[i];\n        }\n      }\n    } else {\n      result += component[i];\n    }\n  }\n  return result;\n}\n\ninline std::string encode_query_component(const std::string &component,\n                                          bool space_as_plus) {\n  std::string result;\n  result.reserve(component.size() * 3);\n\n  for (size_t i = 0; i < component.size(); i++) {\n    auto c = static_cast<unsigned char>(component[i]);\n\n    // Unreserved characters per RFC 3986\n    if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {\n      result += static_cast<char>(c);\n    }\n    // Space handling\n    else if (c == ' ') {\n      if (space_as_plus) {\n        result += '+';\n      } else {\n        result += \"%20\";\n      }\n    }\n    // Plus sign handling\n    else if (c == '+') {\n      if (space_as_plus) {\n        result += \"%2B\";\n      } else {\n        result += static_cast<char>(c);\n      }\n    }\n    // Query-safe sub-delimiters (excluding & and = which are query delimiters)\n    else if (c == '!' || c == '$' || c == '\\'' || c == '(' || c == ')' ||\n             c == '*' || c == ',' || c == ';') {\n      result += static_cast<char>(c);\n    }\n    // Colon and @ are allowed in query\n    else if (c == ':' || c == '@') {\n      result += static_cast<char>(c);\n    }\n    // Forward slash is allowed in query values\n    else if (c == '/') {\n      result += static_cast<char>(c);\n    }\n    // Question mark is allowed in query values (after first ?)\n    else if (c == '?') {\n      result += static_cast<char>(c);\n    } else {\n      result += '%';\n      char hex[3];\n      snprintf(hex, sizeof(hex), \"%02X\", c);\n      result.append(hex, 2);\n    }\n  }\n  return result;\n}\n\ninline std::string decode_query_component(const std::string &component,\n                                          bool plus_as_space) {\n  std::string result;\n  result.reserve(component.size());\n\n  for (size_t i = 0; i < component.size(); i++) {\n    if (component[i] == '%' && i + 2 < component.size()) {\n      std::string hex = component.substr(i + 1, 2);\n      char *end;\n      unsigned long value = std::strtoul(hex.c_str(), &end, 16);\n      if (end == hex.c_str() + 2) {\n        result += static_cast<char>(value);\n        i += 2;\n      } else {\n        result += component[i];\n      }\n    } else if (component[i] == '+' && plus_as_space) {\n      result += ' '; // + becomes space in form-urlencoded\n    } else {\n      result += component[i];\n    }\n  }\n  return result;\n}\n\ninline std::string append_query_params(const std::string &path,\n                                       const Params &params) {\n  std::string path_with_query = path;\n  thread_local const std::regex re(\"[^?]+\\\\?.*\");\n  auto delm = std::regex_match(path, re) ? '&' : '?';\n  path_with_query += delm + detail::params_to_query_str(params);\n  return path_with_query;\n}\n\n// Header utilities\ninline std::pair<std::string, std::string>\nmake_range_header(const Ranges &ranges) {\n  std::string field = \"bytes=\";\n  auto i = 0;\n  for (const auto &r : ranges) {\n    if (i != 0) { field += \", \"; }\n    if (r.first != -1) { field += std::to_string(r.first); }\n    field += '-';\n    if (r.second != -1) { field += std::to_string(r.second); }\n    i++;\n  }\n  return std::make_pair(\"Range\", std::move(field));\n}\n\ninline std::pair<std::string, std::string>\nmake_basic_authentication_header(const std::string &username,\n                                 const std::string &password, bool is_proxy) {\n  auto field = \"Basic \" + detail::base64_encode(username + \":\" + password);\n  auto key = is_proxy ? \"Proxy-Authorization\" : \"Authorization\";\n  return std::make_pair(key, std::move(field));\n}\n\ninline std::pair<std::string, std::string>\nmake_bearer_token_authentication_header(const std::string &token,\n                                        bool is_proxy = false) {\n  auto field = \"Bearer \" + token;\n  auto key = is_proxy ? \"Proxy-Authorization\" : \"Authorization\";\n  return std::make_pair(key, std::move(field));\n}\n\n// Request implementation\ninline size_t Request::get_header_value_u64(const std::string &key, size_t def,\n                                            size_t id) const {\n  return detail::get_header_value_u64(headers, key, def, id);\n}\n\ninline bool Request::has_header(const std::string &key) const {\n  return detail::has_header(headers, key);\n}\n\ninline std::string Request::get_header_value(const std::string &key,\n                                             const char *def, size_t id) const {\n  return detail::get_header_value(headers, key, def, id);\n}\n\ninline size_t Request::get_header_value_count(const std::string &key) const {\n  auto r = headers.equal_range(key);\n  return static_cast<size_t>(std::distance(r.first, r.second));\n}\n\ninline void Request::set_header(const std::string &key,\n                                const std::string &val) {\n  if (detail::fields::is_field_name(key) &&\n      detail::fields::is_field_value(val)) {\n    headers.emplace(key, val);\n  }\n}\n\ninline bool Request::has_trailer(const std::string &key) const {\n  return trailers.find(key) != trailers.end();\n}\n\ninline std::string Request::get_trailer_value(const std::string &key,\n                                              size_t id) const {\n  auto rng = trailers.equal_range(key);\n  auto it = rng.first;\n  std::advance(it, static_cast<ssize_t>(id));\n  if (it != rng.second) { return it->second; }\n  return std::string();\n}\n\ninline size_t Request::get_trailer_value_count(const std::string &key) const {\n  auto r = trailers.equal_range(key);\n  return static_cast<size_t>(std::distance(r.first, r.second));\n}\n\ninline bool Request::has_param(const std::string &key) const {\n  return params.find(key) != params.end();\n}\n\ninline std::string Request::get_param_value(const std::string &key,\n                                            size_t id) const {\n  auto rng = params.equal_range(key);\n  auto it = rng.first;\n  std::advance(it, static_cast<ssize_t>(id));\n  if (it != rng.second) { return it->second; }\n  return std::string();\n}\n\ninline size_t Request::get_param_value_count(const std::string &key) const {\n  auto r = params.equal_range(key);\n  return static_cast<size_t>(std::distance(r.first, r.second));\n}\n\ninline bool Request::is_multipart_form_data() const {\n  const auto &content_type = get_header_value(\"Content-Type\");\n  return !content_type.rfind(\"multipart/form-data\", 0);\n}\n\n// Multipart FormData implementation\ninline std::string MultipartFormData::get_field(const std::string &key,\n                                                size_t id) const {\n  auto rng = fields.equal_range(key);\n  auto it = rng.first;\n  std::advance(it, static_cast<ssize_t>(id));\n  if (it != rng.second) { return it->second.content; }\n  return std::string();\n}\n\ninline std::vector<std::string>\nMultipartFormData::get_fields(const std::string &key) const {\n  std::vector<std::string> values;\n  auto rng = fields.equal_range(key);\n  for (auto it = rng.first; it != rng.second; it++) {\n    values.push_back(it->second.content);\n  }\n  return values;\n}\n\ninline bool MultipartFormData::has_field(const std::string &key) const {\n  return fields.find(key) != fields.end();\n}\n\ninline size_t MultipartFormData::get_field_count(const std::string &key) const {\n  auto r = fields.equal_range(key);\n  return static_cast<size_t>(std::distance(r.first, r.second));\n}\n\ninline FormData MultipartFormData::get_file(const std::string &key,\n                                            size_t id) const {\n  auto rng = files.equal_range(key);\n  auto it = rng.first;\n  std::advance(it, static_cast<ssize_t>(id));\n  if (it != rng.second) { return it->second; }\n  return FormData();\n}\n\ninline std::vector<FormData>\nMultipartFormData::get_files(const std::string &key) const {\n  std::vector<FormData> values;\n  auto rng = files.equal_range(key);\n  for (auto it = rng.first; it != rng.second; it++) {\n    values.push_back(it->second);\n  }\n  return values;\n}\n\ninline bool MultipartFormData::has_file(const std::string &key) const {\n  return files.find(key) != files.end();\n}\n\ninline size_t MultipartFormData::get_file_count(const std::string &key) const {\n  auto r = files.equal_range(key);\n  return static_cast<size_t>(std::distance(r.first, r.second));\n}\n\n// Response implementation\ninline size_t Response::get_header_value_u64(const std::string &key, size_t def,\n                                             size_t id) const {\n  return detail::get_header_value_u64(headers, key, def, id);\n}\n\ninline bool Response::has_header(const std::string &key) const {\n  return headers.find(key) != headers.end();\n}\n\ninline std::string Response::get_header_value(const std::string &key,\n                                              const char *def,\n                                              size_t id) const {\n  return detail::get_header_value(headers, key, def, id);\n}\n\ninline size_t Response::get_header_value_count(const std::string &key) const {\n  auto r = headers.equal_range(key);\n  return static_cast<size_t>(std::distance(r.first, r.second));\n}\n\ninline void Response::set_header(const std::string &key,\n                                 const std::string &val) {\n  if (detail::fields::is_field_name(key) &&\n      detail::fields::is_field_value(val)) {\n    headers.emplace(key, val);\n  }\n}\ninline bool Response::has_trailer(const std::string &key) const {\n  return trailers.find(key) != trailers.end();\n}\n\ninline std::string Response::get_trailer_value(const std::string &key,\n                                               size_t id) const {\n  auto rng = trailers.equal_range(key);\n  auto it = rng.first;\n  std::advance(it, static_cast<ssize_t>(id));\n  if (it != rng.second) { return it->second; }\n  return std::string();\n}\n\ninline size_t Response::get_trailer_value_count(const std::string &key) const {\n  auto r = trailers.equal_range(key);\n  return static_cast<size_t>(std::distance(r.first, r.second));\n}\n\ninline void Response::set_redirect(const std::string &url, int stat) {\n  if (detail::fields::is_field_value(url)) {\n    set_header(\"Location\", url);\n    if (300 <= stat && stat < 400) {\n      this->status = stat;\n    } else {\n      this->status = StatusCode::Found_302;\n    }\n  }\n}\n\ninline void Response::set_content(const char *s, size_t n,\n                                  const std::string &content_type) {\n  body.assign(s, n);\n\n  auto rng = headers.equal_range(\"Content-Type\");\n  headers.erase(rng.first, rng.second);\n  set_header(\"Content-Type\", content_type);\n}\n\ninline void Response::set_content(const std::string &s,\n                                  const std::string &content_type) {\n  set_content(s.data(), s.size(), content_type);\n}\n\ninline void Response::set_content(std::string &&s,\n                                  const std::string &content_type) {\n  body = std::move(s);\n\n  auto rng = headers.equal_range(\"Content-Type\");\n  headers.erase(rng.first, rng.second);\n  set_header(\"Content-Type\", content_type);\n}\n\ninline void Response::set_content_provider(\n    size_t in_length, const std::string &content_type, ContentProvider provider,\n    ContentProviderResourceReleaser resource_releaser) {\n  set_header(\"Content-Type\", content_type);\n  content_length_ = in_length;\n  if (in_length > 0) { content_provider_ = std::move(provider); }\n  content_provider_resource_releaser_ = std::move(resource_releaser);\n  is_chunked_content_provider_ = false;\n}\n\ninline void Response::set_content_provider(\n    const std::string &content_type, ContentProviderWithoutLength provider,\n    ContentProviderResourceReleaser resource_releaser) {\n  set_header(\"Content-Type\", content_type);\n  content_length_ = 0;\n  content_provider_ = detail::ContentProviderAdapter(std::move(provider));\n  content_provider_resource_releaser_ = std::move(resource_releaser);\n  is_chunked_content_provider_ = false;\n}\n\ninline void Response::set_chunked_content_provider(\n    const std::string &content_type, ContentProviderWithoutLength provider,\n    ContentProviderResourceReleaser resource_releaser) {\n  set_header(\"Content-Type\", content_type);\n  content_length_ = 0;\n  content_provider_ = detail::ContentProviderAdapter(std::move(provider));\n  content_provider_resource_releaser_ = std::move(resource_releaser);\n  is_chunked_content_provider_ = true;\n}\n\ninline void Response::set_file_content(const std::string &path,\n                                       const std::string &content_type) {\n  file_content_path_ = path;\n  file_content_content_type_ = content_type;\n}\n\ninline void Response::set_file_content(const std::string &path) {\n  file_content_path_ = path;\n}\n\n// Result implementation\ninline size_t Result::get_request_header_value_u64(const std::string &key,\n                                                   size_t def,\n                                                   size_t id) const {\n  return detail::get_header_value_u64(request_headers_, key, def, id);\n}\n\ninline bool Result::has_request_header(const std::string &key) const {\n  return request_headers_.find(key) != request_headers_.end();\n}\n\ninline std::string Result::get_request_header_value(const std::string &key,\n                                                    const char *def,\n                                                    size_t id) const {\n  return detail::get_header_value(request_headers_, key, def, id);\n}\n\ninline size_t\nResult::get_request_header_value_count(const std::string &key) const {\n  auto r = request_headers_.equal_range(key);\n  return static_cast<size_t>(std::distance(r.first, r.second));\n}\n\n// Stream implementation\ninline ssize_t Stream::write(const char *ptr) {\n  return write(ptr, strlen(ptr));\n}\n\ninline ssize_t Stream::write(const std::string &s) {\n  return write(s.data(), s.size());\n}\n\n// BodyReader implementation\ninline ssize_t detail::BodyReader::read(char *buf, size_t len) {\n  if (!stream) {\n    last_error = Error::Connection;\n    return -1;\n  }\n  if (eof) { return 0; }\n\n  if (!chunked) {\n    // Content-Length based reading\n    if (has_content_length && bytes_read >= content_length) {\n      eof = true;\n      return 0;\n    }\n\n    auto to_read = len;\n    if (has_content_length) {\n      auto remaining = content_length - bytes_read;\n      to_read = (std::min)(len, remaining);\n    }\n    auto n = stream->read(buf, to_read);\n\n    if (n < 0) {\n      last_error = stream->get_error();\n      if (last_error == Error::Success) { last_error = Error::Read; }\n      eof = true;\n      return n;\n    }\n    if (n == 0) {\n      // Unexpected EOF before content_length\n      last_error = stream->get_error();\n      if (last_error == Error::Success) { last_error = Error::Read; }\n      eof = true;\n      return 0;\n    }\n\n    bytes_read += static_cast<size_t>(n);\n    if (has_content_length && bytes_read >= content_length) { eof = true; }\n    if (payload_max_length > 0 && bytes_read > payload_max_length) {\n      last_error = Error::ExceedMaxPayloadSize;\n      eof = true;\n      return -1;\n    }\n    return n;\n  }\n\n  // Chunked transfer encoding: delegate to shared decoder instance.\n  if (!chunked_decoder) { chunked_decoder.reset(new ChunkedDecoder(*stream)); }\n\n  size_t chunk_offset = 0;\n  size_t chunk_total = 0;\n  auto n = chunked_decoder->read_payload(buf, len, chunk_offset, chunk_total);\n  if (n < 0) {\n    last_error = stream->get_error();\n    if (last_error == Error::Success) { last_error = Error::Read; }\n    eof = true;\n    return n;\n  }\n\n  if (n == 0) {\n    // Final chunk observed. Leave trailer parsing to the caller (StreamHandle).\n    eof = true;\n    return 0;\n  }\n\n  bytes_read += static_cast<size_t>(n);\n  if (payload_max_length > 0 && bytes_read > payload_max_length) {\n    last_error = Error::ExceedMaxPayloadSize;\n    eof = true;\n    return -1;\n  }\n  return n;\n}\n\n// ThreadPool implementation\ninline ThreadPool::ThreadPool(size_t n, size_t mqr)\n    : shutdown_(false), max_queued_requests_(mqr) {\n  threads_.reserve(n);\n  while (n) {\n    threads_.emplace_back(worker(*this));\n    n--;\n  }\n}\n\ninline bool ThreadPool::enqueue(std::function<void()> fn) {\n  {\n    std::unique_lock<std::mutex> lock(mutex_);\n    if (max_queued_requests_ > 0 && jobs_.size() >= max_queued_requests_) {\n      return false;\n    }\n    jobs_.push_back(std::move(fn));\n  }\n\n  cond_.notify_one();\n  return true;\n}\n\ninline void ThreadPool::shutdown() {\n  // Stop all worker threads...\n  {\n    std::unique_lock<std::mutex> lock(mutex_);\n    shutdown_ = true;\n  }\n\n  cond_.notify_all();\n\n  // Join...\n  for (auto &t : threads_) {\n    t.join();\n  }\n}\n\ninline ThreadPool::worker::worker(ThreadPool &pool) : pool_(pool) {}\n\ninline void ThreadPool::worker::operator()() {\n  for (;;) {\n    std::function<void()> fn;\n    {\n      std::unique_lock<std::mutex> lock(pool_.mutex_);\n\n      pool_.cond_.wait(lock,\n                       [&] { return !pool_.jobs_.empty() || pool_.shutdown_; });\n\n      if (pool_.shutdown_ && pool_.jobs_.empty()) { break; }\n\n      fn = pool_.jobs_.front();\n      pool_.jobs_.pop_front();\n    }\n\n    assert(true == static_cast<bool>(fn));\n    fn();\n  }\n\n#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && !defined(OPENSSL_IS_BORINGSSL) &&   \\\n    !defined(LIBRESSL_VERSION_NUMBER)\n  OPENSSL_thread_stop();\n#endif\n}\n\n/*\n * Group 1 (continued): detail namespace - Stream implementations\n */\n\nnamespace detail {\n\ninline void calc_actual_timeout(time_t max_timeout_msec, time_t duration_msec,\n                                time_t timeout_sec, time_t timeout_usec,\n                                time_t &actual_timeout_sec,\n                                time_t &actual_timeout_usec) {\n  auto timeout_msec = (timeout_sec * 1000) + (timeout_usec / 1000);\n\n  auto actual_timeout_msec =\n      (std::min)(max_timeout_msec - duration_msec, timeout_msec);\n\n  if (actual_timeout_msec < 0) { actual_timeout_msec = 0; }\n\n  actual_timeout_sec = actual_timeout_msec / 1000;\n  actual_timeout_usec = (actual_timeout_msec % 1000) * 1000;\n}\n\n// Socket stream implementation\ninline SocketStream::SocketStream(\n    socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec,\n    time_t write_timeout_sec, time_t write_timeout_usec,\n    time_t max_timeout_msec,\n    std::chrono::time_point<std::chrono::steady_clock> start_time)\n    : sock_(sock), read_timeout_sec_(read_timeout_sec),\n      read_timeout_usec_(read_timeout_usec),\n      write_timeout_sec_(write_timeout_sec),\n      write_timeout_usec_(write_timeout_usec),\n      max_timeout_msec_(max_timeout_msec), start_time_(start_time),\n      read_buff_(read_buff_size_, 0) {}\n\ninline SocketStream::~SocketStream() = default;\n\ninline bool SocketStream::is_readable() const {\n  return read_buff_off_ < read_buff_content_size_;\n}\n\ninline bool SocketStream::wait_readable() const {\n  if (max_timeout_msec_ <= 0) {\n    return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0;\n  }\n\n  time_t read_timeout_sec;\n  time_t read_timeout_usec;\n  calc_actual_timeout(max_timeout_msec_, duration(), read_timeout_sec_,\n                      read_timeout_usec_, read_timeout_sec, read_timeout_usec);\n\n  return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0;\n}\n\ninline bool SocketStream::wait_writable() const {\n  return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 &&\n         is_socket_alive(sock_);\n}\n\ninline ssize_t SocketStream::read(char *ptr, size_t size) {\n#ifdef _WIN32\n  size =\n      (std::min)(size, static_cast<size_t>((std::numeric_limits<int>::max)()));\n#else\n  size = (std::min)(size,\n                    static_cast<size_t>((std::numeric_limits<ssize_t>::max)()));\n#endif\n\n  if (read_buff_off_ < read_buff_content_size_) {\n    auto remaining_size = read_buff_content_size_ - read_buff_off_;\n    if (size <= remaining_size) {\n      memcpy(ptr, read_buff_.data() + read_buff_off_, size);\n      read_buff_off_ += size;\n      return static_cast<ssize_t>(size);\n    } else {\n      memcpy(ptr, read_buff_.data() + read_buff_off_, remaining_size);\n      read_buff_off_ += remaining_size;\n      return static_cast<ssize_t>(remaining_size);\n    }\n  }\n\n  if (!wait_readable()) {\n    error_ = Error::Timeout;\n    return -1;\n  }\n\n  read_buff_off_ = 0;\n  read_buff_content_size_ = 0;\n\n  if (size < read_buff_size_) {\n    auto n = read_socket(sock_, read_buff_.data(), read_buff_size_,\n                         CPPHTTPLIB_RECV_FLAGS);\n    if (n <= 0) {\n      if (n == 0) {\n        error_ = Error::ConnectionClosed;\n      } else {\n        error_ = Error::Read;\n      }\n      return n;\n    } else if (n <= static_cast<ssize_t>(size)) {\n      memcpy(ptr, read_buff_.data(), static_cast<size_t>(n));\n      return n;\n    } else {\n      memcpy(ptr, read_buff_.data(), size);\n      read_buff_off_ = size;\n      read_buff_content_size_ = static_cast<size_t>(n);\n      return static_cast<ssize_t>(size);\n    }\n  } else {\n    auto n = read_socket(sock_, ptr, size, CPPHTTPLIB_RECV_FLAGS);\n    if (n <= 0) {\n      if (n == 0) {\n        error_ = Error::ConnectionClosed;\n      } else {\n        error_ = Error::Read;\n      }\n    }\n    return n;\n  }\n}\n\ninline ssize_t SocketStream::write(const char *ptr, size_t size) {\n  if (!wait_writable()) { return -1; }\n\n#if defined(_WIN32) && !defined(_WIN64)\n  size =\n      (std::min)(size, static_cast<size_t>((std::numeric_limits<int>::max)()));\n#endif\n\n  return send_socket(sock_, ptr, size, CPPHTTPLIB_SEND_FLAGS);\n}\n\ninline void SocketStream::get_remote_ip_and_port(std::string &ip,\n                                                 int &port) const {\n  return detail::get_remote_ip_and_port(sock_, ip, port);\n}\n\ninline void SocketStream::get_local_ip_and_port(std::string &ip,\n                                                int &port) const {\n  return detail::get_local_ip_and_port(sock_, ip, port);\n}\n\ninline socket_t SocketStream::socket() const { return sock_; }\n\ninline time_t SocketStream::duration() const {\n  return std::chrono::duration_cast<std::chrono::milliseconds>(\n             std::chrono::steady_clock::now() - start_time_)\n      .count();\n}\n\n// Buffer stream implementation\ninline bool BufferStream::is_readable() const { return true; }\n\ninline bool BufferStream::wait_readable() const { return true; }\n\ninline bool BufferStream::wait_writable() const { return true; }\n\ninline ssize_t BufferStream::read(char *ptr, size_t size) {\n#if defined(_MSC_VER) && _MSC_VER < 1910\n  auto len_read = buffer._Copy_s(ptr, size, size, position);\n#else\n  auto len_read = buffer.copy(ptr, size, position);\n#endif\n  position += static_cast<size_t>(len_read);\n  return static_cast<ssize_t>(len_read);\n}\n\ninline ssize_t BufferStream::write(const char *ptr, size_t size) {\n  buffer.append(ptr, size);\n  return static_cast<ssize_t>(size);\n}\n\ninline void BufferStream::get_remote_ip_and_port(std::string & /*ip*/,\n                                                 int & /*port*/) const {}\n\ninline void BufferStream::get_local_ip_and_port(std::string & /*ip*/,\n                                                int & /*port*/) const {}\n\ninline socket_t BufferStream::socket() const { return 0; }\n\ninline time_t BufferStream::duration() const { return 0; }\n\ninline const std::string &BufferStream::get_buffer() const { return buffer; }\n\ninline PathParamsMatcher::PathParamsMatcher(const std::string &pattern)\n    : MatcherBase(pattern) {\n  constexpr const char marker[] = \"/:\";\n\n  // One past the last ending position of a path param substring\n  std::size_t last_param_end = 0;\n\n#ifndef CPPHTTPLIB_NO_EXCEPTIONS\n  // Needed to ensure that parameter names are unique during matcher\n  // construction\n  // If exceptions are disabled, only last duplicate path\n  // parameter will be set\n  std::unordered_set<std::string> param_name_set;\n#endif\n\n  while (true) {\n    const auto marker_pos = pattern.find(\n        marker, last_param_end == 0 ? last_param_end : last_param_end - 1);\n    if (marker_pos == std::string::npos) { break; }\n\n    static_fragments_.push_back(\n        pattern.substr(last_param_end, marker_pos - last_param_end + 1));\n\n    const auto param_name_start = marker_pos + str_len(marker);\n\n    auto sep_pos = pattern.find(separator, param_name_start);\n    if (sep_pos == std::string::npos) { sep_pos = pattern.length(); }\n\n    auto param_name =\n        pattern.substr(param_name_start, sep_pos - param_name_start);\n\n#ifndef CPPHTTPLIB_NO_EXCEPTIONS\n    if (param_name_set.find(param_name) != param_name_set.cend()) {\n      std::string msg = \"Encountered path parameter '\" + param_name +\n                        \"' multiple times in route pattern '\" + pattern + \"'.\";\n      throw std::invalid_argument(msg);\n    }\n#endif\n\n    param_names_.push_back(std::move(param_name));\n\n    last_param_end = sep_pos + 1;\n  }\n\n  if (last_param_end < pattern.length()) {\n    static_fragments_.push_back(pattern.substr(last_param_end));\n  }\n}\n\ninline bool PathParamsMatcher::match(Request &request) const {\n  request.matches = std::smatch();\n  request.path_params.clear();\n  request.path_params.reserve(param_names_.size());\n\n  // One past the position at which the path matched the pattern last time\n  std::size_t starting_pos = 0;\n  for (size_t i = 0; i < static_fragments_.size(); ++i) {\n    const auto &fragment = static_fragments_[i];\n\n    if (starting_pos + fragment.length() > request.path.length()) {\n      return false;\n    }\n\n    // Avoid unnecessary allocation by using strncmp instead of substr +\n    // comparison\n    if (std::strncmp(request.path.c_str() + starting_pos, fragment.c_str(),\n                     fragment.length()) != 0) {\n      return false;\n    }\n\n    starting_pos += fragment.length();\n\n    // Should only happen when we have a static fragment after a param\n    // Example: '/users/:id/subscriptions'\n    // The 'subscriptions' fragment here does not have a corresponding param\n    if (i >= param_names_.size()) { continue; }\n\n    auto sep_pos = request.path.find(separator, starting_pos);\n    if (sep_pos == std::string::npos) { sep_pos = request.path.length(); }\n\n    const auto &param_name = param_names_[i];\n\n    request.path_params.emplace(\n        param_name, request.path.substr(starting_pos, sep_pos - starting_pos));\n\n    // Mark everything up to '/' as matched\n    starting_pos = sep_pos + 1;\n  }\n  // Returns false if the path is longer than the pattern\n  return starting_pos >= request.path.length();\n}\n\ninline bool RegexMatcher::match(Request &request) const {\n  request.path_params.clear();\n  return std::regex_match(request.path, request.matches, regex_);\n}\n\n// Enclose IPv6 address in brackets if needed\ninline std::string prepare_host_string(const std::string &host) {\n  // Enclose IPv6 address in brackets (but not if already enclosed)\n  if (host.find(':') == std::string::npos ||\n      (!host.empty() && host[0] == '[')) {\n    // IPv4, hostname, or already bracketed IPv6\n    return host;\n  } else {\n    // IPv6 address without brackets\n    return \"[\" + host + \"]\";\n  }\n}\n\ninline std::string make_host_and_port_string(const std::string &host, int port,\n                                             bool is_ssl) {\n  auto result = prepare_host_string(host);\n\n  // Append port if not default\n  if ((!is_ssl && port == 80) || (is_ssl && port == 443)) {\n    ; // do nothing\n  } else {\n    result += \":\" + std::to_string(port);\n  }\n\n  return result;\n}\n\n// Create \"host:port\" string always including port number (for CONNECT method)\ninline std::string\nmake_host_and_port_string_always_port(const std::string &host, int port) {\n  return prepare_host_string(host) + \":\" + std::to_string(port);\n}\n\ntemplate <typename T>\ninline bool check_and_write_headers(Stream &strm, Headers &headers,\n                                    T header_writer, Error &error) {\n  for (const auto &h : headers) {\n    if (!detail::fields::is_field_name(h.first) ||\n        !detail::fields::is_field_value(h.second)) {\n      error = Error::InvalidHeaders;\n      return false;\n    }\n  }\n  if (header_writer(strm, headers) <= 0) {\n    error = Error::Write;\n    return false;\n  }\n  return true;\n}\n\n} // namespace detail\n\n/*\n * Group 2 (continued): detail namespace - SSLSocketStream implementation\n */\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\nnamespace detail {\n\n// SSL socket stream implementation\ninline SSLSocketStream::SSLSocketStream(\n    socket_t sock, tls::session_t session, time_t read_timeout_sec,\n    time_t read_timeout_usec, time_t write_timeout_sec,\n    time_t write_timeout_usec, time_t max_timeout_msec,\n    std::chrono::time_point<std::chrono::steady_clock> start_time)\n    : sock_(sock), session_(session), read_timeout_sec_(read_timeout_sec),\n      read_timeout_usec_(read_timeout_usec),\n      write_timeout_sec_(write_timeout_sec),\n      write_timeout_usec_(write_timeout_usec),\n      max_timeout_msec_(max_timeout_msec), start_time_(start_time) {\n#ifdef CPPHTTPLIB_OPENSSL_SUPPORT\n  // Clear AUTO_RETRY for proper non-blocking I/O timeout handling\n  // Note: create_session() also clears this, but SSLClient currently\n  // uses ssl_new() which does not. Until full TLS API migration is complete,\n  // we need to ensure AUTO_RETRY is cleared here regardless of how the\n  // SSL session was created.\n  SSL_clear_mode(static_cast<SSL *>(session), SSL_MODE_AUTO_RETRY);\n#endif\n}\n\ninline SSLSocketStream::~SSLSocketStream() = default;\n\ninline bool SSLSocketStream::is_readable() const {\n  return tls::pending(session_) > 0;\n}\n\ninline bool SSLSocketStream::wait_readable() const {\n  if (max_timeout_msec_ <= 0) {\n    return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0;\n  }\n\n  time_t read_timeout_sec;\n  time_t read_timeout_usec;\n  calc_actual_timeout(max_timeout_msec_, duration(), read_timeout_sec_,\n                      read_timeout_usec_, read_timeout_sec, read_timeout_usec);\n\n  return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0;\n}\n\ninline bool SSLSocketStream::wait_writable() const {\n  return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 &&\n         is_socket_alive(sock_) && !tls::is_peer_closed(session_, sock_);\n}\n\ninline ssize_t SSLSocketStream::read(char *ptr, size_t size) {\n  if (tls::pending(session_) > 0) {\n    tls::TlsError err;\n    auto ret = tls::read(session_, ptr, size, err);\n    if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) {\n      error_ = Error::ConnectionClosed;\n    }\n    return ret;\n  } else if (wait_readable()) {\n    tls::TlsError err;\n    auto ret = tls::read(session_, ptr, size, err);\n    if (ret < 0) {\n      auto n = 1000;\n#ifdef _WIN32\n      while (--n >= 0 && (err.code == tls::ErrorCode::WantRead ||\n                          (err.code == tls::ErrorCode::SyscallError &&\n                           WSAGetLastError() == WSAETIMEDOUT))) {\n#else\n      while (--n >= 0 && err.code == tls::ErrorCode::WantRead) {\n#endif\n        if (tls::pending(session_) > 0) {\n          return tls::read(session_, ptr, size, err);\n        } else if (wait_readable()) {\n          std::this_thread::sleep_for(std::chrono::microseconds{10});\n          ret = tls::read(session_, ptr, size, err);\n          if (ret >= 0) { return ret; }\n        } else {\n          break;\n        }\n      }\n      assert(ret < 0);\n    } else if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) {\n      error_ = Error::ConnectionClosed;\n    }\n    return ret;\n  } else {\n    error_ = Error::Timeout;\n    return -1;\n  }\n}\n\ninline ssize_t SSLSocketStream::write(const char *ptr, size_t size) {\n  if (wait_writable()) {\n    auto handle_size =\n        std::min<size_t>(size, (std::numeric_limits<int>::max)());\n\n    tls::TlsError err;\n    auto ret = tls::write(session_, ptr, handle_size, err);\n    if (ret < 0) {\n      auto n = 1000;\n#ifdef _WIN32\n      while (--n >= 0 && (err.code == tls::ErrorCode::WantWrite ||\n                          (err.code == tls::ErrorCode::SyscallError &&\n                           WSAGetLastError() == WSAETIMEDOUT))) {\n#else\n      while (--n >= 0 && err.code == tls::ErrorCode::WantWrite) {\n#endif\n        if (wait_writable()) {\n          std::this_thread::sleep_for(std::chrono::microseconds{10});\n          ret = tls::write(session_, ptr, handle_size, err);\n          if (ret >= 0) { return ret; }\n        } else {\n          break;\n        }\n      }\n      assert(ret < 0);\n    }\n    return ret;\n  }\n  return -1;\n}\n\ninline void SSLSocketStream::get_remote_ip_and_port(std::string &ip,\n                                                    int &port) const {\n  detail::get_remote_ip_and_port(sock_, ip, port);\n}\n\ninline void SSLSocketStream::get_local_ip_and_port(std::string &ip,\n                                                   int &port) const {\n  detail::get_local_ip_and_port(sock_, ip, port);\n}\n\ninline socket_t SSLSocketStream::socket() const { return sock_; }\n\ninline time_t SSLSocketStream::duration() const {\n  return std::chrono::duration_cast<std::chrono::milliseconds>(\n             std::chrono::steady_clock::now() - start_time_)\n      .count();\n}\n\n} // namespace detail\n#endif // CPPHTTPLIB_SSL_ENABLED\n\n/*\n * Group 4: Server implementation\n */\n\n// HTTP server implementation\ninline Server::Server()\n    : new_task_queue(\n          [] { return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT); }) {\n#ifndef _WIN32\n  signal(SIGPIPE, SIG_IGN);\n#endif\n}\n\ninline Server::~Server() = default;\n\ninline std::unique_ptr<detail::MatcherBase>\nServer::make_matcher(const std::string &pattern) {\n  if (pattern.find(\"/:\") != std::string::npos) {\n    return detail::make_unique<detail::PathParamsMatcher>(pattern);\n  } else {\n    return detail::make_unique<detail::RegexMatcher>(pattern);\n  }\n}\n\ninline Server &Server::Get(const std::string &pattern, Handler handler) {\n  get_handlers_.emplace_back(make_matcher(pattern), std::move(handler));\n  return *this;\n}\n\ninline Server &Server::Post(const std::string &pattern, Handler handler) {\n  post_handlers_.emplace_back(make_matcher(pattern), std::move(handler));\n  return *this;\n}\n\ninline Server &Server::Post(const std::string &pattern,\n                            HandlerWithContentReader handler) {\n  post_handlers_for_content_reader_.emplace_back(make_matcher(pattern),\n                                                 std::move(handler));\n  return *this;\n}\n\ninline Server &Server::Put(const std::string &pattern, Handler handler) {\n  put_handlers_.emplace_back(make_matcher(pattern), std::move(handler));\n  return *this;\n}\n\ninline Server &Server::Put(const std::string &pattern,\n                           HandlerWithContentReader handler) {\n  put_handlers_for_content_reader_.emplace_back(make_matcher(pattern),\n                                                std::move(handler));\n  return *this;\n}\n\ninline Server &Server::Patch(const std::string &pattern, Handler handler) {\n  patch_handlers_.emplace_back(make_matcher(pattern), std::move(handler));\n  return *this;\n}\n\ninline Server &Server::Patch(const std::string &pattern,\n                             HandlerWithContentReader handler) {\n  patch_handlers_for_content_reader_.emplace_back(make_matcher(pattern),\n                                                  std::move(handler));\n  return *this;\n}\n\ninline Server &Server::Delete(const std::string &pattern, Handler handler) {\n  delete_handlers_.emplace_back(make_matcher(pattern), std::move(handler));\n  return *this;\n}\n\ninline Server &Server::Delete(const std::string &pattern,\n                              HandlerWithContentReader handler) {\n  delete_handlers_for_content_reader_.emplace_back(make_matcher(pattern),\n                                                   std::move(handler));\n  return *this;\n}\n\ninline Server &Server::Options(const std::string &pattern, Handler handler) {\n  options_handlers_.emplace_back(make_matcher(pattern), std::move(handler));\n  return *this;\n}\n\ninline bool Server::set_base_dir(const std::string &dir,\n                                 const std::string &mount_point) {\n  return set_mount_point(mount_point, dir);\n}\n\ninline bool Server::set_mount_point(const std::string &mount_point,\n                                    const std::string &dir, Headers headers) {\n  detail::FileStat stat(dir);\n  if (stat.is_dir()) {\n    std::string mnt = !mount_point.empty() ? mount_point : \"/\";\n    if (!mnt.empty() && mnt[0] == '/') {\n      base_dirs_.push_back({std::move(mnt), dir, std::move(headers)});\n      return true;\n    }\n  }\n  return false;\n}\n\ninline bool Server::remove_mount_point(const std::string &mount_point) {\n  for (auto it = base_dirs_.begin(); it != base_dirs_.end(); ++it) {\n    if (it->mount_point == mount_point) {\n      base_dirs_.erase(it);\n      return true;\n    }\n  }\n  return false;\n}\n\ninline Server &\nServer::set_file_extension_and_mimetype_mapping(const std::string &ext,\n                                                const std::string &mime) {\n  file_extension_and_mimetype_map_[ext] = mime;\n  return *this;\n}\n\ninline Server &Server::set_default_file_mimetype(const std::string &mime) {\n  default_file_mimetype_ = mime;\n  return *this;\n}\n\ninline Server &Server::set_file_request_handler(Handler handler) {\n  file_request_handler_ = std::move(handler);\n  return *this;\n}\n\ninline Server &Server::set_error_handler_core(HandlerWithResponse handler,\n                                              std::true_type) {\n  error_handler_ = std::move(handler);\n  return *this;\n}\n\ninline Server &Server::set_error_handler_core(Handler handler,\n                                              std::false_type) {\n  error_handler_ = [handler](const Request &req, Response &res) {\n    handler(req, res);\n    return HandlerResponse::Handled;\n  };\n  return *this;\n}\n\ninline Server &Server::set_exception_handler(ExceptionHandler handler) {\n  exception_handler_ = std::move(handler);\n  return *this;\n}\n\ninline Server &Server::set_pre_routing_handler(HandlerWithResponse handler) {\n  pre_routing_handler_ = std::move(handler);\n  return *this;\n}\n\ninline Server &Server::set_post_routing_handler(Handler handler) {\n  post_routing_handler_ = std::move(handler);\n  return *this;\n}\n\ninline Server &Server::set_pre_request_handler(HandlerWithResponse handler) {\n  pre_request_handler_ = std::move(handler);\n  return *this;\n}\n\ninline Server &Server::set_logger(Logger logger) {\n  logger_ = std::move(logger);\n  return *this;\n}\n\ninline Server &Server::set_error_logger(ErrorLogger error_logger) {\n  error_logger_ = std::move(error_logger);\n  return *this;\n}\n\ninline Server &Server::set_pre_compression_logger(Logger logger) {\n  pre_compression_logger_ = std::move(logger);\n  return *this;\n}\n\ninline Server &\nServer::set_expect_100_continue_handler(Expect100ContinueHandler handler) {\n  expect_100_continue_handler_ = std::move(handler);\n  return *this;\n}\n\ninline Server &Server::set_address_family(int family) {\n  address_family_ = family;\n  return *this;\n}\n\ninline Server &Server::set_tcp_nodelay(bool on) {\n  tcp_nodelay_ = on;\n  return *this;\n}\n\ninline Server &Server::set_ipv6_v6only(bool on) {\n  ipv6_v6only_ = on;\n  return *this;\n}\n\ninline Server &Server::set_socket_options(SocketOptions socket_options) {\n  socket_options_ = std::move(socket_options);\n  return *this;\n}\n\ninline Server &Server::set_default_headers(Headers headers) {\n  default_headers_ = std::move(headers);\n  return *this;\n}\n\ninline Server &Server::set_header_writer(\n    std::function<ssize_t(Stream &, Headers &)> const &writer) {\n  header_writer_ = writer;\n  return *this;\n}\n\ninline Server &\nServer::set_trusted_proxies(const std::vector<std::string> &proxies) {\n  trusted_proxies_ = proxies;\n  return *this;\n}\n\ninline Server &Server::set_keep_alive_max_count(size_t count) {\n  keep_alive_max_count_ = count;\n  return *this;\n}\n\ninline Server &Server::set_keep_alive_timeout(time_t sec) {\n  keep_alive_timeout_sec_ = sec;\n  return *this;\n}\n\ninline Server &Server::set_read_timeout(time_t sec, time_t usec) {\n  read_timeout_sec_ = sec;\n  read_timeout_usec_ = usec;\n  return *this;\n}\n\ninline Server &Server::set_write_timeout(time_t sec, time_t usec) {\n  write_timeout_sec_ = sec;\n  write_timeout_usec_ = usec;\n  return *this;\n}\n\ninline Server &Server::set_idle_interval(time_t sec, time_t usec) {\n  idle_interval_sec_ = sec;\n  idle_interval_usec_ = usec;\n  return *this;\n}\n\ninline Server &Server::set_payload_max_length(size_t length) {\n  payload_max_length_ = length;\n  return *this;\n}\n\ninline bool Server::bind_to_port(const std::string &host, int port,\n                                 int socket_flags) {\n  auto ret = bind_internal(host, port, socket_flags);\n  if (ret == -1) { is_decommissioned = true; }\n  return ret >= 0;\n}\ninline int Server::bind_to_any_port(const std::string &host, int socket_flags) {\n  auto ret = bind_internal(host, 0, socket_flags);\n  if (ret == -1) { is_decommissioned = true; }\n  return ret;\n}\n\ninline bool Server::listen_after_bind() { return listen_internal(); }\n\ninline bool Server::listen(const std::string &host, int port,\n                           int socket_flags) {\n  return bind_to_port(host, port, socket_flags) && listen_internal();\n}\n\ninline bool Server::is_running() const { return is_running_; }\n\ninline void Server::wait_until_ready() const {\n  while (!is_running_ && !is_decommissioned) {\n    std::this_thread::sleep_for(std::chrono::milliseconds{1});\n  }\n}\n\ninline void Server::stop() {\n  if (is_running_) {\n    assert(svr_sock_ != INVALID_SOCKET);\n    std::atomic<socket_t> sock(svr_sock_.exchange(INVALID_SOCKET));\n    detail::shutdown_socket(sock);\n    detail::close_socket(sock);\n  }\n  is_decommissioned = false;\n}\n\ninline void Server::decommission() { is_decommissioned = true; }\n\ninline bool Server::parse_request_line(const char *s, Request &req) const {\n  auto len = strlen(s);\n  if (len < 2 || s[len - 2] != '\\r' || s[len - 1] != '\\n') { return false; }\n  len -= 2;\n\n  {\n    size_t count = 0;\n\n    detail::split(s, s + len, ' ', [&](const char *b, const char *e) {\n      switch (count) {\n      case 0: req.method = std::string(b, e); break;\n      case 1: req.target = std::string(b, e); break;\n      case 2: req.version = std::string(b, e); break;\n      default: break;\n      }\n      count++;\n    });\n\n    if (count != 3) { return false; }\n  }\n\n  thread_local const std::set<std::string> methods{\n      \"GET\",     \"HEAD\",    \"POST\",  \"PUT\",   \"DELETE\",\n      \"CONNECT\", \"OPTIONS\", \"TRACE\", \"PATCH\", \"PRI\"};\n\n  if (methods.find(req.method) == methods.end()) {\n    output_error_log(Error::InvalidHTTPMethod, &req);\n    return false;\n  }\n\n  if (req.version != \"HTTP/1.1\" && req.version != \"HTTP/1.0\") {\n    output_error_log(Error::InvalidHTTPVersion, &req);\n    return false;\n  }\n\n  {\n    // Skip URL fragment\n    for (size_t i = 0; i < req.target.size(); i++) {\n      if (req.target[i] == '#') {\n        req.target.erase(i);\n        break;\n      }\n    }\n\n    detail::divide(req.target, '?',\n                   [&](const char *lhs_data, std::size_t lhs_size,\n                       const char *rhs_data, std::size_t rhs_size) {\n                     req.path =\n                         decode_path_component(std::string(lhs_data, lhs_size));\n                     detail::parse_query_text(rhs_data, rhs_size, req.params);\n                   });\n  }\n\n  return true;\n}\n\ninline bool Server::write_response(Stream &strm, bool close_connection,\n                                   Request &req, Response &res) {\n  // NOTE: `req.ranges` should be empty, otherwise it will be applied\n  // incorrectly to the error content.\n  req.ranges.clear();\n  return write_response_core(strm, close_connection, req, res, false);\n}\n\ninline bool Server::write_response_with_content(Stream &strm,\n                                                bool close_connection,\n                                                const Request &req,\n                                                Response &res) {\n  return write_response_core(strm, close_connection, req, res, true);\n}\n\ninline bool Server::write_response_core(Stream &strm, bool close_connection,\n                                        const Request &req, Response &res,\n                                        bool need_apply_ranges) {\n  assert(res.status != -1);\n\n  if (400 <= res.status && error_handler_ &&\n      error_handler_(req, res) == HandlerResponse::Handled) {\n    need_apply_ranges = true;\n  }\n\n  std::string content_type;\n  std::string boundary;\n  if (need_apply_ranges) { apply_ranges(req, res, content_type, boundary); }\n\n  // Prepare additional headers\n  if (close_connection || req.get_header_value(\"Connection\") == \"close\" ||\n      400 <= res.status) { // Don't leave connections open after errors\n    res.set_header(\"Connection\", \"close\");\n  } else {\n    std::string s = \"timeout=\";\n    s += std::to_string(keep_alive_timeout_sec_);\n    s += \", max=\";\n    s += std::to_string(keep_alive_max_count_);\n    res.set_header(\"Keep-Alive\", s);\n  }\n\n  if ((!res.body.empty() || res.content_length_ > 0 || res.content_provider_) &&\n      !res.has_header(\"Content-Type\")) {\n    res.set_header(\"Content-Type\", \"text/plain\");\n  }\n\n  if (res.body.empty() && !res.content_length_ && !res.content_provider_ &&\n      !res.has_header(\"Content-Length\")) {\n    res.set_header(\"Content-Length\", \"0\");\n  }\n\n  if (req.method == \"HEAD\" && !res.has_header(\"Accept-Ranges\")) {\n    res.set_header(\"Accept-Ranges\", \"bytes\");\n  }\n\n  if (post_routing_handler_) { post_routing_handler_(req, res); }\n\n  // Response line and headers\n  {\n    detail::BufferStream bstrm;\n    if (!detail::write_response_line(bstrm, res.status)) { return false; }\n    if (header_writer_(bstrm, res.headers) <= 0) { return false; }\n\n    // Flush buffer\n    auto &data = bstrm.get_buffer();\n    detail::write_data(strm, data.data(), data.size());\n  }\n\n  // Body\n  auto ret = true;\n  if (req.method != \"HEAD\") {\n    if (!res.body.empty()) {\n      if (!detail::write_data(strm, res.body.data(), res.body.size())) {\n        ret = false;\n      }\n    } else if (res.content_provider_) {\n      if (write_content_with_provider(strm, req, res, boundary, content_type)) {\n        res.content_provider_success_ = true;\n      } else {\n        ret = false;\n      }\n    }\n  }\n\n  // Log\n  output_log(req, res);\n\n  return ret;\n}\n\ninline bool\nServer::write_content_with_provider(Stream &strm, const Request &req,\n                                    Response &res, const std::string &boundary,\n                                    const std::string &content_type) {\n  auto is_shutting_down = [this]() {\n    return this->svr_sock_ == INVALID_SOCKET;\n  };\n\n  if (res.content_length_ > 0) {\n    if (req.ranges.empty()) {\n      return detail::write_content(strm, res.content_provider_, 0,\n                                   res.content_length_, is_shutting_down);\n    } else if (req.ranges.size() == 1) {\n      auto offset_and_length = detail::get_range_offset_and_length(\n          req.ranges[0], res.content_length_);\n\n      return detail::write_content(strm, res.content_provider_,\n                                   offset_and_length.first,\n                                   offset_and_length.second, is_shutting_down);\n    } else {\n      return detail::write_multipart_ranges_data(\n          strm, req, res, boundary, content_type, res.content_length_,\n          is_shutting_down);\n    }\n  } else {\n    if (res.is_chunked_content_provider_) {\n      auto type = detail::encoding_type(req, res);\n\n      std::unique_ptr<detail::compressor> compressor;\n      if (type == detail::EncodingType::Gzip) {\n#ifdef CPPHTTPLIB_ZLIB_SUPPORT\n        compressor = detail::make_unique<detail::gzip_compressor>();\n#endif\n      } else if (type == detail::EncodingType::Brotli) {\n#ifdef CPPHTTPLIB_BROTLI_SUPPORT\n        compressor = detail::make_unique<detail::brotli_compressor>();\n#endif\n      } else if (type == detail::EncodingType::Zstd) {\n#ifdef CPPHTTPLIB_ZSTD_SUPPORT\n        compressor = detail::make_unique<detail::zstd_compressor>();\n#endif\n      } else {\n        compressor = detail::make_unique<detail::nocompressor>();\n      }\n      assert(compressor != nullptr);\n\n      return detail::write_content_chunked(strm, res.content_provider_,\n                                           is_shutting_down, *compressor);\n    } else {\n      return detail::write_content_without_length(strm, res.content_provider_,\n                                                  is_shutting_down);\n    }\n  }\n}\n\ninline bool Server::read_content(Stream &strm, Request &req, Response &res) {\n  FormFields::iterator cur_field;\n  FormFiles::iterator cur_file;\n  auto is_text_field = false;\n  size_t count = 0;\n  if (read_content_core(\n          strm, req, res,\n          // Regular\n          [&](const char *buf, size_t n) {\n            // Prevent arithmetic overflow when checking sizes.\n            // Avoid computing (req.body.size() + n) directly because\n            // adding two unsigned `size_t` values can wrap around and\n            // produce a small result instead of indicating overflow.\n            // Instead, check using subtraction: ensure `n` does not\n            // exceed the remaining capacity `max_size() - size()`.\n            if (req.body.size() >= req.body.max_size() ||\n                n > req.body.max_size() - req.body.size()) {\n              return false;\n            }\n\n            // Limit decompressed body size to payload_max_length_ to protect\n            // against \"zip bomb\" attacks where a small compressed payload\n            // decompresses to a massive size.\n            if (payload_max_length_ > 0 &&\n                (req.body.size() >= payload_max_length_ ||\n                 n > payload_max_length_ - req.body.size())) {\n              return false;\n            }\n\n            req.body.append(buf, n);\n            return true;\n          },\n          // Multipart FormData\n          [&](const FormData &file) {\n            if (count++ == CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT) {\n              output_error_log(Error::TooManyFormDataFiles, &req);\n              return false;\n            }\n\n            if (file.filename.empty()) {\n              cur_field = req.form.fields.emplace(\n                  file.name, FormField{file.name, file.content, file.headers});\n              is_text_field = true;\n            } else {\n              cur_file = req.form.files.emplace(file.name, file);\n              is_text_field = false;\n            }\n            return true;\n          },\n          [&](const char *buf, size_t n) {\n            if (is_text_field) {\n              auto &content = cur_field->second.content;\n              if (content.size() + n > content.max_size()) { return false; }\n              content.append(buf, n);\n            } else {\n              auto &content = cur_file->second.content;\n              if (content.size() + n > content.max_size()) { return false; }\n              content.append(buf, n);\n            }\n            return true;\n          })) {\n    const auto &content_type = req.get_header_value(\"Content-Type\");\n    if (!content_type.find(\"application/x-www-form-urlencoded\")) {\n      if (req.body.size() > CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH) {\n        res.status = StatusCode::PayloadTooLarge_413; // NOTE: should be 414?\n        output_error_log(Error::ExceedMaxPayloadSize, &req);\n        return false;\n      }\n      detail::parse_query_text(req.body, req.params);\n    }\n    return true;\n  }\n  return false;\n}\n\ninline bool Server::read_content_with_content_receiver(\n    Stream &strm, Request &req, Response &res, ContentReceiver receiver,\n    FormDataHeader multipart_header, ContentReceiver multipart_receiver) {\n  return read_content_core(strm, req, res, std::move(receiver),\n                           std::move(multipart_header),\n                           std::move(multipart_receiver));\n}\n\ninline bool Server::read_content_core(\n    Stream &strm, Request &req, Response &res, ContentReceiver receiver,\n    FormDataHeader multipart_header, ContentReceiver multipart_receiver) const {\n  detail::FormDataParser multipart_form_data_parser;\n  ContentReceiverWithProgress out;\n\n  if (req.is_multipart_form_data()) {\n    const auto &content_type = req.get_header_value(\"Content-Type\");\n    std::string boundary;\n    if (!detail::parse_multipart_boundary(content_type, boundary)) {\n      res.status = StatusCode::BadRequest_400;\n      output_error_log(Error::MultipartParsing, &req);\n      return false;\n    }\n\n    multipart_form_data_parser.set_boundary(std::move(boundary));\n    out = [&](const char *buf, size_t n, size_t /*off*/, size_t /*len*/) {\n      return multipart_form_data_parser.parse(buf, n, multipart_header,\n                                              multipart_receiver);\n    };\n  } else {\n    out = [receiver](const char *buf, size_t n, size_t /*off*/,\n                     size_t /*len*/) { return receiver(buf, n); };\n  }\n\n  // RFC 7230 Section 3.3.3: If this is a request message and none of the above\n  // are true (no Transfer-Encoding and no Content-Length), then the message\n  // body length is zero (no message body is present).\n  //\n  // For non-SSL builds, detect clients that send a body without a\n  // Content-Length header (raw HTTP over TCP). Check both the stream's\n  // internal read buffer (data already read from the socket during header\n  // parsing) and the socket itself for pending data. If data is found and\n  // exceeds the configured payload limit, reject with 413.\n  // For SSL builds we cannot reliably peek the decrypted application bytes,\n  // so keep the original behaviour.\n#if !defined(CPPHTTPLIB_SSL_ENABLED)\n  if (!req.has_header(\"Content-Length\") &&\n      !detail::is_chunked_transfer_encoding(req.headers)) {\n    // Only check if payload_max_length is set to a finite value\n    if (payload_max_length_ > 0 &&\n        payload_max_length_ < (std::numeric_limits<size_t>::max)()) {\n      // Check if there is data already buffered in the stream (read during\n      // header parsing) or pending on the socket. Use a non-blocking socket\n      // check to avoid deadlock when the client sends no body.\n      bool has_data = strm.is_readable();\n      if (!has_data) {\n        socket_t s = strm.socket();\n        if (s != INVALID_SOCKET) {\n          has_data = detail::select_read(s, 0, 0) > 0;\n        }\n      }\n      if (has_data) {\n        auto result =\n            detail::read_content_without_length(strm, payload_max_length_, out);\n        if (result == detail::ReadContentResult::PayloadTooLarge) {\n          res.status = StatusCode::PayloadTooLarge_413;\n          return false;\n        } else if (result != detail::ReadContentResult::Success) {\n          return false;\n        }\n        return true;\n      }\n    }\n    return true;\n  }\n#else\n  if (!req.has_header(\"Content-Length\") &&\n      !detail::is_chunked_transfer_encoding(req.headers)) {\n    return true;\n  }\n#endif\n\n  if (!detail::read_content(strm, req, payload_max_length_, res.status, nullptr,\n                            out, true)) {\n    return false;\n  }\n\n  if (req.is_multipart_form_data()) {\n    if (!multipart_form_data_parser.is_valid()) {\n      res.status = StatusCode::BadRequest_400;\n      output_error_log(Error::MultipartParsing, &req);\n      return false;\n    }\n  }\n\n  return true;\n}\n\ninline bool Server::handle_file_request(Request &req, Response &res) {\n  for (const auto &entry : base_dirs_) {\n    // Prefix match\n    if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) {\n      std::string sub_path = \"/\" + req.path.substr(entry.mount_point.size());\n      if (detail::is_valid_path(sub_path)) {\n        auto path = entry.base_dir + sub_path;\n        if (path.back() == '/') { path += \"index.html\"; }\n\n        detail::FileStat stat(path);\n\n        if (stat.is_dir()) {\n          res.set_redirect(sub_path + \"/\", StatusCode::MovedPermanently_301);\n          return true;\n        }\n\n        if (stat.is_file()) {\n          for (const auto &kv : entry.headers) {\n            res.set_header(kv.first, kv.second);\n          }\n\n          auto etag = detail::compute_etag(stat);\n          if (!etag.empty()) { res.set_header(\"ETag\", etag); }\n\n          auto mtime = stat.mtime();\n\n          auto last_modified = detail::file_mtime_to_http_date(mtime);\n          if (!last_modified.empty()) {\n            res.set_header(\"Last-Modified\", last_modified);\n          }\n\n          if (check_if_not_modified(req, res, etag, mtime)) { return true; }\n\n          check_if_range(req, etag, mtime);\n\n          auto mm = std::make_shared<detail::mmap>(path.c_str());\n          if (!mm->is_open()) {\n            output_error_log(Error::OpenFile, &req);\n            return false;\n          }\n\n          res.set_content_provider(\n              mm->size(),\n              detail::find_content_type(path, file_extension_and_mimetype_map_,\n                                        default_file_mimetype_),\n              [mm](size_t offset, size_t length, DataSink &sink) -> bool {\n                sink.write(mm->data() + offset, length);\n                return true;\n              });\n\n          if (req.method != \"HEAD\" && file_request_handler_) {\n            file_request_handler_(req, res);\n          }\n\n          return true;\n        } else {\n          output_error_log(Error::OpenFile, &req);\n        }\n      }\n    }\n  }\n  return false;\n}\n\ninline bool Server::check_if_not_modified(const Request &req, Response &res,\n                                          const std::string &etag,\n                                          time_t mtime) const {\n  // Handle conditional GET:\n  // 1. If-None-Match takes precedence (RFC 9110 Section 13.1.2)\n  // 2. If-Modified-Since is checked only when If-None-Match is absent\n  if (req.has_header(\"If-None-Match\")) {\n    if (!etag.empty()) {\n      auto val = req.get_header_value(\"If-None-Match\");\n\n      // NOTE: We use exact string matching here. This works correctly\n      // because our server always generates weak ETags (W/\"...\"), and\n      // clients typically send back the same ETag they received.\n      // RFC 9110 Section 8.8.3.2 allows weak comparison for\n      // If-None-Match, where W/\"x\" and \"x\" would match, but this\n      // simplified implementation requires exact matches.\n      auto ret = detail::split_find(val.data(), val.data() + val.size(), ',',\n                                    [&](const char *b, const char *e) {\n                                      auto seg_len = static_cast<size_t>(e - b);\n                                      return (seg_len == 1 && *b == '*') ||\n                                             (seg_len == etag.size() &&\n                                              std::equal(b, e, etag.begin()));\n                                    });\n\n      if (ret) {\n        res.status = StatusCode::NotModified_304;\n        return true;\n      }\n    }\n  } else if (req.has_header(\"If-Modified-Since\")) {\n    auto val = req.get_header_value(\"If-Modified-Since\");\n    auto t = detail::parse_http_date(val);\n\n    if (t != static_cast<time_t>(-1) && mtime <= t) {\n      res.status = StatusCode::NotModified_304;\n      return true;\n    }\n  }\n  return false;\n}\n\ninline bool Server::check_if_range(Request &req, const std::string &etag,\n                                   time_t mtime) const {\n  // Handle If-Range for partial content requests (RFC 9110\n  // Section 13.1.5). If-Range is only evaluated when Range header is\n  // present. If the validator matches, serve partial content; otherwise\n  // serve full content.\n  if (!req.ranges.empty() && req.has_header(\"If-Range\")) {\n    auto val = req.get_header_value(\"If-Range\");\n\n    auto is_valid_range = [&]() {\n      if (detail::is_strong_etag(val)) {\n        // RFC 9110 Section 13.1.5: If-Range requires strong ETag\n        // comparison.\n        return (!etag.empty() && val == etag);\n      } else if (detail::is_weak_etag(val)) {\n        // Weak ETags are not valid for If-Range (RFC 9110 Section 13.1.5)\n        return false;\n      } else {\n        // HTTP-date comparison\n        auto t = detail::parse_http_date(val);\n        return (t != static_cast<time_t>(-1) && mtime <= t);\n      }\n    };\n\n    if (!is_valid_range()) {\n      // Validator doesn't match: ignore Range and serve full content\n      req.ranges.clear();\n      return false;\n    }\n  }\n\n  return true;\n}\n\ninline socket_t\nServer::create_server_socket(const std::string &host, int port,\n                             int socket_flags,\n                             SocketOptions socket_options) const {\n  return detail::create_socket(\n      host, std::string(), port, address_family_, socket_flags, tcp_nodelay_,\n      ipv6_v6only_, std::move(socket_options),\n      [&](socket_t sock, struct addrinfo &ai, bool & /*quit*/) -> bool {\n        if (::bind(sock, ai.ai_addr, static_cast<socklen_t>(ai.ai_addrlen))) {\n          output_error_log(Error::BindIPAddress, nullptr);\n          return false;\n        }\n        if (::listen(sock, CPPHTTPLIB_LISTEN_BACKLOG)) {\n          output_error_log(Error::Listen, nullptr);\n          return false;\n        }\n        return true;\n      });\n}\n\ninline int Server::bind_internal(const std::string &host, int port,\n                                 int socket_flags) {\n  if (is_decommissioned) { return -1; }\n\n  if (!is_valid()) { return -1; }\n\n  svr_sock_ = create_server_socket(host, port, socket_flags, socket_options_);\n  if (svr_sock_ == INVALID_SOCKET) { return -1; }\n\n  if (port == 0) {\n    struct sockaddr_storage addr;\n    socklen_t addr_len = sizeof(addr);\n    if (getsockname(svr_sock_, reinterpret_cast<struct sockaddr *>(&addr),\n                    &addr_len) == -1) {\n      output_error_log(Error::GetSockName, nullptr);\n      return -1;\n    }\n    if (addr.ss_family == AF_INET) {\n      return ntohs(reinterpret_cast<struct sockaddr_in *>(&addr)->sin_port);\n    } else if (addr.ss_family == AF_INET6) {\n      return ntohs(reinterpret_cast<struct sockaddr_in6 *>(&addr)->sin6_port);\n    } else {\n      output_error_log(Error::UnsupportedAddressFamily, nullptr);\n      return -1;\n    }\n  } else {\n    return port;\n  }\n}\n\ninline bool Server::listen_internal() {\n  if (is_decommissioned) { return false; }\n\n  auto ret = true;\n  is_running_ = true;\n  auto se = detail::scope_exit([&]() { is_running_ = false; });\n\n  {\n    std::unique_ptr<TaskQueue> task_queue(new_task_queue());\n\n    while (svr_sock_ != INVALID_SOCKET) {\n#ifndef _WIN32\n      if (idle_interval_sec_ > 0 || idle_interval_usec_ > 0) {\n#endif\n        auto val = detail::select_read(svr_sock_, idle_interval_sec_,\n                                       idle_interval_usec_);\n        if (val == 0) { // Timeout\n          task_queue->on_idle();\n          continue;\n        }\n#ifndef _WIN32\n      }\n#endif\n\n#if defined _WIN32\n      // sockets connected via WASAccept inherit flags NO_HANDLE_INHERIT,\n      // OVERLAPPED\n      socket_t sock = WSAAccept(svr_sock_, nullptr, nullptr, nullptr, 0);\n#elif defined SOCK_CLOEXEC\n      socket_t sock = accept4(svr_sock_, nullptr, nullptr, SOCK_CLOEXEC);\n#else\n      socket_t sock = accept(svr_sock_, nullptr, nullptr);\n#endif\n\n      if (sock == INVALID_SOCKET) {\n        if (errno == EMFILE) {\n          // The per-process limit of open file descriptors has been reached.\n          // Try to accept new connections after a short sleep.\n          std::this_thread::sleep_for(std::chrono::microseconds{1});\n          continue;\n        } else if (errno == EINTR || errno == EAGAIN) {\n          continue;\n        }\n        if (svr_sock_ != INVALID_SOCKET) {\n          detail::close_socket(svr_sock_);\n          ret = false;\n          output_error_log(Error::Connection, nullptr);\n        } else {\n          ; // The server socket was closed by user.\n        }\n        break;\n      }\n\n      detail::set_socket_opt_time(sock, SOL_SOCKET, SO_RCVTIMEO,\n                                  read_timeout_sec_, read_timeout_usec_);\n      detail::set_socket_opt_time(sock, SOL_SOCKET, SO_SNDTIMEO,\n                                  write_timeout_sec_, write_timeout_usec_);\n\n      if (!task_queue->enqueue(\n              [this, sock]() { process_and_close_socket(sock); })) {\n        output_error_log(Error::ResourceExhaustion, nullptr);\n        detail::shutdown_socket(sock);\n        detail::close_socket(sock);\n      }\n    }\n\n    task_queue->shutdown();\n  }\n\n  is_decommissioned = !ret;\n  return ret;\n}\n\ninline bool Server::routing(Request &req, Response &res, Stream &strm) {\n  if (pre_routing_handler_ &&\n      pre_routing_handler_(req, res) == HandlerResponse::Handled) {\n    return true;\n  }\n\n  // File handler\n  if ((req.method == \"GET\" || req.method == \"HEAD\") &&\n      handle_file_request(req, res)) {\n    return true;\n  }\n\n  if (detail::expect_content(req)) {\n    // Content reader handler\n    {\n      ContentReader reader(\n          [&](ContentReceiver receiver) {\n            auto result = read_content_with_content_receiver(\n                strm, req, res, std::move(receiver), nullptr, nullptr);\n            if (!result) { output_error_log(Error::Read, &req); }\n            return result;\n          },\n          [&](FormDataHeader header, ContentReceiver receiver) {\n            auto result = read_content_with_content_receiver(\n                strm, req, res, nullptr, std::move(header),\n                std::move(receiver));\n            if (!result) { output_error_log(Error::Read, &req); }\n            return result;\n          });\n\n      if (req.method == \"POST\") {\n        if (dispatch_request_for_content_reader(\n                req, res, std::move(reader),\n                post_handlers_for_content_reader_)) {\n          return true;\n        }\n      } else if (req.method == \"PUT\") {\n        if (dispatch_request_for_content_reader(\n                req, res, std::move(reader),\n                put_handlers_for_content_reader_)) {\n          return true;\n        }\n      } else if (req.method == \"PATCH\") {\n        if (dispatch_request_for_content_reader(\n                req, res, std::move(reader),\n                patch_handlers_for_content_reader_)) {\n          return true;\n        }\n      } else if (req.method == \"DELETE\") {\n        if (dispatch_request_for_content_reader(\n                req, res, std::move(reader),\n                delete_handlers_for_content_reader_)) {\n          return true;\n        }\n      }\n    }\n\n    // Read content into `req.body`\n    if (!read_content(strm, req, res)) {\n      output_error_log(Error::Read, &req);\n      return false;\n    }\n  }\n\n  // Regular handler\n  if (req.method == \"GET\" || req.method == \"HEAD\") {\n    return dispatch_request(req, res, get_handlers_);\n  } else if (req.method == \"POST\") {\n    return dispatch_request(req, res, post_handlers_);\n  } else if (req.method == \"PUT\") {\n    return dispatch_request(req, res, put_handlers_);\n  } else if (req.method == \"DELETE\") {\n    return dispatch_request(req, res, delete_handlers_);\n  } else if (req.method == \"OPTIONS\") {\n    return dispatch_request(req, res, options_handlers_);\n  } else if (req.method == \"PATCH\") {\n    return dispatch_request(req, res, patch_handlers_);\n  }\n\n  res.status = StatusCode::BadRequest_400;\n  return false;\n}\n\ninline bool Server::dispatch_request(Request &req, Response &res,\n                                     const Handlers &handlers) const {\n  for (const auto &x : handlers) {\n    const auto &matcher = x.first;\n    const auto &handler = x.second;\n\n    if (matcher->match(req)) {\n      req.matched_route = matcher->pattern();\n      if (!pre_request_handler_ ||\n          pre_request_handler_(req, res) != HandlerResponse::Handled) {\n        handler(req, res);\n      }\n      return true;\n    }\n  }\n  return false;\n}\n\ninline void Server::apply_ranges(const Request &req, Response &res,\n                                 std::string &content_type,\n                                 std::string &boundary) const {\n  if (req.ranges.size() > 1 && res.status == StatusCode::PartialContent_206) {\n    auto it = res.headers.find(\"Content-Type\");\n    if (it != res.headers.end()) {\n      content_type = it->second;\n      res.headers.erase(it);\n    }\n\n    boundary = detail::make_multipart_data_boundary();\n\n    res.set_header(\"Content-Type\",\n                   \"multipart/byteranges; boundary=\" + boundary);\n  }\n\n  auto type = detail::encoding_type(req, res);\n\n  if (res.body.empty()) {\n    if (res.content_length_ > 0) {\n      size_t length = 0;\n      if (req.ranges.empty() || res.status != StatusCode::PartialContent_206) {\n        length = res.content_length_;\n      } else if (req.ranges.size() == 1) {\n        auto offset_and_length = detail::get_range_offset_and_length(\n            req.ranges[0], res.content_length_);\n\n        length = offset_and_length.second;\n\n        auto content_range = detail::make_content_range_header_field(\n            offset_and_length, res.content_length_);\n        res.set_header(\"Content-Range\", content_range);\n      } else {\n        length = detail::get_multipart_ranges_data_length(\n            req, boundary, content_type, res.content_length_);\n      }\n      res.set_header(\"Content-Length\", std::to_string(length));\n    } else {\n      if (res.content_provider_) {\n        if (res.is_chunked_content_provider_) {\n          res.set_header(\"Transfer-Encoding\", \"chunked\");\n          if (type == detail::EncodingType::Gzip) {\n            res.set_header(\"Content-Encoding\", \"gzip\");\n            res.set_header(\"Vary\", \"Accept-Encoding\");\n          } else if (type == detail::EncodingType::Brotli) {\n            res.set_header(\"Content-Encoding\", \"br\");\n            res.set_header(\"Vary\", \"Accept-Encoding\");\n          } else if (type == detail::EncodingType::Zstd) {\n            res.set_header(\"Content-Encoding\", \"zstd\");\n            res.set_header(\"Vary\", \"Accept-Encoding\");\n          }\n        }\n      }\n    }\n  } else {\n    if (req.ranges.empty() || res.status != StatusCode::PartialContent_206) {\n      ;\n    } else if (req.ranges.size() == 1) {\n      auto offset_and_length =\n          detail::get_range_offset_and_length(req.ranges[0], res.body.size());\n      auto offset = offset_and_length.first;\n      auto length = offset_and_length.second;\n\n      auto content_range = detail::make_content_range_header_field(\n          offset_and_length, res.body.size());\n      res.set_header(\"Content-Range\", content_range);\n\n      assert(offset + length <= res.body.size());\n      res.body = res.body.substr(offset, length);\n    } else {\n      std::string data;\n      detail::make_multipart_ranges_data(req, res, boundary, content_type,\n                                         res.body.size(), data);\n      res.body.swap(data);\n    }\n\n    if (type != detail::EncodingType::None) {\n      output_pre_compression_log(req, res);\n\n      std::unique_ptr<detail::compressor> compressor;\n      std::string content_encoding;\n\n      if (type == detail::EncodingType::Gzip) {\n#ifdef CPPHTTPLIB_ZLIB_SUPPORT\n        compressor = detail::make_unique<detail::gzip_compressor>();\n        content_encoding = \"gzip\";\n#endif\n      } else if (type == detail::EncodingType::Brotli) {\n#ifdef CPPHTTPLIB_BROTLI_SUPPORT\n        compressor = detail::make_unique<detail::brotli_compressor>();\n        content_encoding = \"br\";\n#endif\n      } else if (type == detail::EncodingType::Zstd) {\n#ifdef CPPHTTPLIB_ZSTD_SUPPORT\n        compressor = detail::make_unique<detail::zstd_compressor>();\n        content_encoding = \"zstd\";\n#endif\n      }\n\n      if (compressor) {\n        std::string compressed;\n        if (compressor->compress(res.body.data(), res.body.size(), true,\n                                 [&](const char *data, size_t data_len) {\n                                   compressed.append(data, data_len);\n                                   return true;\n                                 })) {\n          res.body.swap(compressed);\n          res.set_header(\"Content-Encoding\", content_encoding);\n          res.set_header(\"Vary\", \"Accept-Encoding\");\n        }\n      }\n    }\n\n    auto length = std::to_string(res.body.size());\n    res.set_header(\"Content-Length\", length);\n  }\n}\n\ninline bool Server::dispatch_request_for_content_reader(\n    Request &req, Response &res, ContentReader content_reader,\n    const HandlersForContentReader &handlers) const {\n  for (const auto &x : handlers) {\n    const auto &matcher = x.first;\n    const auto &handler = x.second;\n\n    if (matcher->match(req)) {\n      req.matched_route = matcher->pattern();\n      if (!pre_request_handler_ ||\n          pre_request_handler_(req, res) != HandlerResponse::Handled) {\n        handler(req, res, content_reader);\n      }\n      return true;\n    }\n  }\n  return false;\n}\n\ninline std::string\nget_client_ip(const std::string &x_forwarded_for,\n              const std::vector<std::string> &trusted_proxies) {\n  // X-Forwarded-For is a comma-separated list per RFC 7239\n  std::vector<std::string> ip_list;\n  detail::split(x_forwarded_for.data(),\n                x_forwarded_for.data() + x_forwarded_for.size(), ',',\n                [&](const char *b, const char *e) {\n                  auto r = detail::trim(b, e, 0, static_cast<size_t>(e - b));\n                  ip_list.emplace_back(std::string(b + r.first, b + r.second));\n                });\n\n  for (size_t i = 0; i < ip_list.size(); ++i) {\n    auto ip = ip_list[i];\n\n    auto is_trusted_proxy =\n        std::any_of(trusted_proxies.begin(), trusted_proxies.end(),\n                    [&](const std::string &proxy) { return ip == proxy; });\n\n    if (is_trusted_proxy) {\n      if (i == 0) {\n        // If the trusted proxy is the first IP, there's no preceding client IP\n        return ip;\n      } else {\n        // Return the IP immediately before the trusted proxy\n        return ip_list[i - 1];\n      }\n    }\n  }\n\n  // If no trusted proxy is found, return the first IP in the list\n  return ip_list.front();\n}\n\ninline bool\nServer::process_request(Stream &strm, const std::string &remote_addr,\n                        int remote_port, const std::string &local_addr,\n                        int local_port, bool close_connection,\n                        bool &connection_closed,\n                        const std::function<void(Request &)> &setup_request) {\n  std::array<char, 2048> buf{};\n\n  detail::stream_line_reader line_reader(strm, buf.data(), buf.size());\n\n  // Connection has been closed on client\n  if (!line_reader.getline()) { return false; }\n\n  Request req;\n  req.start_time_ = std::chrono::steady_clock::now();\n  req.remote_addr = remote_addr;\n  req.remote_port = remote_port;\n  req.local_addr = local_addr;\n  req.local_port = local_port;\n\n  Response res;\n  res.version = \"HTTP/1.1\";\n  res.headers = default_headers_;\n\n#ifdef __APPLE__\n  // Socket file descriptor exceeded FD_SETSIZE...\n  if (strm.socket() >= FD_SETSIZE) {\n    Headers dummy;\n    detail::read_headers(strm, dummy);\n    res.status = StatusCode::InternalServerError_500;\n    output_error_log(Error::ExceedMaxSocketDescriptorCount, &req);\n    return write_response(strm, close_connection, req, res);\n  }\n#endif\n\n  // Request line and headers\n  if (!parse_request_line(line_reader.ptr(), req)) {\n    res.status = StatusCode::BadRequest_400;\n    output_error_log(Error::InvalidRequestLine, &req);\n    return write_response(strm, close_connection, req, res);\n  }\n\n  // Request headers\n  if (!detail::read_headers(strm, req.headers)) {\n    res.status = StatusCode::BadRequest_400;\n    output_error_log(Error::InvalidHeaders, &req);\n    return write_response(strm, close_connection, req, res);\n  }\n\n  // Check if the request URI doesn't exceed the limit\n  if (req.target.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {\n    res.status = StatusCode::UriTooLong_414;\n    output_error_log(Error::ExceedUriMaxLength, &req);\n    return write_response(strm, close_connection, req, res);\n  }\n\n  if (req.get_header_value(\"Connection\") == \"close\") {\n    connection_closed = true;\n  }\n\n  if (req.version == \"HTTP/1.0\" &&\n      req.get_header_value(\"Connection\") != \"Keep-Alive\") {\n    connection_closed = true;\n  }\n\n  if (!trusted_proxies_.empty() && req.has_header(\"X-Forwarded-For\")) {\n    auto x_forwarded_for = req.get_header_value(\"X-Forwarded-For\");\n    req.remote_addr = get_client_ip(x_forwarded_for, trusted_proxies_);\n  } else {\n    req.remote_addr = remote_addr;\n  }\n  req.remote_port = remote_port;\n\n  req.local_addr = local_addr;\n  req.local_port = local_port;\n\n  if (req.has_header(\"Accept\")) {\n    const auto &accept_header = req.get_header_value(\"Accept\");\n    if (!detail::parse_accept_header(accept_header, req.accept_content_types)) {\n      res.status = StatusCode::BadRequest_400;\n      output_error_log(Error::HTTPParsing, &req);\n      return write_response(strm, close_connection, req, res);\n    }\n  }\n\n  if (req.has_header(\"Range\")) {\n    const auto &range_header_value = req.get_header_value(\"Range\");\n    if (!detail::parse_range_header(range_header_value, req.ranges)) {\n      res.status = StatusCode::RangeNotSatisfiable_416;\n      output_error_log(Error::InvalidRangeHeader, &req);\n      return write_response(strm, close_connection, req, res);\n    }\n  }\n\n  if (setup_request) { setup_request(req); }\n\n  if (req.get_header_value(\"Expect\") == \"100-continue\") {\n    int status = StatusCode::Continue_100;\n    if (expect_100_continue_handler_) {\n      status = expect_100_continue_handler_(req, res);\n    }\n    switch (status) {\n    case StatusCode::Continue_100:\n    case StatusCode::ExpectationFailed_417:\n      detail::write_response_line(strm, status);\n      strm.write(\"\\r\\n\");\n      break;\n    default:\n      connection_closed = true;\n      return write_response(strm, true, req, res);\n    }\n  }\n\n  // Setup `is_connection_closed` method\n  auto sock = strm.socket();\n  req.is_connection_closed = [sock]() {\n    return !detail::is_socket_alive(sock);\n  };\n\n  // Routing\n  auto routed = false;\n#ifdef CPPHTTPLIB_NO_EXCEPTIONS\n  routed = routing(req, res, strm);\n#else\n  try {\n    routed = routing(req, res, strm);\n  } catch (std::exception &e) {\n    if (exception_handler_) {\n      auto ep = std::current_exception();\n      exception_handler_(req, res, ep);\n      routed = true;\n    } else {\n      res.status = StatusCode::InternalServerError_500;\n      std::string val;\n      auto s = e.what();\n      for (size_t i = 0; s[i]; i++) {\n        switch (s[i]) {\n        case '\\r': val += \"\\\\r\"; break;\n        case '\\n': val += \"\\\\n\"; break;\n        default: val += s[i]; break;\n        }\n      }\n      res.set_header(\"EXCEPTION_WHAT\", val);\n    }\n  } catch (...) {\n    if (exception_handler_) {\n      auto ep = std::current_exception();\n      exception_handler_(req, res, ep);\n      routed = true;\n    } else {\n      res.status = StatusCode::InternalServerError_500;\n      res.set_header(\"EXCEPTION_WHAT\", \"UNKNOWN\");\n    }\n  }\n#endif\n  if (routed) {\n    if (res.status == -1) {\n      res.status = req.ranges.empty() ? StatusCode::OK_200\n                                      : StatusCode::PartialContent_206;\n    }\n\n    // Serve file content by using a content provider\n    if (!res.file_content_path_.empty()) {\n      const auto &path = res.file_content_path_;\n      auto mm = std::make_shared<detail::mmap>(path.c_str());\n      if (!mm->is_open()) {\n        res.body.clear();\n        res.content_length_ = 0;\n        res.content_provider_ = nullptr;\n        res.status = StatusCode::NotFound_404;\n        output_error_log(Error::OpenFile, &req);\n        return write_response(strm, close_connection, req, res);\n      }\n\n      auto content_type = res.file_content_content_type_;\n      if (content_type.empty()) {\n        content_type = detail::find_content_type(\n            path, file_extension_and_mimetype_map_, default_file_mimetype_);\n      }\n\n      res.set_content_provider(\n          mm->size(), content_type,\n          [mm](size_t offset, size_t length, DataSink &sink) -> bool {\n            sink.write(mm->data() + offset, length);\n            return true;\n          });\n    }\n\n    if (detail::range_error(req, res)) {\n      res.body.clear();\n      res.content_length_ = 0;\n      res.content_provider_ = nullptr;\n      res.status = StatusCode::RangeNotSatisfiable_416;\n      return write_response(strm, close_connection, req, res);\n    }\n\n    return write_response_with_content(strm, close_connection, req, res);\n  } else {\n    if (res.status == -1) { res.status = StatusCode::NotFound_404; }\n\n    return write_response(strm, close_connection, req, res);\n  }\n}\n\ninline bool Server::is_valid() const { return true; }\n\ninline bool Server::process_and_close_socket(socket_t sock) {\n  std::string remote_addr;\n  int remote_port = 0;\n  detail::get_remote_ip_and_port(sock, remote_addr, remote_port);\n\n  std::string local_addr;\n  int local_port = 0;\n  detail::get_local_ip_and_port(sock, local_addr, local_port);\n\n  auto ret = detail::process_server_socket(\n      svr_sock_, sock, keep_alive_max_count_, keep_alive_timeout_sec_,\n      read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,\n      write_timeout_usec_,\n      [&](Stream &strm, bool close_connection, bool &connection_closed) {\n        return process_request(strm, remote_addr, remote_port, local_addr,\n                               local_port, close_connection, connection_closed,\n                               nullptr);\n      });\n\n  detail::shutdown_socket(sock);\n  detail::close_socket(sock);\n  return ret;\n}\n\ninline void Server::output_log(const Request &req, const Response &res) const {\n  if (logger_) {\n    std::lock_guard<std::mutex> guard(logger_mutex_);\n    logger_(req, res);\n  }\n}\n\ninline void Server::output_pre_compression_log(const Request &req,\n                                               const Response &res) const {\n  if (pre_compression_logger_) {\n    std::lock_guard<std::mutex> guard(logger_mutex_);\n    pre_compression_logger_(req, res);\n  }\n}\n\ninline void Server::output_error_log(const Error &err,\n                                     const Request *req) const {\n  if (error_logger_) {\n    std::lock_guard<std::mutex> guard(logger_mutex_);\n    error_logger_(err, req);\n  }\n}\n\n/*\n * Group 5: ClientImpl and Client (Universal) implementation\n */\n// HTTP client implementation\ninline ClientImpl::ClientImpl(const std::string &host)\n    : ClientImpl(host, 80, std::string(), std::string()) {}\n\ninline ClientImpl::ClientImpl(const std::string &host, int port)\n    : ClientImpl(host, port, std::string(), std::string()) {}\n\ninline ClientImpl::ClientImpl(const std::string &host, int port,\n                              const std::string &client_cert_path,\n                              const std::string &client_key_path)\n    : host_(detail::escape_abstract_namespace_unix_domain(host)), port_(port),\n      client_cert_path_(client_cert_path), client_key_path_(client_key_path) {}\n\ninline ClientImpl::~ClientImpl() {\n  // Wait until all the requests in flight are handled.\n  size_t retry_count = 10;\n  while (retry_count-- > 0) {\n    {\n      std::lock_guard<std::mutex> guard(socket_mutex_);\n      if (socket_requests_in_flight_ == 0) { break; }\n    }\n    std::this_thread::sleep_for(std::chrono::milliseconds{1});\n  }\n\n  std::lock_guard<std::mutex> guard(socket_mutex_);\n  shutdown_socket(socket_);\n  close_socket(socket_);\n}\n\ninline bool ClientImpl::is_valid() const { return true; }\n\ninline void ClientImpl::copy_settings(const ClientImpl &rhs) {\n  client_cert_path_ = rhs.client_cert_path_;\n  client_key_path_ = rhs.client_key_path_;\n  connection_timeout_sec_ = rhs.connection_timeout_sec_;\n  read_timeout_sec_ = rhs.read_timeout_sec_;\n  read_timeout_usec_ = rhs.read_timeout_usec_;\n  write_timeout_sec_ = rhs.write_timeout_sec_;\n  write_timeout_usec_ = rhs.write_timeout_usec_;\n  max_timeout_msec_ = rhs.max_timeout_msec_;\n  basic_auth_username_ = rhs.basic_auth_username_;\n  basic_auth_password_ = rhs.basic_auth_password_;\n  bearer_token_auth_token_ = rhs.bearer_token_auth_token_;\n  keep_alive_ = rhs.keep_alive_;\n  follow_location_ = rhs.follow_location_;\n  path_encode_ = rhs.path_encode_;\n  address_family_ = rhs.address_family_;\n  tcp_nodelay_ = rhs.tcp_nodelay_;\n  ipv6_v6only_ = rhs.ipv6_v6only_;\n  socket_options_ = rhs.socket_options_;\n  compress_ = rhs.compress_;\n  decompress_ = rhs.decompress_;\n  payload_max_length_ = rhs.payload_max_length_;\n  has_payload_max_length_ = rhs.has_payload_max_length_;\n  interface_ = rhs.interface_;\n  proxy_host_ = rhs.proxy_host_;\n  proxy_port_ = rhs.proxy_port_;\n  proxy_basic_auth_username_ = rhs.proxy_basic_auth_username_;\n  proxy_basic_auth_password_ = rhs.proxy_basic_auth_password_;\n  proxy_bearer_token_auth_token_ = rhs.proxy_bearer_token_auth_token_;\n  logger_ = rhs.logger_;\n  error_logger_ = rhs.error_logger_;\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n  digest_auth_username_ = rhs.digest_auth_username_;\n  digest_auth_password_ = rhs.digest_auth_password_;\n  proxy_digest_auth_username_ = rhs.proxy_digest_auth_username_;\n  proxy_digest_auth_password_ = rhs.proxy_digest_auth_password_;\n  ca_cert_file_path_ = rhs.ca_cert_file_path_;\n  ca_cert_dir_path_ = rhs.ca_cert_dir_path_;\n  server_certificate_verification_ = rhs.server_certificate_verification_;\n  server_hostname_verification_ = rhs.server_hostname_verification_;\n#endif\n}\n\ninline socket_t ClientImpl::create_client_socket(Error &error) const {\n  if (!proxy_host_.empty() && proxy_port_ != -1) {\n    return detail::create_client_socket(\n        proxy_host_, std::string(), proxy_port_, address_family_, tcp_nodelay_,\n        ipv6_v6only_, socket_options_, connection_timeout_sec_,\n        connection_timeout_usec_, read_timeout_sec_, read_timeout_usec_,\n        write_timeout_sec_, write_timeout_usec_, interface_, error);\n  }\n\n  // Check is custom IP specified for host_\n  std::string ip;\n  auto it = addr_map_.find(host_);\n  if (it != addr_map_.end()) { ip = it->second; }\n\n  return detail::create_client_socket(\n      host_, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,\n      socket_options_, connection_timeout_sec_, connection_timeout_usec_,\n      read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,\n      write_timeout_usec_, interface_, error);\n}\n\ninline bool ClientImpl::create_and_connect_socket(Socket &socket,\n                                                  Error &error) {\n  auto sock = create_client_socket(error);\n  if (sock == INVALID_SOCKET) { return false; }\n  socket.sock = sock;\n  return true;\n}\n\ninline bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) {\n  return create_and_connect_socket(socket, error);\n}\n\ninline void ClientImpl::shutdown_ssl(Socket & /*socket*/,\n                                     bool /*shutdown_gracefully*/) {\n  // If there are any requests in flight from threads other than us, then it's\n  // a thread-unsafe race because individual ssl* objects are not thread-safe.\n  assert(socket_requests_in_flight_ == 0 ||\n         socket_requests_are_from_thread_ == std::this_thread::get_id());\n}\n\ninline void ClientImpl::shutdown_socket(Socket &socket) const {\n  if (socket.sock == INVALID_SOCKET) { return; }\n  detail::shutdown_socket(socket.sock);\n}\n\ninline void ClientImpl::close_socket(Socket &socket) {\n  // If there are requests in flight in another thread, usually closing\n  // the socket will be fine and they will simply receive an error when\n  // using the closed socket, but it is still a bug since rarely the OS\n  // may reassign the socket id to be used for a new socket, and then\n  // suddenly they will be operating on a live socket that is different\n  // than the one they intended!\n  assert(socket_requests_in_flight_ == 0 ||\n         socket_requests_are_from_thread_ == std::this_thread::get_id());\n\n  // It is also a bug if this happens while SSL is still active\n#ifdef CPPHTTPLIB_SSL_ENABLED\n  assert(socket.ssl == nullptr);\n#endif\n\n  if (socket.sock == INVALID_SOCKET) { return; }\n  detail::close_socket(socket.sock);\n  socket.sock = INVALID_SOCKET;\n}\n\ninline bool ClientImpl::read_response_line(Stream &strm, const Request &req,\n                                           Response &res,\n                                           bool skip_100_continue) const {\n  std::array<char, 2048> buf{};\n\n  detail::stream_line_reader line_reader(strm, buf.data(), buf.size());\n\n  if (!line_reader.getline()) { return false; }\n\n#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR\n  thread_local const std::regex re(\"(HTTP/1\\\\.[01]) (\\\\d{3})(?: (.*?))?\\r?\\n\");\n#else\n  thread_local const std::regex re(\"(HTTP/1\\\\.[01]) (\\\\d{3})(?: (.*?))?\\r\\n\");\n#endif\n\n  std::cmatch m;\n  if (!std::regex_match(line_reader.ptr(), m, re)) {\n    return req.method == \"CONNECT\";\n  }\n  res.version = std::string(m[1]);\n  res.status = std::stoi(std::string(m[2]));\n  res.reason = std::string(m[3]);\n\n  // Ignore '100 Continue' (only when not using Expect: 100-continue explicitly)\n  while (skip_100_continue && res.status == StatusCode::Continue_100) {\n    if (!line_reader.getline()) { return false; } // CRLF\n    if (!line_reader.getline()) { return false; } // next response line\n\n    if (!std::regex_match(line_reader.ptr(), m, re)) { return false; }\n    res.version = std::string(m[1]);\n    res.status = std::stoi(std::string(m[2]));\n    res.reason = std::string(m[3]);\n  }\n\n  return true;\n}\n\ninline bool ClientImpl::send(Request &req, Response &res, Error &error) {\n  std::lock_guard<std::recursive_mutex> request_mutex_guard(request_mutex_);\n  auto ret = send_(req, res, error);\n  if (error == Error::SSLPeerCouldBeClosed_) {\n    assert(!ret);\n    ret = send_(req, res, error);\n    // If still failing with SSLPeerCouldBeClosed_, convert to Read error\n    if (error == Error::SSLPeerCouldBeClosed_) { error = Error::Read; }\n  }\n  return ret;\n}\n\ninline bool ClientImpl::send_(Request &req, Response &res, Error &error) {\n  {\n    std::lock_guard<std::mutex> guard(socket_mutex_);\n\n    // Set this to false immediately - if it ever gets set to true by the end\n    // of the request, we know another thread instructed us to close the\n    // socket.\n    socket_should_be_closed_when_request_is_done_ = false;\n\n    auto is_alive = false;\n    if (socket_.is_open()) {\n      is_alive = detail::is_socket_alive(socket_.sock);\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n      if (is_alive && is_ssl()) {\n        if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {\n          is_alive = false;\n        }\n      }\n#endif\n\n      if (!is_alive) {\n        // Attempt to avoid sigpipe by shutting down non-gracefully if it\n        // seems like the other side has already closed the connection Also,\n        // there cannot be any requests in flight from other threads since we\n        // locked request_mutex_, so safe to close everything immediately\n        const bool shutdown_gracefully = false;\n        shutdown_ssl(socket_, shutdown_gracefully);\n        shutdown_socket(socket_);\n        close_socket(socket_);\n      }\n    }\n\n    if (!is_alive) {\n      if (!ensure_socket_connection(socket_, error)) {\n        output_error_log(error, &req);\n        return false;\n      }\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n      // TODO: refactoring\n      if (is_ssl()) {\n        auto &scli = static_cast<SSLClient &>(*this);\n        if (!proxy_host_.empty() && proxy_port_ != -1) {\n          auto success = false;\n          if (!scli.connect_with_proxy(socket_, req.start_time_, res, success,\n                                       error)) {\n            if (!success) { output_error_log(error, &req); }\n            return success;\n          }\n        }\n\n        if (!proxy_host_.empty() && proxy_port_ != -1) {\n          if (!scli.initialize_ssl(socket_, error)) {\n            output_error_log(error, &req);\n            return false;\n          }\n        }\n      }\n#endif\n    }\n\n    // Mark the current socket as being in use so that it cannot be closed by\n    // anyone else while this request is ongoing, even though we will be\n    // releasing the mutex.\n    if (socket_requests_in_flight_ > 1) {\n      assert(socket_requests_are_from_thread_ == std::this_thread::get_id());\n    }\n    socket_requests_in_flight_ += 1;\n    socket_requests_are_from_thread_ = std::this_thread::get_id();\n  }\n\n  for (const auto &header : default_headers_) {\n    if (req.headers.find(header.first) == req.headers.end()) {\n      req.headers.insert(header);\n    }\n  }\n\n  auto ret = false;\n  auto close_connection = !keep_alive_;\n\n  auto se = detail::scope_exit([&]() {\n    // Briefly lock mutex in order to mark that a request is no longer ongoing\n    std::lock_guard<std::mutex> guard(socket_mutex_);\n    socket_requests_in_flight_ -= 1;\n    if (socket_requests_in_flight_ <= 0) {\n      assert(socket_requests_in_flight_ == 0);\n      socket_requests_are_from_thread_ = std::thread::id();\n    }\n\n    if (socket_should_be_closed_when_request_is_done_ || close_connection ||\n        !ret) {\n      shutdown_ssl(socket_, true);\n      shutdown_socket(socket_);\n      close_socket(socket_);\n    }\n  });\n\n  ret = process_socket(socket_, req.start_time_, [&](Stream &strm) {\n    return handle_request(strm, req, res, close_connection, error);\n  });\n\n  if (!ret) {\n    if (error == Error::Success) {\n      error = Error::Unknown;\n      output_error_log(error, &req);\n    }\n  }\n\n  return ret;\n}\n\ninline Result ClientImpl::send(const Request &req) {\n  auto req2 = req;\n  return send_(std::move(req2));\n}\n\ninline Result ClientImpl::send_(Request &&req) {\n  auto res = detail::make_unique<Response>();\n  auto error = Error::Success;\n  auto ret = send(req, *res, error);\n#ifdef CPPHTTPLIB_SSL_ENABLED\n  return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers),\n                last_ssl_error_, last_backend_error_};\n#else\n  return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)};\n#endif\n}\n\ninline void ClientImpl::prepare_default_headers(Request &r, bool for_stream,\n                                                const std::string &ct) {\n  (void)for_stream;\n  for (const auto &header : default_headers_) {\n    if (!r.has_header(header.first)) { r.headers.insert(header); }\n  }\n\n  if (!r.has_header(\"Host\")) {\n    if (address_family_ == AF_UNIX) {\n      r.headers.emplace(\"Host\", \"localhost\");\n    } else {\n      r.headers.emplace(\n          \"Host\", detail::make_host_and_port_string(host_, port_, is_ssl()));\n    }\n  }\n\n  if (!r.has_header(\"Accept\")) { r.headers.emplace(\"Accept\", \"*/*\"); }\n\n  if (!r.content_receiver) {\n    if (!r.has_header(\"Accept-Encoding\")) {\n      std::string accept_encoding;\n#ifdef CPPHTTPLIB_BROTLI_SUPPORT\n      accept_encoding = \"br\";\n#endif\n#ifdef CPPHTTPLIB_ZLIB_SUPPORT\n      if (!accept_encoding.empty()) { accept_encoding += \", \"; }\n      accept_encoding += \"gzip, deflate\";\n#endif\n#ifdef CPPHTTPLIB_ZSTD_SUPPORT\n      if (!accept_encoding.empty()) { accept_encoding += \", \"; }\n      accept_encoding += \"zstd\";\n#endif\n      r.set_header(\"Accept-Encoding\", accept_encoding);\n    }\n\n#ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT\n    if (!r.has_header(\"User-Agent\")) {\n      auto agent = std::string(\"cpp-httplib/\") + CPPHTTPLIB_VERSION;\n      r.set_header(\"User-Agent\", agent);\n    }\n#endif\n  }\n\n  if (!r.body.empty()) {\n    if (!ct.empty() && !r.has_header(\"Content-Type\")) {\n      r.headers.emplace(\"Content-Type\", ct);\n    }\n    if (!r.has_header(\"Content-Length\")) {\n      r.headers.emplace(\"Content-Length\", std::to_string(r.body.size()));\n    }\n  }\n}\n\ninline ClientImpl::StreamHandle\nClientImpl::open_stream(const std::string &method, const std::string &path,\n                        const Params &params, const Headers &headers,\n                        const std::string &body,\n                        const std::string &content_type) {\n  StreamHandle handle;\n  handle.response = detail::make_unique<Response>();\n  handle.error = Error::Success;\n\n  auto query_path = params.empty() ? path : append_query_params(path, params);\n  handle.connection_ = detail::make_unique<ClientConnection>();\n\n  {\n    std::lock_guard<std::mutex> guard(socket_mutex_);\n\n    auto is_alive = false;\n    if (socket_.is_open()) {\n      is_alive = detail::is_socket_alive(socket_.sock);\n#ifdef CPPHTTPLIB_SSL_ENABLED\n      if (is_alive && is_ssl()) {\n        if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {\n          is_alive = false;\n        }\n      }\n#endif\n      if (!is_alive) {\n        shutdown_ssl(socket_, false);\n        shutdown_socket(socket_);\n        close_socket(socket_);\n      }\n    }\n\n    if (!is_alive) {\n      if (!ensure_socket_connection(socket_, handle.error)) {\n        handle.response.reset();\n        return handle;\n      }\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n      if (is_ssl()) {\n        auto &scli = static_cast<SSLClient &>(*this);\n        if (!proxy_host_.empty() && proxy_port_ != -1) {\n          if (!scli.initialize_ssl(socket_, handle.error)) {\n            handle.response.reset();\n            return handle;\n          }\n        }\n      }\n#endif\n    }\n\n    transfer_socket_ownership_to_handle(handle);\n  }\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n  if (is_ssl() && handle.connection_->session) {\n    handle.socket_stream_ = detail::make_unique<detail::SSLSocketStream>(\n        handle.connection_->sock, handle.connection_->session,\n        read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,\n        write_timeout_usec_);\n  } else {\n    handle.socket_stream_ = detail::make_unique<detail::SocketStream>(\n        handle.connection_->sock, read_timeout_sec_, read_timeout_usec_,\n        write_timeout_sec_, write_timeout_usec_);\n  }\n#else\n  handle.socket_stream_ = detail::make_unique<detail::SocketStream>(\n      handle.connection_->sock, read_timeout_sec_, read_timeout_usec_,\n      write_timeout_sec_, write_timeout_usec_);\n#endif\n  handle.stream_ = handle.socket_stream_.get();\n\n  Request req;\n  req.method = method;\n  req.path = query_path;\n  req.headers = headers;\n  req.body = body;\n\n  prepare_default_headers(req, true, content_type);\n\n  auto &strm = *handle.stream_;\n  if (detail::write_request_line(strm, req.method, req.path) < 0) {\n    handle.error = Error::Write;\n    handle.response.reset();\n    return handle;\n  }\n\n  if (!detail::check_and_write_headers(strm, req.headers, header_writer_,\n                                       handle.error)) {\n    handle.response.reset();\n    return handle;\n  }\n\n  if (!body.empty()) {\n    if (strm.write(body.data(), body.size()) < 0) {\n      handle.error = Error::Write;\n      handle.response.reset();\n      return handle;\n    }\n  }\n\n  if (!read_response_line(strm, req, *handle.response) ||\n      !detail::read_headers(strm, handle.response->headers)) {\n    handle.error = Error::Read;\n    handle.response.reset();\n    return handle;\n  }\n\n  handle.body_reader_.stream = handle.stream_;\n  handle.body_reader_.payload_max_length = payload_max_length_;\n\n  auto content_length_str = handle.response->get_header_value(\"Content-Length\");\n  if (!content_length_str.empty()) {\n    handle.body_reader_.has_content_length = true;\n    handle.body_reader_.content_length =\n        static_cast<size_t>(std::stoull(content_length_str));\n  }\n\n  auto transfer_encoding =\n      handle.response->get_header_value(\"Transfer-Encoding\");\n  handle.body_reader_.chunked = (transfer_encoding == \"chunked\");\n\n  auto content_encoding = handle.response->get_header_value(\"Content-Encoding\");\n  if (!content_encoding.empty()) {\n    handle.decompressor_ = detail::create_decompressor(content_encoding);\n  }\n\n  return handle;\n}\n\ninline ssize_t ClientImpl::StreamHandle::read(char *buf, size_t len) {\n  if (!is_valid() || !response) { return -1; }\n\n  if (decompressor_) { return read_with_decompression(buf, len); }\n  auto n = detail::read_body_content(stream_, body_reader_, buf, len);\n\n  if (n <= 0 && body_reader_.chunked && !trailers_parsed_ && stream_) {\n    trailers_parsed_ = true;\n    if (body_reader_.chunked_decoder) {\n      if (!body_reader_.chunked_decoder->parse_trailers_into(\n              response->trailers, response->headers)) {\n        return n;\n      }\n    } else {\n      detail::ChunkedDecoder dec(*stream_);\n      if (!dec.parse_trailers_into(response->trailers, response->headers)) {\n        return n;\n      }\n    }\n  }\n\n  return n;\n}\n\ninline ssize_t ClientImpl::StreamHandle::read_with_decompression(char *buf,\n                                                                 size_t len) {\n  if (decompress_offset_ < decompress_buffer_.size()) {\n    auto available = decompress_buffer_.size() - decompress_offset_;\n    auto to_copy = (std::min)(len, available);\n    std::memcpy(buf, decompress_buffer_.data() + decompress_offset_, to_copy);\n    decompress_offset_ += to_copy;\n    decompressed_bytes_read_ += to_copy;\n    return static_cast<ssize_t>(to_copy);\n  }\n\n  decompress_buffer_.clear();\n  decompress_offset_ = 0;\n\n  constexpr size_t kDecompressionBufferSize = 8192;\n  char compressed_buf[kDecompressionBufferSize];\n\n  while (true) {\n    auto n = detail::read_body_content(stream_, body_reader_, compressed_buf,\n                                       sizeof(compressed_buf));\n\n    if (n <= 0) { return n; }\n\n    bool decompress_ok = decompressor_->decompress(\n        compressed_buf, static_cast<size_t>(n),\n        [this](const char *data, size_t data_len) {\n          decompress_buffer_.append(data, data_len);\n          auto limit = body_reader_.payload_max_length;\n          if (decompressed_bytes_read_ + decompress_buffer_.size() > limit) {\n            return false;\n          }\n          return true;\n        });\n\n    if (!decompress_ok) {\n      body_reader_.last_error = Error::Read;\n      return -1;\n    }\n\n    if (!decompress_buffer_.empty()) { break; }\n  }\n\n  auto to_copy = (std::min)(len, decompress_buffer_.size());\n  std::memcpy(buf, decompress_buffer_.data(), to_copy);\n  decompress_offset_ = to_copy;\n  decompressed_bytes_read_ += to_copy;\n  return static_cast<ssize_t>(to_copy);\n}\n\ninline void ClientImpl::StreamHandle::parse_trailers_if_needed() {\n  if (!response || !stream_ || !body_reader_.chunked || trailers_parsed_) {\n    return;\n  }\n\n  trailers_parsed_ = true;\n\n  const auto bufsiz = 128;\n  char line_buf[bufsiz];\n  detail::stream_line_reader line_reader(*stream_, line_buf, bufsiz);\n\n  if (!line_reader.getline()) { return; }\n\n  if (!detail::parse_trailers(line_reader, response->trailers,\n                              response->headers)) {\n    return;\n  }\n}\n\nnamespace detail {\n\ninline ChunkedDecoder::ChunkedDecoder(Stream &s) : strm(s) {}\n\ninline ssize_t ChunkedDecoder::read_payload(char *buf, size_t len,\n                                            size_t &out_chunk_offset,\n                                            size_t &out_chunk_total) {\n  if (finished) { return 0; }\n\n  if (chunk_remaining == 0) {\n    stream_line_reader lr(strm, line_buf, sizeof(line_buf));\n    if (!lr.getline()) { return -1; }\n\n    char *endptr = nullptr;\n    unsigned long chunk_len = std::strtoul(lr.ptr(), &endptr, 16);\n    if (endptr == lr.ptr()) { return -1; }\n    if (chunk_len == ULONG_MAX) { return -1; }\n\n    if (chunk_len == 0) {\n      chunk_remaining = 0;\n      finished = true;\n      out_chunk_offset = 0;\n      out_chunk_total = 0;\n      return 0;\n    }\n\n    chunk_remaining = static_cast<size_t>(chunk_len);\n    last_chunk_total = chunk_remaining;\n    last_chunk_offset = 0;\n  }\n\n  auto to_read = (std::min)(chunk_remaining, len);\n  auto n = strm.read(buf, to_read);\n  if (n <= 0) { return -1; }\n\n  auto offset_before = last_chunk_offset;\n  last_chunk_offset += static_cast<size_t>(n);\n  chunk_remaining -= static_cast<size_t>(n);\n\n  out_chunk_offset = offset_before;\n  out_chunk_total = last_chunk_total;\n\n  if (chunk_remaining == 0) {\n    stream_line_reader lr(strm, line_buf, sizeof(line_buf));\n    if (!lr.getline()) { return -1; }\n    if (std::strcmp(lr.ptr(), \"\\r\\n\") != 0) { return -1; }\n  }\n\n  return n;\n}\n\ninline bool ChunkedDecoder::parse_trailers_into(Headers &dest,\n                                                const Headers &src_headers) {\n  stream_line_reader lr(strm, line_buf, sizeof(line_buf));\n  if (!lr.getline()) { return false; }\n  return parse_trailers(lr, dest, src_headers);\n}\n\n} // namespace detail\n\ninline void\nClientImpl::transfer_socket_ownership_to_handle(StreamHandle &handle) {\n  handle.connection_->sock = socket_.sock;\n#ifdef CPPHTTPLIB_SSL_ENABLED\n  handle.connection_->session = socket_.ssl;\n  socket_.ssl = nullptr;\n#endif\n  socket_.sock = INVALID_SOCKET;\n}\n\ninline bool ClientImpl::handle_request(Stream &strm, Request &req,\n                                       Response &res, bool close_connection,\n                                       Error &error) {\n  if (req.path.empty()) {\n    error = Error::Connection;\n    output_error_log(error, &req);\n    return false;\n  }\n\n  auto req_save = req;\n\n  bool ret;\n\n  if (!is_ssl() && !proxy_host_.empty() && proxy_port_ != -1) {\n    auto req2 = req;\n    req2.path = \"http://\" +\n                detail::make_host_and_port_string(host_, port_, false) +\n                req.path;\n    ret = process_request(strm, req2, res, close_connection, error);\n    req = std::move(req2);\n    req.path = req_save.path;\n  } else {\n    ret = process_request(strm, req, res, close_connection, error);\n  }\n\n  if (!ret) { return false; }\n\n  if (res.get_header_value(\"Connection\") == \"close\" ||\n      (res.version == \"HTTP/1.0\" && res.reason != \"Connection established\")) {\n    // TODO this requires a not-entirely-obvious chain of calls to be correct\n    // for this to be safe.\n\n    // This is safe to call because handle_request is only called by send_\n    // which locks the request mutex during the process. It would be a bug\n    // to call it from a different thread since it's a thread-safety issue\n    // to do these things to the socket if another thread is using the socket.\n    std::lock_guard<std::mutex> guard(socket_mutex_);\n    shutdown_ssl(socket_, true);\n    shutdown_socket(socket_);\n    close_socket(socket_);\n  }\n\n  if (300 < res.status && res.status < 400 && follow_location_) {\n    req = std::move(req_save);\n    ret = redirect(req, res, error);\n  }\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n  if ((res.status == StatusCode::Unauthorized_401 ||\n       res.status == StatusCode::ProxyAuthenticationRequired_407) &&\n      req.authorization_count_ < 5) {\n    auto is_proxy = res.status == StatusCode::ProxyAuthenticationRequired_407;\n    const auto &username =\n        is_proxy ? proxy_digest_auth_username_ : digest_auth_username_;\n    const auto &password =\n        is_proxy ? proxy_digest_auth_password_ : digest_auth_password_;\n\n    if (!username.empty() && !password.empty()) {\n      std::map<std::string, std::string> auth;\n      if (detail::parse_www_authenticate(res, auth, is_proxy)) {\n        Request new_req = req;\n        new_req.authorization_count_ += 1;\n        new_req.headers.erase(is_proxy ? \"Proxy-Authorization\"\n                                       : \"Authorization\");\n        new_req.headers.insert(detail::make_digest_authentication_header(\n            req, auth, new_req.authorization_count_, detail::random_string(10),\n            username, password, is_proxy));\n\n        Response new_res;\n\n        ret = send(new_req, new_res, error);\n        if (ret) { res = std::move(new_res); }\n      }\n    }\n  }\n#endif\n\n  return ret;\n}\n\ninline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {\n  if (req.redirect_count_ == 0) {\n    error = Error::ExceedRedirectCount;\n    output_error_log(error, &req);\n    return false;\n  }\n\n  auto location = res.get_header_value(\"location\");\n  if (location.empty()) { return false; }\n\n  thread_local const std::regex re(\n      R\"((?:(https?):)?(?://(?:\\[([a-fA-F\\d:]+)\\]|([^:/?#]+))(?::(\\d+))?)?([^?#]*)(\\?[^#]*)?(?:#.*)?)\");\n\n  std::smatch m;\n  if (!std::regex_match(location, m, re)) { return false; }\n\n  auto scheme = is_ssl() ? \"https\" : \"http\";\n\n  auto next_scheme = m[1].str();\n  auto next_host = m[2].str();\n  if (next_host.empty()) { next_host = m[3].str(); }\n  auto port_str = m[4].str();\n  auto next_path = m[5].str();\n  auto next_query = m[6].str();\n\n  auto next_port = port_;\n  if (!port_str.empty()) {\n    next_port = std::stoi(port_str);\n  } else if (!next_scheme.empty()) {\n    next_port = next_scheme == \"https\" ? 443 : 80;\n  }\n\n  if (next_scheme.empty()) { next_scheme = scheme; }\n  if (next_host.empty()) { next_host = host_; }\n  if (next_path.empty()) { next_path = \"/\"; }\n\n  auto path = decode_query_component(next_path, true) + next_query;\n\n  // Same host redirect - use current client\n  if (next_scheme == scheme && next_host == host_ && next_port == port_) {\n    return detail::redirect(*this, req, res, path, location, error);\n  }\n\n  // Cross-host/scheme redirect - create new client with robust setup\n  return create_redirect_client(next_scheme, next_host, next_port, req, res,\n                                path, location, error);\n}\n\n// New method for robust redirect client creation\ninline bool ClientImpl::create_redirect_client(\n    const std::string &scheme, const std::string &host, int port, Request &req,\n    Response &res, const std::string &path, const std::string &location,\n    Error &error) {\n  // Determine if we need SSL\n  auto need_ssl = (scheme == \"https\");\n\n  // Clean up request headers that are host/client specific\n  // Remove headers that should not be carried over to new host\n  auto headers_to_remove =\n      std::vector<std::string>{\"Host\", \"Proxy-Authorization\", \"Authorization\"};\n\n  for (const auto &header_name : headers_to_remove) {\n    auto it = req.headers.find(header_name);\n    while (it != req.headers.end()) {\n      it = req.headers.erase(it);\n      it = req.headers.find(header_name);\n    }\n  }\n\n  // Create appropriate client type and handle redirect\n  if (need_ssl) {\n#ifdef CPPHTTPLIB_SSL_ENABLED\n    // Create SSL client for HTTPS redirect\n    SSLClient redirect_client(host, port);\n\n    // Setup basic client configuration first\n    setup_redirect_client(redirect_client);\n\n    // SSL-specific configuration for proxy environments\n    if (!proxy_host_.empty() && proxy_port_ != -1) {\n      // Critical: Disable SSL verification for proxy environments\n      redirect_client.enable_server_certificate_verification(false);\n      redirect_client.enable_server_hostname_verification(false);\n    } else {\n      // For direct SSL connections, copy SSL verification settings\n      redirect_client.enable_server_certificate_verification(\n          server_certificate_verification_);\n      redirect_client.enable_server_hostname_verification(\n          server_hostname_verification_);\n    }\n\n    // Transfer CA certificate to redirect client\n    if (!ca_cert_pem_.empty()) {\n      redirect_client.load_ca_cert_store(ca_cert_pem_.c_str(),\n                                         ca_cert_pem_.size());\n    }\n    if (!ca_cert_file_path_.empty()) {\n      redirect_client.set_ca_cert_path(ca_cert_file_path_, ca_cert_dir_path_);\n    }\n\n    // Client certificates are set through constructor for SSLClient\n    // NOTE: SSLClient constructor already takes client_cert_path and\n    // client_key_path so we need to create it properly if client certs are\n    // needed\n\n    // Execute the redirect\n    return detail::redirect(redirect_client, req, res, path, location, error);\n#else\n    // SSL not supported - set appropriate error\n    error = Error::SSLConnection;\n    output_error_log(error, &req);\n    return false;\n#endif\n  } else {\n    // HTTP redirect\n    ClientImpl redirect_client(host, port);\n\n    // Setup client with robust configuration\n    setup_redirect_client(redirect_client);\n\n    // Execute the redirect\n    return detail::redirect(redirect_client, req, res, path, location, error);\n  }\n}\n\n// New method for robust client setup (based on basic_manual_redirect.cpp\n// logic)\ntemplate <typename ClientType>\ninline void ClientImpl::setup_redirect_client(ClientType &client) {\n  // Copy basic settings first\n  client.set_connection_timeout(connection_timeout_sec_);\n  client.set_read_timeout(read_timeout_sec_, read_timeout_usec_);\n  client.set_write_timeout(write_timeout_sec_, write_timeout_usec_);\n  client.set_keep_alive(keep_alive_);\n  client.set_follow_location(\n      true); // Enable redirects to handle multi-step redirects\n  client.set_path_encode(path_encode_);\n  client.set_compress(compress_);\n  client.set_decompress(decompress_);\n\n  // Copy authentication settings BEFORE proxy setup\n  if (!basic_auth_username_.empty()) {\n    client.set_basic_auth(basic_auth_username_, basic_auth_password_);\n  }\n  if (!bearer_token_auth_token_.empty()) {\n    client.set_bearer_token_auth(bearer_token_auth_token_);\n  }\n#ifdef CPPHTTPLIB_SSL_ENABLED\n  if (!digest_auth_username_.empty()) {\n    client.set_digest_auth(digest_auth_username_, digest_auth_password_);\n  }\n#endif\n\n  // Setup proxy configuration (CRITICAL ORDER - proxy must be set\n  // before proxy auth)\n  if (!proxy_host_.empty() && proxy_port_ != -1) {\n    // First set proxy host and port\n    client.set_proxy(proxy_host_, proxy_port_);\n\n    // Then set proxy authentication (order matters!)\n    if (!proxy_basic_auth_username_.empty()) {\n      client.set_proxy_basic_auth(proxy_basic_auth_username_,\n                                  proxy_basic_auth_password_);\n    }\n    if (!proxy_bearer_token_auth_token_.empty()) {\n      client.set_proxy_bearer_token_auth(proxy_bearer_token_auth_token_);\n    }\n#ifdef CPPHTTPLIB_SSL_ENABLED\n    if (!proxy_digest_auth_username_.empty()) {\n      client.set_proxy_digest_auth(proxy_digest_auth_username_,\n                                   proxy_digest_auth_password_);\n    }\n#endif\n  }\n\n  // Copy network and socket settings\n  client.set_address_family(address_family_);\n  client.set_tcp_nodelay(tcp_nodelay_);\n  client.set_ipv6_v6only(ipv6_v6only_);\n  if (socket_options_) { client.set_socket_options(socket_options_); }\n  if (!interface_.empty()) { client.set_interface(interface_); }\n\n  // Copy logging and headers\n  if (logger_) { client.set_logger(logger_); }\n  if (error_logger_) { client.set_error_logger(error_logger_); }\n\n  // NOTE: DO NOT copy default_headers_ as they may contain stale Host headers\n  // Each new client should generate its own headers based on its target host\n}\n\ninline bool ClientImpl::write_content_with_provider(Stream &strm,\n                                                    const Request &req,\n                                                    Error &error) const {\n  auto is_shutting_down = []() { return false; };\n\n  if (req.is_chunked_content_provider_) {\n    // TODO: Brotli support\n    std::unique_ptr<detail::compressor> compressor;\n#ifdef CPPHTTPLIB_ZLIB_SUPPORT\n    if (compress_) {\n      compressor = detail::make_unique<detail::gzip_compressor>();\n    } else\n#endif\n    {\n      compressor = detail::make_unique<detail::nocompressor>();\n    }\n\n    return detail::write_content_chunked(strm, req.content_provider_,\n                                         is_shutting_down, *compressor, error);\n  } else {\n    return detail::write_content_with_progress(\n        strm, req.content_provider_, 0, req.content_length_, is_shutting_down,\n        req.upload_progress, error);\n  }\n}\n\ninline bool ClientImpl::write_request(Stream &strm, Request &req,\n                                      bool close_connection, Error &error,\n                                      bool skip_body) {\n  // Prepare additional headers\n  if (close_connection) {\n    if (!req.has_header(\"Connection\")) {\n      req.set_header(\"Connection\", \"close\");\n    }\n  }\n\n  std::string ct_for_defaults;\n  if (!req.has_header(\"Content-Type\") && !req.body.empty()) {\n    ct_for_defaults = \"text/plain\";\n  }\n  prepare_default_headers(req, false, ct_for_defaults);\n\n  if (req.body.empty()) {\n    if (req.content_provider_) {\n      if (!req.is_chunked_content_provider_) {\n        if (!req.has_header(\"Content-Length\")) {\n          auto length = std::to_string(req.content_length_);\n          req.set_header(\"Content-Length\", length);\n        }\n      }\n    } else {\n      if (req.method == \"POST\" || req.method == \"PUT\" ||\n          req.method == \"PATCH\") {\n        req.set_header(\"Content-Length\", \"0\");\n      }\n    }\n  }\n\n  if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) {\n    if (!req.has_header(\"Authorization\")) {\n      req.headers.insert(make_basic_authentication_header(\n          basic_auth_username_, basic_auth_password_, false));\n    }\n  }\n\n  if (!proxy_basic_auth_username_.empty() &&\n      !proxy_basic_auth_password_.empty()) {\n    if (!req.has_header(\"Proxy-Authorization\")) {\n      req.headers.insert(make_basic_authentication_header(\n          proxy_basic_auth_username_, proxy_basic_auth_password_, true));\n    }\n  }\n\n  if (!bearer_token_auth_token_.empty()) {\n    if (!req.has_header(\"Authorization\")) {\n      req.headers.insert(make_bearer_token_authentication_header(\n          bearer_token_auth_token_, false));\n    }\n  }\n\n  if (!proxy_bearer_token_auth_token_.empty()) {\n    if (!req.has_header(\"Proxy-Authorization\")) {\n      req.headers.insert(make_bearer_token_authentication_header(\n          proxy_bearer_token_auth_token_, true));\n    }\n  }\n\n  // Request line and headers\n  {\n    detail::BufferStream bstrm;\n\n    // Extract path and query from req.path\n    std::string path_part, query_part;\n    auto query_pos = req.path.find('?');\n    if (query_pos != std::string::npos) {\n      path_part = req.path.substr(0, query_pos);\n      query_part = req.path.substr(query_pos + 1);\n    } else {\n      path_part = req.path;\n      query_part = \"\";\n    }\n\n    // Encode path part. If the original `req.path` already contained a\n    // query component, preserve its raw query string (including parameter\n    // order) instead of reparsing and reassembling it which may reorder\n    // parameters due to container ordering (e.g. `Params` uses\n    // `std::multimap`). When there is no query in `req.path`, fall back to\n    // building a query from `req.params` so existing callers that pass\n    // `Params` continue to work.\n    auto path_with_query =\n        path_encode_ ? detail::encode_path(path_part) : path_part;\n\n    if (!query_part.empty()) {\n      // Normalize the query string (decode then re-encode) while preserving\n      // the original parameter order.\n      auto normalized = detail::normalize_query_string(query_part);\n      if (!normalized.empty()) { path_with_query += '?' + normalized; }\n\n      // Still populate req.params for handlers/users who read them.\n      detail::parse_query_text(query_part, req.params);\n    } else {\n      // No query in path; parse any query_part (empty) and append params\n      // from `req.params` when present (preserves prior behavior for\n      // callers who provide Params separately).\n      detail::parse_query_text(query_part, req.params);\n      if (!req.params.empty()) {\n        path_with_query = append_query_params(path_with_query, req.params);\n      }\n    }\n\n    // Write request line and headers\n    detail::write_request_line(bstrm, req.method, path_with_query);\n    if (!detail::check_and_write_headers(bstrm, req.headers, header_writer_,\n                                         error)) {\n      output_error_log(error, &req);\n      return false;\n    }\n\n    // Flush buffer\n    auto &data = bstrm.get_buffer();\n    if (!detail::write_data(strm, data.data(), data.size())) {\n      error = Error::Write;\n      output_error_log(error, &req);\n      return false;\n    }\n  }\n\n  // After sending request line and headers, wait briefly for an early server\n  // response (e.g. 4xx) and avoid sending a potentially large request body\n  // unnecessarily. This workaround is only enabled on Windows because Unix\n  // platforms surface write errors (EPIPE) earlier; on Windows kernel send\n  // buffering can accept large writes even when the peer already responded.\n  // Check the stream first (which covers SSL via `is_readable()`), then\n  // fall back to select on the socket. Only perform the wait for very large\n  // request bodies to avoid interfering with normal small requests and\n  // reduce side-effects. Poll briefly (up to 50ms as default) for an early\n  // response. Skip this check when using Expect: 100-continue, as the protocol\n  // handles early responses properly.\n#if defined(_WIN32)\n  if (!skip_body &&\n      req.body.size() > CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD &&\n      req.path.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {\n    auto start = std::chrono::high_resolution_clock::now();\n\n    for (;;) {\n      // Prefer socket-level readiness to avoid SSL_pending() false-positives\n      // from SSL internals. If the underlying socket is readable, assume an\n      // early response may be present.\n      auto sock = strm.socket();\n      if (sock != INVALID_SOCKET && detail::select_read(sock, 0, 0) > 0) {\n        return false;\n      }\n\n      // Fallback to stream-level check for non-socket streams or when the\n      // socket isn't reporting readable. Avoid using `is_readable()` for\n      // SSL, since `SSL_pending()` may report buffered records that do not\n      // indicate a complete application-level response yet.\n      if (!is_ssl() && strm.is_readable()) { return false; }\n\n      auto now = std::chrono::high_resolution_clock::now();\n      auto elapsed =\n          std::chrono::duration_cast<std::chrono::milliseconds>(now - start)\n              .count();\n      if (elapsed >= CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND) {\n        break;\n      }\n\n      std::this_thread::sleep_for(std::chrono::milliseconds(1));\n    }\n  }\n#endif\n\n  // Body\n  if (skip_body) { return true; }\n\n  return write_request_body(strm, req, error);\n}\n\ninline bool ClientImpl::write_request_body(Stream &strm, Request &req,\n                                           Error &error) {\n  if (req.body.empty()) {\n    return write_content_with_provider(strm, req, error);\n  }\n\n  if (req.upload_progress) {\n    auto body_size = req.body.size();\n    size_t written = 0;\n    auto data = req.body.data();\n\n    while (written < body_size) {\n      size_t to_write = (std::min)(CPPHTTPLIB_SEND_BUFSIZ, body_size - written);\n      if (!detail::write_data(strm, data + written, to_write)) {\n        error = Error::Write;\n        output_error_log(error, &req);\n        return false;\n      }\n      written += to_write;\n\n      if (!req.upload_progress(written, body_size)) {\n        error = Error::Canceled;\n        output_error_log(error, &req);\n        return false;\n      }\n    }\n  } else {\n    if (!detail::write_data(strm, req.body.data(), req.body.size())) {\n      error = Error::Write;\n      output_error_log(error, &req);\n      return false;\n    }\n  }\n\n  return true;\n}\n\ninline std::unique_ptr<Response>\nClientImpl::send_with_content_provider_and_receiver(\n    Request &req, const char *body, size_t content_length,\n    ContentProvider content_provider,\n    ContentProviderWithoutLength content_provider_without_length,\n    const std::string &content_type, ContentReceiver content_receiver,\n    Error &error) {\n  if (!content_type.empty()) { req.set_header(\"Content-Type\", content_type); }\n\n#ifdef CPPHTTPLIB_ZLIB_SUPPORT\n  if (compress_) { req.set_header(\"Content-Encoding\", \"gzip\"); }\n#endif\n\n#ifdef CPPHTTPLIB_ZLIB_SUPPORT\n  if (compress_ && !content_provider_without_length) {\n    // TODO: Brotli support\n    detail::gzip_compressor compressor;\n\n    if (content_provider) {\n      auto ok = true;\n      size_t offset = 0;\n      DataSink data_sink;\n\n      data_sink.write = [&](const char *data, size_t data_len) -> bool {\n        if (ok) {\n          auto last = offset + data_len == content_length;\n\n          auto ret = compressor.compress(\n              data, data_len, last,\n              [&](const char *compressed_data, size_t compressed_data_len) {\n                req.body.append(compressed_data, compressed_data_len);\n                return true;\n              });\n\n          if (ret) {\n            offset += data_len;\n          } else {\n            ok = false;\n          }\n        }\n        return ok;\n      };\n\n      while (ok && offset < content_length) {\n        if (!content_provider(offset, content_length - offset, data_sink)) {\n          error = Error::Canceled;\n          output_error_log(error, &req);\n          return nullptr;\n        }\n      }\n    } else {\n      if (!compressor.compress(body, content_length, true,\n                               [&](const char *data, size_t data_len) {\n                                 req.body.append(data, data_len);\n                                 return true;\n                               })) {\n        error = Error::Compression;\n        output_error_log(error, &req);\n        return nullptr;\n      }\n    }\n  } else\n#endif\n  {\n    if (content_provider) {\n      req.content_length_ = content_length;\n      req.content_provider_ = std::move(content_provider);\n      req.is_chunked_content_provider_ = false;\n    } else if (content_provider_without_length) {\n      req.content_length_ = 0;\n      req.content_provider_ = detail::ContentProviderAdapter(\n          std::move(content_provider_without_length));\n      req.is_chunked_content_provider_ = true;\n      req.set_header(\"Transfer-Encoding\", \"chunked\");\n    } else {\n      req.body.assign(body, content_length);\n    }\n  }\n\n  if (content_receiver) {\n    req.content_receiver =\n        [content_receiver](const char *data, size_t data_length,\n                           size_t /*offset*/, size_t /*total_length*/) {\n          return content_receiver(data, data_length);\n        };\n  }\n\n  auto res = detail::make_unique<Response>();\n  return send(req, *res, error) ? std::move(res) : nullptr;\n}\n\ninline Result ClientImpl::send_with_content_provider_and_receiver(\n    const std::string &method, const std::string &path, const Headers &headers,\n    const char *body, size_t content_length, ContentProvider content_provider,\n    ContentProviderWithoutLength content_provider_without_length,\n    const std::string &content_type, ContentReceiver content_receiver,\n    UploadProgress progress) {\n  Request req;\n  req.method = method;\n  req.headers = headers;\n  req.path = path;\n  req.upload_progress = std::move(progress);\n  if (max_timeout_msec_ > 0) {\n    req.start_time_ = std::chrono::steady_clock::now();\n  }\n\n  auto error = Error::Success;\n\n  auto res = send_with_content_provider_and_receiver(\n      req, body, content_length, std::move(content_provider),\n      std::move(content_provider_without_length), content_type,\n      std::move(content_receiver), error);\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n  return Result{std::move(res), error, std::move(req.headers), last_ssl_error_,\n                last_backend_error_};\n#else\n  return Result{std::move(res), error, std::move(req.headers)};\n#endif\n}\n\ninline void ClientImpl::output_log(const Request &req,\n                                   const Response &res) const {\n  if (logger_) {\n    std::lock_guard<std::mutex> guard(logger_mutex_);\n    logger_(req, res);\n  }\n}\n\ninline void ClientImpl::output_error_log(const Error &err,\n                                         const Request *req) const {\n  if (error_logger_) {\n    std::lock_guard<std::mutex> guard(logger_mutex_);\n    error_logger_(err, req);\n  }\n}\n\ninline bool ClientImpl::process_request(Stream &strm, Request &req,\n                                        Response &res, bool close_connection,\n                                        Error &error) {\n  // Auto-add Expect: 100-continue for large bodies\n  if (CPPHTTPLIB_EXPECT_100_THRESHOLD > 0 && !req.has_header(\"Expect\")) {\n    auto body_size = req.body.empty() ? req.content_length_ : req.body.size();\n    if (body_size >= CPPHTTPLIB_EXPECT_100_THRESHOLD) {\n      req.set_header(\"Expect\", \"100-continue\");\n    }\n  }\n\n  // Check for Expect: 100-continue\n  auto expect_100_continue = req.get_header_value(\"Expect\") == \"100-continue\";\n\n  // Send request (skip body if using Expect: 100-continue)\n  auto write_request_success =\n      write_request(strm, req, close_connection, error, expect_100_continue);\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n  if (is_ssl() && !expect_100_continue) {\n    auto is_proxy_enabled = !proxy_host_.empty() && proxy_port_ != -1;\n    if (!is_proxy_enabled) {\n      if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {\n        error = Error::SSLPeerCouldBeClosed_;\n        output_error_log(error, &req);\n        return false;\n      }\n    }\n  }\n#endif\n\n  // Handle Expect: 100-continue with timeout\n  if (expect_100_continue && CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND > 0) {\n    time_t sec = CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND / 1000;\n    time_t usec = (CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND % 1000) * 1000;\n    auto ret = detail::select_read(strm.socket(), sec, usec);\n    if (ret <= 0) {\n      // Timeout or error: send body anyway (server didn't respond in time)\n      if (!write_request_body(strm, req, error)) { return false; }\n      expect_100_continue = false; // Switch to normal response handling\n    }\n  }\n\n  // Receive response and headers\n  // When using Expect: 100-continue, don't auto-skip `100 Continue` response\n  if (!read_response_line(strm, req, res, !expect_100_continue) ||\n      !detail::read_headers(strm, res.headers)) {\n    if (write_request_success) { error = Error::Read; }\n    output_error_log(error, &req);\n    return false;\n  }\n\n  if (!write_request_success) { return false; }\n\n  // Handle Expect: 100-continue response\n  if (expect_100_continue) {\n    if (res.status == StatusCode::Continue_100) {\n      // Server accepted, send the body\n      if (!write_request_body(strm, req, error)) { return false; }\n\n      // Read the actual response\n      res.headers.clear();\n      res.body.clear();\n      if (!read_response_line(strm, req, res) ||\n          !detail::read_headers(strm, res.headers)) {\n        error = Error::Read;\n        output_error_log(error, &req);\n        return false;\n      }\n    }\n    // If not 100 Continue, server returned an error; proceed with that response\n  }\n\n  // Body\n  if ((res.status != StatusCode::NoContent_204) && req.method != \"HEAD\" &&\n      req.method != \"CONNECT\") {\n    auto redirect = 300 < res.status && res.status < 400 &&\n                    res.status != StatusCode::NotModified_304 &&\n                    follow_location_;\n\n    if (req.response_handler && !redirect) {\n      if (!req.response_handler(res)) {\n        error = Error::Canceled;\n        output_error_log(error, &req);\n        return false;\n      }\n    }\n\n    auto out =\n        req.content_receiver\n            ? static_cast<ContentReceiverWithProgress>(\n                  [&](const char *buf, size_t n, size_t off, size_t len) {\n                    if (redirect) { return true; }\n                    auto ret = req.content_receiver(buf, n, off, len);\n                    if (!ret) {\n                      error = Error::Canceled;\n                      output_error_log(error, &req);\n                    }\n                    return ret;\n                  })\n            : static_cast<ContentReceiverWithProgress>(\n                  [&](const char *buf, size_t n, size_t /*off*/,\n                      size_t /*len*/) {\n                    assert(res.body.size() + n <= res.body.max_size());\n                    if (payload_max_length_ > 0 &&\n                        (res.body.size() >= payload_max_length_ ||\n                         n > payload_max_length_ - res.body.size())) {\n                      return false;\n                    }\n                    res.body.append(buf, n);\n                    return true;\n                  });\n\n    auto progress = [&](size_t current, size_t total) {\n      if (!req.download_progress || redirect) { return true; }\n      auto ret = req.download_progress(current, total);\n      if (!ret) {\n        error = Error::Canceled;\n        output_error_log(error, &req);\n      }\n      return ret;\n    };\n\n    if (res.has_header(\"Content-Length\")) {\n      if (!req.content_receiver) {\n        auto len = res.get_header_value_u64(\"Content-Length\");\n        if (len > res.body.max_size()) {\n          error = Error::Read;\n          output_error_log(error, &req);\n          return false;\n        }\n        res.body.reserve(static_cast<size_t>(len));\n      }\n    }\n\n    if (res.status != StatusCode::NotModified_304) {\n      int dummy_status;\n      auto max_length = (!has_payload_max_length_ && req.content_receiver)\n                            ? (std::numeric_limits<size_t>::max)()\n                            : payload_max_length_;\n      if (!detail::read_content(strm, res, max_length, dummy_status,\n                                std::move(progress), std::move(out),\n                                decompress_)) {\n        if (error != Error::Canceled) { error = Error::Read; }\n        output_error_log(error, &req);\n        return false;\n      }\n    }\n  }\n\n  // Log\n  output_log(req, res);\n\n  return true;\n}\n\ninline ContentProviderWithoutLength ClientImpl::get_multipart_content_provider(\n    const std::string &boundary, const UploadFormDataItems &items,\n    const FormDataProviderItems &provider_items) const {\n  size_t cur_item = 0;\n  size_t cur_start = 0;\n  // cur_item and cur_start are copied to within the std::function and\n  // maintain state between successive calls\n  return [&, cur_item, cur_start](size_t offset,\n                                  DataSink &sink) mutable -> bool {\n    if (!offset && !items.empty()) {\n      sink.os << detail::serialize_multipart_formdata(items, boundary, false);\n      return true;\n    } else if (cur_item < provider_items.size()) {\n      if (!cur_start) {\n        const auto &begin = detail::serialize_multipart_formdata_item_begin(\n            provider_items[cur_item], boundary);\n        offset += begin.size();\n        cur_start = offset;\n        sink.os << begin;\n      }\n\n      DataSink cur_sink;\n      auto has_data = true;\n      cur_sink.write = sink.write;\n      cur_sink.done = [&]() { has_data = false; };\n\n      if (!provider_items[cur_item].provider(offset - cur_start, cur_sink)) {\n        return false;\n      }\n\n      if (!has_data) {\n        sink.os << detail::serialize_multipart_formdata_item_end();\n        cur_item++;\n        cur_start = 0;\n      }\n      return true;\n    } else {\n      sink.os << detail::serialize_multipart_formdata_finish(boundary);\n      sink.done();\n      return true;\n    }\n  };\n}\n\ninline bool ClientImpl::process_socket(\n    const Socket &socket,\n    std::chrono::time_point<std::chrono::steady_clock> start_time,\n    std::function<bool(Stream &strm)> callback) {\n  return detail::process_client_socket(\n      socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,\n      write_timeout_usec_, max_timeout_msec_, start_time, std::move(callback));\n}\n\ninline bool ClientImpl::is_ssl() const { return false; }\n\ninline Result ClientImpl::Get(const std::string &path,\n                              DownloadProgress progress) {\n  return Get(path, Headers(), std::move(progress));\n}\n\ninline Result ClientImpl::Get(const std::string &path, const Params &params,\n                              const Headers &headers,\n                              DownloadProgress progress) {\n  if (params.empty()) { return Get(path, headers); }\n\n  std::string path_with_query = append_query_params(path, params);\n  return Get(path_with_query, headers, std::move(progress));\n}\n\ninline Result ClientImpl::Get(const std::string &path, const Headers &headers,\n                              DownloadProgress progress) {\n  Request req;\n  req.method = \"GET\";\n  req.path = path;\n  req.headers = headers;\n  req.download_progress = std::move(progress);\n  if (max_timeout_msec_ > 0) {\n    req.start_time_ = std::chrono::steady_clock::now();\n  }\n\n  return send_(std::move(req));\n}\n\ninline Result ClientImpl::Get(const std::string &path,\n                              ContentReceiver content_receiver,\n                              DownloadProgress progress) {\n  return Get(path, Headers(), nullptr, std::move(content_receiver),\n             std::move(progress));\n}\n\ninline Result ClientImpl::Get(const std::string &path, const Headers &headers,\n                              ContentReceiver content_receiver,\n                              DownloadProgress progress) {\n  return Get(path, headers, nullptr, std::move(content_receiver),\n             std::move(progress));\n}\n\ninline Result ClientImpl::Get(const std::string &path,\n                              ResponseHandler response_handler,\n                              ContentReceiver content_receiver,\n                              DownloadProgress progress) {\n  return Get(path, Headers(), std::move(response_handler),\n             std::move(content_receiver), std::move(progress));\n}\n\ninline Result ClientImpl::Get(const std::string &path, const Headers &headers,\n                              ResponseHandler response_handler,\n                              ContentReceiver content_receiver,\n                              DownloadProgress progress) {\n  Request req;\n  req.method = \"GET\";\n  req.path = path;\n  req.headers = headers;\n  req.response_handler = std::move(response_handler);\n  req.content_receiver =\n      [content_receiver](const char *data, size_t data_length,\n                         size_t /*offset*/, size_t /*total_length*/) {\n        return content_receiver(data, data_length);\n      };\n  req.download_progress = std::move(progress);\n  if (max_timeout_msec_ > 0) {\n    req.start_time_ = std::chrono::steady_clock::now();\n  }\n\n  return send_(std::move(req));\n}\n\ninline Result ClientImpl::Get(const std::string &path, const Params &params,\n                              const Headers &headers,\n                              ContentReceiver content_receiver,\n                              DownloadProgress progress) {\n  return Get(path, params, headers, nullptr, std::move(content_receiver),\n             std::move(progress));\n}\n\ninline Result ClientImpl::Get(const std::string &path, const Params &params,\n                              const Headers &headers,\n                              ResponseHandler response_handler,\n                              ContentReceiver content_receiver,\n                              DownloadProgress progress) {\n  if (params.empty()) {\n    return Get(path, headers, std::move(response_handler),\n               std::move(content_receiver), std::move(progress));\n  }\n\n  std::string path_with_query = append_query_params(path, params);\n  return Get(path_with_query, headers, std::move(response_handler),\n             std::move(content_receiver), std::move(progress));\n}\n\ninline Result ClientImpl::Head(const std::string &path) {\n  return Head(path, Headers());\n}\n\ninline Result ClientImpl::Head(const std::string &path,\n                               const Headers &headers) {\n  Request req;\n  req.method = \"HEAD\";\n  req.headers = headers;\n  req.path = path;\n  if (max_timeout_msec_ > 0) {\n    req.start_time_ = std::chrono::steady_clock::now();\n  }\n\n  return send_(std::move(req));\n}\n\ninline Result ClientImpl::Post(const std::string &path) {\n  return Post(path, std::string(), std::string());\n}\n\ninline Result ClientImpl::Post(const std::string &path,\n                               const Headers &headers) {\n  return Post(path, headers, nullptr, 0, std::string());\n}\n\ninline Result ClientImpl::Post(const std::string &path, const char *body,\n                               size_t content_length,\n                               const std::string &content_type,\n                               UploadProgress progress) {\n  return Post(path, Headers(), body, content_length, content_type, progress);\n}\n\ninline Result ClientImpl::Post(const std::string &path, const std::string &body,\n                               const std::string &content_type,\n                               UploadProgress progress) {\n  return Post(path, Headers(), body, content_type, progress);\n}\n\ninline Result ClientImpl::Post(const std::string &path, const Params &params) {\n  return Post(path, Headers(), params);\n}\n\ninline Result ClientImpl::Post(const std::string &path, size_t content_length,\n                               ContentProvider content_provider,\n                               const std::string &content_type,\n                               UploadProgress progress) {\n  return Post(path, Headers(), content_length, std::move(content_provider),\n              content_type, progress);\n}\n\ninline Result ClientImpl::Post(const std::string &path, size_t content_length,\n                               ContentProvider content_provider,\n                               const std::string &content_type,\n                               ContentReceiver content_receiver,\n                               UploadProgress progress) {\n  return Post(path, Headers(), content_length, std::move(content_provider),\n              content_type, std::move(content_receiver), progress);\n}\n\ninline Result ClientImpl::Post(const std::string &path,\n                               ContentProviderWithoutLength content_provider,\n                               const std::string &content_type,\n                               UploadProgress progress) {\n  return Post(path, Headers(), std::move(content_provider), content_type,\n              progress);\n}\n\ninline Result ClientImpl::Post(const std::string &path,\n                               ContentProviderWithoutLength content_provider,\n                               const std::string &content_type,\n                               ContentReceiver content_receiver,\n                               UploadProgress progress) {\n  return Post(path, Headers(), std::move(content_provider), content_type,\n              std::move(content_receiver), progress);\n}\n\ninline Result ClientImpl::Post(const std::string &path, const Headers &headers,\n                               const Params &params) {\n  auto query = detail::params_to_query_str(params);\n  return Post(path, headers, query, \"application/x-www-form-urlencoded\");\n}\n\ninline Result ClientImpl::Post(const std::string &path,\n                               const UploadFormDataItems &items,\n                               UploadProgress progress) {\n  return Post(path, Headers(), items, progress);\n}\n\ninline Result ClientImpl::Post(const std::string &path, const Headers &headers,\n                               const UploadFormDataItems &items,\n                               UploadProgress progress) {\n  const auto &boundary = detail::make_multipart_data_boundary();\n  const auto &content_type =\n      detail::serialize_multipart_formdata_get_content_type(boundary);\n  const auto &body = detail::serialize_multipart_formdata(items, boundary);\n  return Post(path, headers, body, content_type, progress);\n}\n\ninline Result ClientImpl::Post(const std::string &path, const Headers &headers,\n                               const UploadFormDataItems &items,\n                               const std::string &boundary,\n                               UploadProgress progress) {\n  if (!detail::is_multipart_boundary_chars_valid(boundary)) {\n    return Result{nullptr, Error::UnsupportedMultipartBoundaryChars};\n  }\n\n  const auto &content_type =\n      detail::serialize_multipart_formdata_get_content_type(boundary);\n  const auto &body = detail::serialize_multipart_formdata(items, boundary);\n  return Post(path, headers, body, content_type, progress);\n}\n\ninline Result ClientImpl::Post(const std::string &path, const Headers &headers,\n                               const char *body, size_t content_length,\n                               const std::string &content_type,\n                               UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"POST\", path, headers, body, content_length, nullptr, nullptr,\n      content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Post(const std::string &path, const Headers &headers,\n                               const std::string &body,\n                               const std::string &content_type,\n                               UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"POST\", path, headers, body.data(), body.size(), nullptr, nullptr,\n      content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Post(const std::string &path, const Headers &headers,\n                               size_t content_length,\n                               ContentProvider content_provider,\n                               const std::string &content_type,\n                               UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"POST\", path, headers, nullptr, content_length,\n      std::move(content_provider), nullptr, content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Post(const std::string &path, const Headers &headers,\n                               size_t content_length,\n                               ContentProvider content_provider,\n                               const std::string &content_type,\n                               ContentReceiver content_receiver,\n                               DownloadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"POST\", path, headers, nullptr, content_length,\n      std::move(content_provider), nullptr, content_type,\n      std::move(content_receiver), std::move(progress));\n}\n\ninline Result ClientImpl::Post(const std::string &path, const Headers &headers,\n                               ContentProviderWithoutLength content_provider,\n                               const std::string &content_type,\n                               UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"POST\", path, headers, nullptr, 0, nullptr, std::move(content_provider),\n      content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Post(const std::string &path, const Headers &headers,\n                               ContentProviderWithoutLength content_provider,\n                               const std::string &content_type,\n                               ContentReceiver content_receiver,\n                               DownloadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"POST\", path, headers, nullptr, 0, nullptr, std::move(content_provider),\n      content_type, std::move(content_receiver), std::move(progress));\n}\n\ninline Result ClientImpl::Post(const std::string &path, const Headers &headers,\n                               const UploadFormDataItems &items,\n                               const FormDataProviderItems &provider_items,\n                               UploadProgress progress) {\n  const auto &boundary = detail::make_multipart_data_boundary();\n  const auto &content_type =\n      detail::serialize_multipart_formdata_get_content_type(boundary);\n  return send_with_content_provider_and_receiver(\n      \"POST\", path, headers, nullptr, 0, nullptr,\n      get_multipart_content_provider(boundary, items, provider_items),\n      content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Post(const std::string &path, const Headers &headers,\n                               const std::string &body,\n                               const std::string &content_type,\n                               ContentReceiver content_receiver,\n                               DownloadProgress progress) {\n  Request req;\n  req.method = \"POST\";\n  req.path = path;\n  req.headers = headers;\n  req.body = body;\n  req.content_receiver =\n      [content_receiver](const char *data, size_t data_length,\n                         size_t /*offset*/, size_t /*total_length*/) {\n        return content_receiver(data, data_length);\n      };\n  req.download_progress = std::move(progress);\n\n  if (max_timeout_msec_ > 0) {\n    req.start_time_ = std::chrono::steady_clock::now();\n  }\n\n  if (!content_type.empty()) { req.set_header(\"Content-Type\", content_type); }\n\n  return send_(std::move(req));\n}\n\ninline Result ClientImpl::Put(const std::string &path) {\n  return Put(path, std::string(), std::string());\n}\n\ninline Result ClientImpl::Put(const std::string &path, const Headers &headers) {\n  return Put(path, headers, nullptr, 0, std::string());\n}\n\ninline Result ClientImpl::Put(const std::string &path, const char *body,\n                              size_t content_length,\n                              const std::string &content_type,\n                              UploadProgress progress) {\n  return Put(path, Headers(), body, content_length, content_type, progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path, const std::string &body,\n                              const std::string &content_type,\n                              UploadProgress progress) {\n  return Put(path, Headers(), body, content_type, progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path, const Params &params) {\n  return Put(path, Headers(), params);\n}\n\ninline Result ClientImpl::Put(const std::string &path, size_t content_length,\n                              ContentProvider content_provider,\n                              const std::string &content_type,\n                              UploadProgress progress) {\n  return Put(path, Headers(), content_length, std::move(content_provider),\n             content_type, progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path, size_t content_length,\n                              ContentProvider content_provider,\n                              const std::string &content_type,\n                              ContentReceiver content_receiver,\n                              UploadProgress progress) {\n  return Put(path, Headers(), content_length, std::move(content_provider),\n             content_type, std::move(content_receiver), progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path,\n                              ContentProviderWithoutLength content_provider,\n                              const std::string &content_type,\n                              UploadProgress progress) {\n  return Put(path, Headers(), std::move(content_provider), content_type,\n             progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path,\n                              ContentProviderWithoutLength content_provider,\n                              const std::string &content_type,\n                              ContentReceiver content_receiver,\n                              UploadProgress progress) {\n  return Put(path, Headers(), std::move(content_provider), content_type,\n             std::move(content_receiver), progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path, const Headers &headers,\n                              const Params &params) {\n  auto query = detail::params_to_query_str(params);\n  return Put(path, headers, query, \"application/x-www-form-urlencoded\");\n}\n\ninline Result ClientImpl::Put(const std::string &path,\n                              const UploadFormDataItems &items,\n                              UploadProgress progress) {\n  return Put(path, Headers(), items, progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path, const Headers &headers,\n                              const UploadFormDataItems &items,\n                              UploadProgress progress) {\n  const auto &boundary = detail::make_multipart_data_boundary();\n  const auto &content_type =\n      detail::serialize_multipart_formdata_get_content_type(boundary);\n  const auto &body = detail::serialize_multipart_formdata(items, boundary);\n  return Put(path, headers, body, content_type, progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path, const Headers &headers,\n                              const UploadFormDataItems &items,\n                              const std::string &boundary,\n                              UploadProgress progress) {\n  if (!detail::is_multipart_boundary_chars_valid(boundary)) {\n    return Result{nullptr, Error::UnsupportedMultipartBoundaryChars};\n  }\n\n  const auto &content_type =\n      detail::serialize_multipart_formdata_get_content_type(boundary);\n  const auto &body = detail::serialize_multipart_formdata(items, boundary);\n  return Put(path, headers, body, content_type, progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path, const Headers &headers,\n                              const char *body, size_t content_length,\n                              const std::string &content_type,\n                              UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"PUT\", path, headers, body, content_length, nullptr, nullptr,\n      content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path, const Headers &headers,\n                              const std::string &body,\n                              const std::string &content_type,\n                              UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"PUT\", path, headers, body.data(), body.size(), nullptr, nullptr,\n      content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path, const Headers &headers,\n                              size_t content_length,\n                              ContentProvider content_provider,\n                              const std::string &content_type,\n                              UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"PUT\", path, headers, nullptr, content_length,\n      std::move(content_provider), nullptr, content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path, const Headers &headers,\n                              size_t content_length,\n                              ContentProvider content_provider,\n                              const std::string &content_type,\n                              ContentReceiver content_receiver,\n                              UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"PUT\", path, headers, nullptr, content_length,\n      std::move(content_provider), nullptr, content_type,\n      std::move(content_receiver), progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path, const Headers &headers,\n                              ContentProviderWithoutLength content_provider,\n                              const std::string &content_type,\n                              UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"PUT\", path, headers, nullptr, 0, nullptr, std::move(content_provider),\n      content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path, const Headers &headers,\n                              ContentProviderWithoutLength content_provider,\n                              const std::string &content_type,\n                              ContentReceiver content_receiver,\n                              UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"PUT\", path, headers, nullptr, 0, nullptr, std::move(content_provider),\n      content_type, std::move(content_receiver), progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path, const Headers &headers,\n                              const UploadFormDataItems &items,\n                              const FormDataProviderItems &provider_items,\n                              UploadProgress progress) {\n  const auto &boundary = detail::make_multipart_data_boundary();\n  const auto &content_type =\n      detail::serialize_multipart_formdata_get_content_type(boundary);\n  return send_with_content_provider_and_receiver(\n      \"PUT\", path, headers, nullptr, 0, nullptr,\n      get_multipart_content_provider(boundary, items, provider_items),\n      content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Put(const std::string &path, const Headers &headers,\n                              const std::string &body,\n                              const std::string &content_type,\n                              ContentReceiver content_receiver,\n                              DownloadProgress progress) {\n  Request req;\n  req.method = \"PUT\";\n  req.path = path;\n  req.headers = headers;\n  req.body = body;\n  req.content_receiver =\n      [content_receiver](const char *data, size_t data_length,\n                         size_t /*offset*/, size_t /*total_length*/) {\n        return content_receiver(data, data_length);\n      };\n  req.download_progress = std::move(progress);\n\n  if (max_timeout_msec_ > 0) {\n    req.start_time_ = std::chrono::steady_clock::now();\n  }\n\n  if (!content_type.empty()) { req.set_header(\"Content-Type\", content_type); }\n\n  return send_(std::move(req));\n}\n\ninline Result ClientImpl::Patch(const std::string &path) {\n  return Patch(path, std::string(), std::string());\n}\n\ninline Result ClientImpl::Patch(const std::string &path, const Headers &headers,\n                                UploadProgress progress) {\n  return Patch(path, headers, nullptr, 0, std::string(), progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, const char *body,\n                                size_t content_length,\n                                const std::string &content_type,\n                                UploadProgress progress) {\n  return Patch(path, Headers(), body, content_length, content_type, progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path,\n                                const std::string &body,\n                                const std::string &content_type,\n                                UploadProgress progress) {\n  return Patch(path, Headers(), body, content_type, progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, const Params &params) {\n  return Patch(path, Headers(), params);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, size_t content_length,\n                                ContentProvider content_provider,\n                                const std::string &content_type,\n                                UploadProgress progress) {\n  return Patch(path, Headers(), content_length, std::move(content_provider),\n               content_type, progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, size_t content_length,\n                                ContentProvider content_provider,\n                                const std::string &content_type,\n                                ContentReceiver content_receiver,\n                                UploadProgress progress) {\n  return Patch(path, Headers(), content_length, std::move(content_provider),\n               content_type, std::move(content_receiver), progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path,\n                                ContentProviderWithoutLength content_provider,\n                                const std::string &content_type,\n                                UploadProgress progress) {\n  return Patch(path, Headers(), std::move(content_provider), content_type,\n               progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path,\n                                ContentProviderWithoutLength content_provider,\n                                const std::string &content_type,\n                                ContentReceiver content_receiver,\n                                UploadProgress progress) {\n  return Patch(path, Headers(), std::move(content_provider), content_type,\n               std::move(content_receiver), progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, const Headers &headers,\n                                const Params &params) {\n  auto query = detail::params_to_query_str(params);\n  return Patch(path, headers, query, \"application/x-www-form-urlencoded\");\n}\n\ninline Result ClientImpl::Patch(const std::string &path,\n                                const UploadFormDataItems &items,\n                                UploadProgress progress) {\n  return Patch(path, Headers(), items, progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, const Headers &headers,\n                                const UploadFormDataItems &items,\n                                UploadProgress progress) {\n  const auto &boundary = detail::make_multipart_data_boundary();\n  const auto &content_type =\n      detail::serialize_multipart_formdata_get_content_type(boundary);\n  const auto &body = detail::serialize_multipart_formdata(items, boundary);\n  return Patch(path, headers, body, content_type, progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, const Headers &headers,\n                                const UploadFormDataItems &items,\n                                const std::string &boundary,\n                                UploadProgress progress) {\n  if (!detail::is_multipart_boundary_chars_valid(boundary)) {\n    return Result{nullptr, Error::UnsupportedMultipartBoundaryChars};\n  }\n\n  const auto &content_type =\n      detail::serialize_multipart_formdata_get_content_type(boundary);\n  const auto &body = detail::serialize_multipart_formdata(items, boundary);\n  return Patch(path, headers, body, content_type, progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, const Headers &headers,\n                                const char *body, size_t content_length,\n                                const std::string &content_type,\n                                UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"PATCH\", path, headers, body, content_length, nullptr, nullptr,\n      content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, const Headers &headers,\n                                const std::string &body,\n                                const std::string &content_type,\n                                UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"PATCH\", path, headers, body.data(), body.size(), nullptr, nullptr,\n      content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, const Headers &headers,\n                                size_t content_length,\n                                ContentProvider content_provider,\n                                const std::string &content_type,\n                                UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"PATCH\", path, headers, nullptr, content_length,\n      std::move(content_provider), nullptr, content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, const Headers &headers,\n                                size_t content_length,\n                                ContentProvider content_provider,\n                                const std::string &content_type,\n                                ContentReceiver content_receiver,\n                                UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"PATCH\", path, headers, nullptr, content_length,\n      std::move(content_provider), nullptr, content_type,\n      std::move(content_receiver), progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, const Headers &headers,\n                                ContentProviderWithoutLength content_provider,\n                                const std::string &content_type,\n                                UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"PATCH\", path, headers, nullptr, 0, nullptr, std::move(content_provider),\n      content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, const Headers &headers,\n                                ContentProviderWithoutLength content_provider,\n                                const std::string &content_type,\n                                ContentReceiver content_receiver,\n                                UploadProgress progress) {\n  return send_with_content_provider_and_receiver(\n      \"PATCH\", path, headers, nullptr, 0, nullptr, std::move(content_provider),\n      content_type, std::move(content_receiver), progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, const Headers &headers,\n                                const UploadFormDataItems &items,\n                                const FormDataProviderItems &provider_items,\n                                UploadProgress progress) {\n  const auto &boundary = detail::make_multipart_data_boundary();\n  const auto &content_type =\n      detail::serialize_multipart_formdata_get_content_type(boundary);\n  return send_with_content_provider_and_receiver(\n      \"PATCH\", path, headers, nullptr, 0, nullptr,\n      get_multipart_content_provider(boundary, items, provider_items),\n      content_type, nullptr, progress);\n}\n\ninline Result ClientImpl::Patch(const std::string &path, const Headers &headers,\n                                const std::string &body,\n                                const std::string &content_type,\n                                ContentReceiver content_receiver,\n                                DownloadProgress progress) {\n  Request req;\n  req.method = \"PATCH\";\n  req.path = path;\n  req.headers = headers;\n  req.body = body;\n  req.content_receiver =\n      [content_receiver](const char *data, size_t data_length,\n                         size_t /*offset*/, size_t /*total_length*/) {\n        return content_receiver(data, data_length);\n      };\n  req.download_progress = std::move(progress);\n\n  if (max_timeout_msec_ > 0) {\n    req.start_time_ = std::chrono::steady_clock::now();\n  }\n\n  if (!content_type.empty()) { req.set_header(\"Content-Type\", content_type); }\n\n  return send_(std::move(req));\n}\n\ninline Result ClientImpl::Delete(const std::string &path,\n                                 DownloadProgress progress) {\n  return Delete(path, Headers(), std::string(), std::string(), progress);\n}\n\ninline Result ClientImpl::Delete(const std::string &path,\n                                 const Headers &headers,\n                                 DownloadProgress progress) {\n  return Delete(path, headers, std::string(), std::string(), progress);\n}\n\ninline Result ClientImpl::Delete(const std::string &path, const char *body,\n                                 size_t content_length,\n                                 const std::string &content_type,\n                                 DownloadProgress progress) {\n  return Delete(path, Headers(), body, content_length, content_type, progress);\n}\n\ninline Result ClientImpl::Delete(const std::string &path,\n                                 const std::string &body,\n                                 const std::string &content_type,\n                                 DownloadProgress progress) {\n  return Delete(path, Headers(), body.data(), body.size(), content_type,\n                progress);\n}\n\ninline Result ClientImpl::Delete(const std::string &path,\n                                 const Headers &headers,\n                                 const std::string &body,\n                                 const std::string &content_type,\n                                 DownloadProgress progress) {\n  return Delete(path, headers, body.data(), body.size(), content_type,\n                progress);\n}\n\ninline Result ClientImpl::Delete(const std::string &path, const Params &params,\n                                 DownloadProgress progress) {\n  return Delete(path, Headers(), params, progress);\n}\n\ninline Result ClientImpl::Delete(const std::string &path,\n                                 const Headers &headers, const Params &params,\n                                 DownloadProgress progress) {\n  auto query = detail::params_to_query_str(params);\n  return Delete(path, headers, query, \"application/x-www-form-urlencoded\",\n                progress);\n}\n\ninline Result ClientImpl::Delete(const std::string &path,\n                                 const Headers &headers, const char *body,\n                                 size_t content_length,\n                                 const std::string &content_type,\n                                 DownloadProgress progress) {\n  Request req;\n  req.method = \"DELETE\";\n  req.headers = headers;\n  req.path = path;\n  req.download_progress = std::move(progress);\n  if (max_timeout_msec_ > 0) {\n    req.start_time_ = std::chrono::steady_clock::now();\n  }\n\n  if (!content_type.empty()) { req.set_header(\"Content-Type\", content_type); }\n  req.body.assign(body, content_length);\n\n  return send_(std::move(req));\n}\n\ninline Result ClientImpl::Options(const std::string &path) {\n  return Options(path, Headers());\n}\n\ninline Result ClientImpl::Options(const std::string &path,\n                                  const Headers &headers) {\n  Request req;\n  req.method = \"OPTIONS\";\n  req.headers = headers;\n  req.path = path;\n  if (max_timeout_msec_ > 0) {\n    req.start_time_ = std::chrono::steady_clock::now();\n  }\n\n  return send_(std::move(req));\n}\n\ninline void ClientImpl::stop() {\n  std::lock_guard<std::mutex> guard(socket_mutex_);\n\n  // If there is anything ongoing right now, the ONLY thread-safe thing we can\n  // do is to shutdown_socket, so that threads using this socket suddenly\n  // discover they can't read/write any more and error out. Everything else\n  // (closing the socket, shutting ssl down) is unsafe because these actions\n  // are not thread-safe.\n  if (socket_requests_in_flight_ > 0) {\n    shutdown_socket(socket_);\n\n    // Aside from that, we set a flag for the socket to be closed when we're\n    // done.\n    socket_should_be_closed_when_request_is_done_ = true;\n    return;\n  }\n\n  // Otherwise, still holding the mutex, we can shut everything down ourselves\n  shutdown_ssl(socket_, true);\n  shutdown_socket(socket_);\n  close_socket(socket_);\n}\n\ninline std::string ClientImpl::host() const { return host_; }\n\ninline int ClientImpl::port() const { return port_; }\n\ninline size_t ClientImpl::is_socket_open() const {\n  std::lock_guard<std::mutex> guard(socket_mutex_);\n  return socket_.is_open();\n}\n\ninline socket_t ClientImpl::socket() const { return socket_.sock; }\n\ninline void ClientImpl::set_connection_timeout(time_t sec, time_t usec) {\n  connection_timeout_sec_ = sec;\n  connection_timeout_usec_ = usec;\n}\n\ninline void ClientImpl::set_read_timeout(time_t sec, time_t usec) {\n  read_timeout_sec_ = sec;\n  read_timeout_usec_ = usec;\n}\n\ninline void ClientImpl::set_write_timeout(time_t sec, time_t usec) {\n  write_timeout_sec_ = sec;\n  write_timeout_usec_ = usec;\n}\n\ninline void ClientImpl::set_max_timeout(time_t msec) {\n  max_timeout_msec_ = msec;\n}\n\ninline void ClientImpl::set_basic_auth(const std::string &username,\n                                       const std::string &password) {\n  basic_auth_username_ = username;\n  basic_auth_password_ = password;\n}\n\ninline void ClientImpl::set_bearer_token_auth(const std::string &token) {\n  bearer_token_auth_token_ = token;\n}\n\ninline void ClientImpl::set_keep_alive(bool on) { keep_alive_ = on; }\n\ninline void ClientImpl::set_follow_location(bool on) { follow_location_ = on; }\n\ninline void ClientImpl::set_path_encode(bool on) { path_encode_ = on; }\n\ninline void\nClientImpl::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {\n  addr_map_ = std::move(addr_map);\n}\n\ninline void ClientImpl::set_default_headers(Headers headers) {\n  default_headers_ = std::move(headers);\n}\n\ninline void ClientImpl::set_header_writer(\n    std::function<ssize_t(Stream &, Headers &)> const &writer) {\n  header_writer_ = writer;\n}\n\ninline void ClientImpl::set_address_family(int family) {\n  address_family_ = family;\n}\n\ninline void ClientImpl::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; }\n\ninline void ClientImpl::set_ipv6_v6only(bool on) { ipv6_v6only_ = on; }\n\ninline void ClientImpl::set_socket_options(SocketOptions socket_options) {\n  socket_options_ = std::move(socket_options);\n}\n\ninline void ClientImpl::set_compress(bool on) { compress_ = on; }\n\ninline void ClientImpl::set_decompress(bool on) { decompress_ = on; }\n\ninline void ClientImpl::set_payload_max_length(size_t length) {\n  payload_max_length_ = length;\n  has_payload_max_length_ = true;\n}\n\ninline void ClientImpl::set_interface(const std::string &intf) {\n  interface_ = intf;\n}\n\ninline void ClientImpl::set_proxy(const std::string &host, int port) {\n  proxy_host_ = host;\n  proxy_port_ = port;\n}\n\ninline void ClientImpl::set_proxy_basic_auth(const std::string &username,\n                                             const std::string &password) {\n  proxy_basic_auth_username_ = username;\n  proxy_basic_auth_password_ = password;\n}\n\ninline void ClientImpl::set_proxy_bearer_token_auth(const std::string &token) {\n  proxy_bearer_token_auth_token_ = token;\n}\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\ninline void ClientImpl::set_digest_auth(const std::string &username,\n                                        const std::string &password) {\n  digest_auth_username_ = username;\n  digest_auth_password_ = password;\n}\n\ninline void ClientImpl::set_ca_cert_path(const std::string &ca_cert_file_path,\n                                         const std::string &ca_cert_dir_path) {\n  ca_cert_file_path_ = ca_cert_file_path;\n  ca_cert_dir_path_ = ca_cert_dir_path;\n}\n\ninline void ClientImpl::set_proxy_digest_auth(const std::string &username,\n                                              const std::string &password) {\n  proxy_digest_auth_username_ = username;\n  proxy_digest_auth_password_ = password;\n}\n\ninline void ClientImpl::enable_server_certificate_verification(bool enabled) {\n  server_certificate_verification_ = enabled;\n}\n\ninline void ClientImpl::enable_server_hostname_verification(bool enabled) {\n  server_hostname_verification_ = enabled;\n}\n#endif\n\n// ClientImpl::set_ca_cert_store is defined after TLS namespace (uses helpers)\n#ifdef CPPHTTPLIB_OPENSSL_SUPPORT\ninline X509_STORE *ClientImpl::create_ca_cert_store(const char *ca_cert,\n                                                    std::size_t size) const {\n  auto mem = BIO_new_mem_buf(ca_cert, static_cast<int>(size));\n  auto se = detail::scope_exit([&] { BIO_free_all(mem); });\n  if (!mem) { return nullptr; }\n\n  auto inf = PEM_X509_INFO_read_bio(mem, nullptr, nullptr, nullptr);\n  if (!inf) { return nullptr; }\n\n  auto cts = X509_STORE_new();\n  if (cts) {\n    for (auto i = 0; i < static_cast<int>(sk_X509_INFO_num(inf)); i++) {\n      auto itmp = sk_X509_INFO_value(inf, i);\n      if (!itmp) { continue; }\n\n      if (itmp->x509) { X509_STORE_add_cert(cts, itmp->x509); }\n      if (itmp->crl) { X509_STORE_add_crl(cts, itmp->crl); }\n    }\n  }\n\n  sk_X509_INFO_pop_free(inf, X509_INFO_free);\n  return cts;\n}\n\ninline void ClientImpl::set_server_certificate_verifier(\n    std::function<SSLVerifierResponse(SSL *ssl)> /*verifier*/) {\n  // Base implementation does nothing - SSLClient overrides this\n}\n#endif\n\ninline void ClientImpl::set_logger(Logger logger) {\n  logger_ = std::move(logger);\n}\n\ninline void ClientImpl::set_error_logger(ErrorLogger error_logger) {\n  error_logger_ = std::move(error_logger);\n}\n\n/*\n * SSL/TLS Common Implementation\n */\n\ninline ClientConnection::~ClientConnection() {\n#ifdef CPPHTTPLIB_SSL_ENABLED\n  if (session) {\n    tls::shutdown(session, true);\n    tls::free_session(session);\n    session = nullptr;\n  }\n#endif\n\n  if (sock != INVALID_SOCKET) {\n    detail::close_socket(sock);\n    sock = INVALID_SOCKET;\n  }\n}\n\n// Universal client implementation\ninline Client::Client(const std::string &scheme_host_port)\n    : Client(scheme_host_port, std::string(), std::string()) {}\n\ninline Client::Client(const std::string &scheme_host_port,\n                      const std::string &client_cert_path,\n                      const std::string &client_key_path) {\n  const static std::regex re(\n      R\"((?:([a-z]+):\\/\\/)?(?:\\[([a-fA-F\\d:]+)\\]|([^:/?#]+))(?::(\\d+))?)\");\n\n  std::smatch m;\n  if (std::regex_match(scheme_host_port, m, re)) {\n    auto scheme = m[1].str();\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n    if (!scheme.empty() && (scheme != \"http\" && scheme != \"https\")) {\n#else\n    if (!scheme.empty() && scheme != \"http\") {\n#endif\n#ifndef CPPHTTPLIB_NO_EXCEPTIONS\n      std::string msg = \"'\" + scheme + \"' scheme is not supported.\";\n      throw std::invalid_argument(msg);\n#endif\n      return;\n    }\n\n    auto is_ssl = scheme == \"https\";\n\n    auto host = m[2].str();\n    if (host.empty()) { host = m[3].str(); }\n\n    auto port_str = m[4].str();\n    auto port = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80);\n\n    if (is_ssl) {\n#ifdef CPPHTTPLIB_SSL_ENABLED\n      cli_ = detail::make_unique<SSLClient>(host, port, client_cert_path,\n                                            client_key_path);\n      is_ssl_ = is_ssl;\n#endif\n    } else {\n      cli_ = detail::make_unique<ClientImpl>(host, port, client_cert_path,\n                                             client_key_path);\n    }\n  } else {\n    // NOTE: Update TEST(UniversalClientImplTest, Ipv6LiteralAddress)\n    // if port param below changes.\n    cli_ = detail::make_unique<ClientImpl>(scheme_host_port, 80,\n                                           client_cert_path, client_key_path);\n  }\n} // namespace detail\n\ninline Client::Client(const std::string &host, int port)\n    : cli_(detail::make_unique<ClientImpl>(host, port)) {}\n\ninline Client::Client(const std::string &host, int port,\n                      const std::string &client_cert_path,\n                      const std::string &client_key_path)\n    : cli_(detail::make_unique<ClientImpl>(host, port, client_cert_path,\n                                           client_key_path)) {}\n\ninline Client::~Client() = default;\n\ninline bool Client::is_valid() const {\n  return cli_ != nullptr && cli_->is_valid();\n}\n\ninline Result Client::Get(const std::string &path, DownloadProgress progress) {\n  return cli_->Get(path, std::move(progress));\n}\ninline Result Client::Get(const std::string &path, const Headers &headers,\n                          DownloadProgress progress) {\n  return cli_->Get(path, headers, std::move(progress));\n}\ninline Result Client::Get(const std::string &path,\n                          ContentReceiver content_receiver,\n                          DownloadProgress progress) {\n  return cli_->Get(path, std::move(content_receiver), std::move(progress));\n}\ninline Result Client::Get(const std::string &path, const Headers &headers,\n                          ContentReceiver content_receiver,\n                          DownloadProgress progress) {\n  return cli_->Get(path, headers, std::move(content_receiver),\n                   std::move(progress));\n}\ninline Result Client::Get(const std::string &path,\n                          ResponseHandler response_handler,\n                          ContentReceiver content_receiver,\n                          DownloadProgress progress) {\n  return cli_->Get(path, std::move(response_handler),\n                   std::move(content_receiver), std::move(progress));\n}\ninline Result Client::Get(const std::string &path, const Headers &headers,\n                          ResponseHandler response_handler,\n                          ContentReceiver content_receiver,\n                          DownloadProgress progress) {\n  return cli_->Get(path, headers, std::move(response_handler),\n                   std::move(content_receiver), std::move(progress));\n}\ninline Result Client::Get(const std::string &path, const Params &params,\n                          const Headers &headers, DownloadProgress progress) {\n  return cli_->Get(path, params, headers, std::move(progress));\n}\ninline Result Client::Get(const std::string &path, const Params &params,\n                          const Headers &headers,\n                          ContentReceiver content_receiver,\n                          DownloadProgress progress) {\n  return cli_->Get(path, params, headers, std::move(content_receiver),\n                   std::move(progress));\n}\ninline Result Client::Get(const std::string &path, const Params &params,\n                          const Headers &headers,\n                          ResponseHandler response_handler,\n                          ContentReceiver content_receiver,\n                          DownloadProgress progress) {\n  return cli_->Get(path, params, headers, std::move(response_handler),\n                   std::move(content_receiver), std::move(progress));\n}\n\ninline Result Client::Head(const std::string &path) { return cli_->Head(path); }\ninline Result Client::Head(const std::string &path, const Headers &headers) {\n  return cli_->Head(path, headers);\n}\n\ninline Result Client::Post(const std::string &path) { return cli_->Post(path); }\ninline Result Client::Post(const std::string &path, const Headers &headers) {\n  return cli_->Post(path, headers);\n}\ninline Result Client::Post(const std::string &path, const char *body,\n                           size_t content_length,\n                           const std::string &content_type,\n                           UploadProgress progress) {\n  return cli_->Post(path, body, content_length, content_type, progress);\n}\ninline Result Client::Post(const std::string &path, const Headers &headers,\n                           const char *body, size_t content_length,\n                           const std::string &content_type,\n                           UploadProgress progress) {\n  return cli_->Post(path, headers, body, content_length, content_type,\n                    progress);\n}\ninline Result Client::Post(const std::string &path, const std::string &body,\n                           const std::string &content_type,\n                           UploadProgress progress) {\n  return cli_->Post(path, body, content_type, progress);\n}\ninline Result Client::Post(const std::string &path, const Headers &headers,\n                           const std::string &body,\n                           const std::string &content_type,\n                           UploadProgress progress) {\n  return cli_->Post(path, headers, body, content_type, progress);\n}\ninline Result Client::Post(const std::string &path, size_t content_length,\n                           ContentProvider content_provider,\n                           const std::string &content_type,\n                           UploadProgress progress) {\n  return cli_->Post(path, content_length, std::move(content_provider),\n                    content_type, progress);\n}\ninline Result Client::Post(const std::string &path, size_t content_length,\n                           ContentProvider content_provider,\n                           const std::string &content_type,\n                           ContentReceiver content_receiver,\n                           UploadProgress progress) {\n  return cli_->Post(path, content_length, std::move(content_provider),\n                    content_type, std::move(content_receiver), progress);\n}\ninline Result Client::Post(const std::string &path,\n                           ContentProviderWithoutLength content_provider,\n                           const std::string &content_type,\n                           UploadProgress progress) {\n  return cli_->Post(path, std::move(content_provider), content_type, progress);\n}\ninline Result Client::Post(const std::string &path,\n                           ContentProviderWithoutLength content_provider,\n                           const std::string &content_type,\n                           ContentReceiver content_receiver,\n                           UploadProgress progress) {\n  return cli_->Post(path, std::move(content_provider), content_type,\n                    std::move(content_receiver), progress);\n}\ninline Result Client::Post(const std::string &path, const Headers &headers,\n                           size_t content_length,\n                           ContentProvider content_provider,\n                           const std::string &content_type,\n                           UploadProgress progress) {\n  return cli_->Post(path, headers, content_length, std::move(content_provider),\n                    content_type, progress);\n}\ninline Result Client::Post(const std::string &path, const Headers &headers,\n                           size_t content_length,\n                           ContentProvider content_provider,\n                           const std::string &content_type,\n                           ContentReceiver content_receiver,\n                           DownloadProgress progress) {\n  return cli_->Post(path, headers, content_length, std::move(content_provider),\n                    content_type, std::move(content_receiver), progress);\n}\ninline Result Client::Post(const std::string &path, const Headers &headers,\n                           ContentProviderWithoutLength content_provider,\n                           const std::string &content_type,\n                           UploadProgress progress) {\n  return cli_->Post(path, headers, std::move(content_provider), content_type,\n                    progress);\n}\ninline Result Client::Post(const std::string &path, const Headers &headers,\n                           ContentProviderWithoutLength content_provider,\n                           const std::string &content_type,\n                           ContentReceiver content_receiver,\n                           DownloadProgress progress) {\n  return cli_->Post(path, headers, std::move(content_provider), content_type,\n                    std::move(content_receiver), progress);\n}\ninline Result Client::Post(const std::string &path, const Params &params) {\n  return cli_->Post(path, params);\n}\ninline Result Client::Post(const std::string &path, const Headers &headers,\n                           const Params &params) {\n  return cli_->Post(path, headers, params);\n}\ninline Result Client::Post(const std::string &path,\n                           const UploadFormDataItems &items,\n                           UploadProgress progress) {\n  return cli_->Post(path, items, progress);\n}\ninline Result Client::Post(const std::string &path, const Headers &headers,\n                           const UploadFormDataItems &items,\n                           UploadProgress progress) {\n  return cli_->Post(path, headers, items, progress);\n}\ninline Result Client::Post(const std::string &path, const Headers &headers,\n                           const UploadFormDataItems &items,\n                           const std::string &boundary,\n                           UploadProgress progress) {\n  return cli_->Post(path, headers, items, boundary, progress);\n}\ninline Result Client::Post(const std::string &path, const Headers &headers,\n                           const UploadFormDataItems &items,\n                           const FormDataProviderItems &provider_items,\n                           UploadProgress progress) {\n  return cli_->Post(path, headers, items, provider_items, progress);\n}\ninline Result Client::Post(const std::string &path, const Headers &headers,\n                           const std::string &body,\n                           const std::string &content_type,\n                           ContentReceiver content_receiver,\n                           DownloadProgress progress) {\n  return cli_->Post(path, headers, body, content_type,\n                    std::move(content_receiver), progress);\n}\n\ninline Result Client::Put(const std::string &path) { return cli_->Put(path); }\ninline Result Client::Put(const std::string &path, const Headers &headers) {\n  return cli_->Put(path, headers);\n}\ninline Result Client::Put(const std::string &path, const char *body,\n                          size_t content_length,\n                          const std::string &content_type,\n                          UploadProgress progress) {\n  return cli_->Put(path, body, content_length, content_type, progress);\n}\ninline Result Client::Put(const std::string &path, const Headers &headers,\n                          const char *body, size_t content_length,\n                          const std::string &content_type,\n                          UploadProgress progress) {\n  return cli_->Put(path, headers, body, content_length, content_type, progress);\n}\ninline Result Client::Put(const std::string &path, const std::string &body,\n                          const std::string &content_type,\n                          UploadProgress progress) {\n  return cli_->Put(path, body, content_type, progress);\n}\ninline Result Client::Put(const std::string &path, const Headers &headers,\n                          const std::string &body,\n                          const std::string &content_type,\n                          UploadProgress progress) {\n  return cli_->Put(path, headers, body, content_type, progress);\n}\ninline Result Client::Put(const std::string &path, size_t content_length,\n                          ContentProvider content_provider,\n                          const std::string &content_type,\n                          UploadProgress progress) {\n  return cli_->Put(path, content_length, std::move(content_provider),\n                   content_type, progress);\n}\ninline Result Client::Put(const std::string &path, size_t content_length,\n                          ContentProvider content_provider,\n                          const std::string &content_type,\n                          ContentReceiver content_receiver,\n                          UploadProgress progress) {\n  return cli_->Put(path, content_length, std::move(content_provider),\n                   content_type, std::move(content_receiver), progress);\n}\ninline Result Client::Put(const std::string &path,\n                          ContentProviderWithoutLength content_provider,\n                          const std::string &content_type,\n                          UploadProgress progress) {\n  return cli_->Put(path, std::move(content_provider), content_type, progress);\n}\ninline Result Client::Put(const std::string &path,\n                          ContentProviderWithoutLength content_provider,\n                          const std::string &content_type,\n                          ContentReceiver content_receiver,\n                          UploadProgress progress) {\n  return cli_->Put(path, std::move(content_provider), content_type,\n                   std::move(content_receiver), progress);\n}\ninline Result Client::Put(const std::string &path, const Headers &headers,\n                          size_t content_length,\n                          ContentProvider content_provider,\n                          const std::string &content_type,\n                          UploadProgress progress) {\n  return cli_->Put(path, headers, content_length, std::move(content_provider),\n                   content_type, progress);\n}\ninline Result Client::Put(const std::string &path, const Headers &headers,\n                          size_t content_length,\n                          ContentProvider content_provider,\n                          const std::string &content_type,\n                          ContentReceiver content_receiver,\n                          UploadProgress progress) {\n  return cli_->Put(path, headers, content_length, std::move(content_provider),\n                   content_type, std::move(content_receiver), progress);\n}\ninline Result Client::Put(const std::string &path, const Headers &headers,\n                          ContentProviderWithoutLength content_provider,\n                          const std::string &content_type,\n                          UploadProgress progress) {\n  return cli_->Put(path, headers, std::move(content_provider), content_type,\n                   progress);\n}\ninline Result Client::Put(const std::string &path, const Headers &headers,\n                          ContentProviderWithoutLength content_provider,\n                          const std::string &content_type,\n                          ContentReceiver content_receiver,\n                          UploadProgress progress) {\n  return cli_->Put(path, headers, std::move(content_provider), content_type,\n                   std::move(content_receiver), progress);\n}\ninline Result Client::Put(const std::string &path, const Params &params) {\n  return cli_->Put(path, params);\n}\ninline Result Client::Put(const std::string &path, const Headers &headers,\n                          const Params &params) {\n  return cli_->Put(path, headers, params);\n}\ninline Result Client::Put(const std::string &path,\n                          const UploadFormDataItems &items,\n                          UploadProgress progress) {\n  return cli_->Put(path, items, progress);\n}\ninline Result Client::Put(const std::string &path, const Headers &headers,\n                          const UploadFormDataItems &items,\n                          UploadProgress progress) {\n  return cli_->Put(path, headers, items, progress);\n}\ninline Result Client::Put(const std::string &path, const Headers &headers,\n                          const UploadFormDataItems &items,\n                          const std::string &boundary,\n                          UploadProgress progress) {\n  return cli_->Put(path, headers, items, boundary, progress);\n}\ninline Result Client::Put(const std::string &path, const Headers &headers,\n                          const UploadFormDataItems &items,\n                          const FormDataProviderItems &provider_items,\n                          UploadProgress progress) {\n  return cli_->Put(path, headers, items, provider_items, progress);\n}\ninline Result Client::Put(const std::string &path, const Headers &headers,\n                          const std::string &body,\n                          const std::string &content_type,\n                          ContentReceiver content_receiver,\n                          DownloadProgress progress) {\n  return cli_->Put(path, headers, body, content_type, content_receiver,\n                   progress);\n}\n\ninline Result Client::Patch(const std::string &path) {\n  return cli_->Patch(path);\n}\ninline Result Client::Patch(const std::string &path, const Headers &headers) {\n  return cli_->Patch(path, headers);\n}\ninline Result Client::Patch(const std::string &path, const char *body,\n                            size_t content_length,\n                            const std::string &content_type,\n                            UploadProgress progress) {\n  return cli_->Patch(path, body, content_length, content_type, progress);\n}\ninline Result Client::Patch(const std::string &path, const Headers &headers,\n                            const char *body, size_t content_length,\n                            const std::string &content_type,\n                            UploadProgress progress) {\n  return cli_->Patch(path, headers, body, content_length, content_type,\n                     progress);\n}\ninline Result Client::Patch(const std::string &path, const std::string &body,\n                            const std::string &content_type,\n                            UploadProgress progress) {\n  return cli_->Patch(path, body, content_type, progress);\n}\ninline Result Client::Patch(const std::string &path, const Headers &headers,\n                            const std::string &body,\n                            const std::string &content_type,\n                            UploadProgress progress) {\n  return cli_->Patch(path, headers, body, content_type, progress);\n}\ninline Result Client::Patch(const std::string &path, size_t content_length,\n                            ContentProvider content_provider,\n                            const std::string &content_type,\n                            UploadProgress progress) {\n  return cli_->Patch(path, content_length, std::move(content_provider),\n                     content_type, progress);\n}\ninline Result Client::Patch(const std::string &path, size_t content_length,\n                            ContentProvider content_provider,\n                            const std::string &content_type,\n                            ContentReceiver content_receiver,\n                            UploadProgress progress) {\n  return cli_->Patch(path, content_length, std::move(content_provider),\n                     content_type, std::move(content_receiver), progress);\n}\ninline Result Client::Patch(const std::string &path,\n                            ContentProviderWithoutLength content_provider,\n                            const std::string &content_type,\n                            UploadProgress progress) {\n  return cli_->Patch(path, std::move(content_provider), content_type, progress);\n}\ninline Result Client::Patch(const std::string &path,\n                            ContentProviderWithoutLength content_provider,\n                            const std::string &content_type,\n                            ContentReceiver content_receiver,\n                            UploadProgress progress) {\n  return cli_->Patch(path, std::move(content_provider), content_type,\n                     std::move(content_receiver), progress);\n}\ninline Result Client::Patch(const std::string &path, const Headers &headers,\n                            size_t content_length,\n                            ContentProvider content_provider,\n                            const std::string &content_type,\n                            UploadProgress progress) {\n  return cli_->Patch(path, headers, content_length, std::move(content_provider),\n                     content_type, progress);\n}\ninline Result Client::Patch(const std::string &path, const Headers &headers,\n                            size_t content_length,\n                            ContentProvider content_provider,\n                            const std::string &content_type,\n                            ContentReceiver content_receiver,\n                            UploadProgress progress) {\n  return cli_->Patch(path, headers, content_length, std::move(content_provider),\n                     content_type, std::move(content_receiver), progress);\n}\ninline Result Client::Patch(const std::string &path, const Headers &headers,\n                            ContentProviderWithoutLength content_provider,\n                            const std::string &content_type,\n                            UploadProgress progress) {\n  return cli_->Patch(path, headers, std::move(content_provider), content_type,\n                     progress);\n}\ninline Result Client::Patch(const std::string &path, const Headers &headers,\n                            ContentProviderWithoutLength content_provider,\n                            const std::string &content_type,\n                            ContentReceiver content_receiver,\n                            UploadProgress progress) {\n  return cli_->Patch(path, headers, std::move(content_provider), content_type,\n                     std::move(content_receiver), progress);\n}\ninline Result Client::Patch(const std::string &path, const Params &params) {\n  return cli_->Patch(path, params);\n}\ninline Result Client::Patch(const std::string &path, const Headers &headers,\n                            const Params &params) {\n  return cli_->Patch(path, headers, params);\n}\ninline Result Client::Patch(const std::string &path,\n                            const UploadFormDataItems &items,\n                            UploadProgress progress) {\n  return cli_->Patch(path, items, progress);\n}\ninline Result Client::Patch(const std::string &path, const Headers &headers,\n                            const UploadFormDataItems &items,\n                            UploadProgress progress) {\n  return cli_->Patch(path, headers, items, progress);\n}\ninline Result Client::Patch(const std::string &path, const Headers &headers,\n                            const UploadFormDataItems &items,\n                            const std::string &boundary,\n                            UploadProgress progress) {\n  return cli_->Patch(path, headers, items, boundary, progress);\n}\ninline Result Client::Patch(const std::string &path, const Headers &headers,\n                            const UploadFormDataItems &items,\n                            const FormDataProviderItems &provider_items,\n                            UploadProgress progress) {\n  return cli_->Patch(path, headers, items, provider_items, progress);\n}\ninline Result Client::Patch(const std::string &path, const Headers &headers,\n                            const std::string &body,\n                            const std::string &content_type,\n                            ContentReceiver content_receiver,\n                            DownloadProgress progress) {\n  return cli_->Patch(path, headers, body, content_type, content_receiver,\n                     progress);\n}\n\ninline Result Client::Delete(const std::string &path,\n                             DownloadProgress progress) {\n  return cli_->Delete(path, progress);\n}\ninline Result Client::Delete(const std::string &path, const Headers &headers,\n                             DownloadProgress progress) {\n  return cli_->Delete(path, headers, progress);\n}\ninline Result Client::Delete(const std::string &path, const char *body,\n                             size_t content_length,\n                             const std::string &content_type,\n                             DownloadProgress progress) {\n  return cli_->Delete(path, body, content_length, content_type, progress);\n}\ninline Result Client::Delete(const std::string &path, const Headers &headers,\n                             const char *body, size_t content_length,\n                             const std::string &content_type,\n                             DownloadProgress progress) {\n  return cli_->Delete(path, headers, body, content_length, content_type,\n                      progress);\n}\ninline Result Client::Delete(const std::string &path, const std::string &body,\n                             const std::string &content_type,\n                             DownloadProgress progress) {\n  return cli_->Delete(path, body, content_type, progress);\n}\ninline Result Client::Delete(const std::string &path, const Headers &headers,\n                             const std::string &body,\n                             const std::string &content_type,\n                             DownloadProgress progress) {\n  return cli_->Delete(path, headers, body, content_type, progress);\n}\ninline Result Client::Delete(const std::string &path, const Params &params,\n                             DownloadProgress progress) {\n  return cli_->Delete(path, params, progress);\n}\ninline Result Client::Delete(const std::string &path, const Headers &headers,\n                             const Params &params, DownloadProgress progress) {\n  return cli_->Delete(path, headers, params, progress);\n}\n\ninline Result Client::Options(const std::string &path) {\n  return cli_->Options(path);\n}\ninline Result Client::Options(const std::string &path, const Headers &headers) {\n  return cli_->Options(path, headers);\n}\n\ninline ClientImpl::StreamHandle\nClient::open_stream(const std::string &method, const std::string &path,\n                    const Params &params, const Headers &headers,\n                    const std::string &body, const std::string &content_type) {\n  return cli_->open_stream(method, path, params, headers, body, content_type);\n}\n\ninline bool Client::send(Request &req, Response &res, Error &error) {\n  return cli_->send(req, res, error);\n}\n\ninline Result Client::send(const Request &req) { return cli_->send(req); }\n\ninline void Client::stop() { cli_->stop(); }\n\ninline std::string Client::host() const { return cli_->host(); }\n\ninline int Client::port() const { return cli_->port(); }\n\ninline size_t Client::is_socket_open() const { return cli_->is_socket_open(); }\n\ninline socket_t Client::socket() const { return cli_->socket(); }\n\ninline void\nClient::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {\n  cli_->set_hostname_addr_map(std::move(addr_map));\n}\n\ninline void Client::set_default_headers(Headers headers) {\n  cli_->set_default_headers(std::move(headers));\n}\n\ninline void Client::set_header_writer(\n    std::function<ssize_t(Stream &, Headers &)> const &writer) {\n  cli_->set_header_writer(writer);\n}\n\ninline void Client::set_address_family(int family) {\n  cli_->set_address_family(family);\n}\n\ninline void Client::set_tcp_nodelay(bool on) { cli_->set_tcp_nodelay(on); }\n\ninline void Client::set_socket_options(SocketOptions socket_options) {\n  cli_->set_socket_options(std::move(socket_options));\n}\n\ninline void Client::set_connection_timeout(time_t sec, time_t usec) {\n  cli_->set_connection_timeout(sec, usec);\n}\n\ninline void Client::set_read_timeout(time_t sec, time_t usec) {\n  cli_->set_read_timeout(sec, usec);\n}\n\ninline void Client::set_write_timeout(time_t sec, time_t usec) {\n  cli_->set_write_timeout(sec, usec);\n}\n\ninline void Client::set_basic_auth(const std::string &username,\n                                   const std::string &password) {\n  cli_->set_basic_auth(username, password);\n}\ninline void Client::set_bearer_token_auth(const std::string &token) {\n  cli_->set_bearer_token_auth(token);\n}\n\ninline void Client::set_keep_alive(bool on) { cli_->set_keep_alive(on); }\ninline void Client::set_follow_location(bool on) {\n  cli_->set_follow_location(on);\n}\n\ninline void Client::set_path_encode(bool on) { cli_->set_path_encode(on); }\n\n[[deprecated(\"Use set_path_encode instead\")]]\ninline void Client::set_url_encode(bool on) {\n  cli_->set_path_encode(on);\n}\n\ninline void Client::set_compress(bool on) { cli_->set_compress(on); }\n\ninline void Client::set_decompress(bool on) { cli_->set_decompress(on); }\n\ninline void Client::set_payload_max_length(size_t length) {\n  cli_->set_payload_max_length(length);\n}\n\ninline void Client::set_interface(const std::string &intf) {\n  cli_->set_interface(intf);\n}\n\ninline void Client::set_proxy(const std::string &host, int port) {\n  cli_->set_proxy(host, port);\n}\ninline void Client::set_proxy_basic_auth(const std::string &username,\n                                         const std::string &password) {\n  cli_->set_proxy_basic_auth(username, password);\n}\ninline void Client::set_proxy_bearer_token_auth(const std::string &token) {\n  cli_->set_proxy_bearer_token_auth(token);\n}\n\ninline void Client::set_logger(Logger logger) {\n  cli_->set_logger(std::move(logger));\n}\n\ninline void Client::set_error_logger(ErrorLogger error_logger) {\n  cli_->set_error_logger(std::move(error_logger));\n}\n\n/*\n * Group 6: SSL Server and Client implementation\n */\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n\n// SSL HTTP server implementation\ninline SSLServer::SSLServer(const char *cert_path, const char *private_key_path,\n                            const char *client_ca_cert_file_path,\n                            const char *client_ca_cert_dir_path,\n                            const char *private_key_password) {\n  using namespace tls;\n\n  ctx_ = create_server_context();\n  if (!ctx_) { return; }\n\n  // Load server certificate and private key\n  if (!set_server_cert_file(ctx_, cert_path, private_key_path,\n                            private_key_password)) {\n    last_ssl_error_ = static_cast<int>(get_error());\n    free_context(ctx_);\n    ctx_ = nullptr;\n    return;\n  }\n\n  // Load client CA certificates for client authentication\n  if (client_ca_cert_file_path || client_ca_cert_dir_path) {\n    if (!set_client_ca_file(ctx_, client_ca_cert_file_path,\n                            client_ca_cert_dir_path)) {\n      last_ssl_error_ = static_cast<int>(get_error());\n      free_context(ctx_);\n      ctx_ = nullptr;\n      return;\n    }\n    // Enable client certificate verification\n    set_verify_client(ctx_, true);\n  }\n}\n\ninline SSLServer::SSLServer(const PemMemory &pem) {\n  using namespace tls;\n  ctx_ = create_server_context();\n  if (ctx_) {\n    if (!set_server_cert_pem(ctx_, pem.cert_pem, pem.key_pem,\n                             pem.private_key_password)) {\n      last_ssl_error_ = static_cast<int>(get_error());\n      free_context(ctx_);\n      ctx_ = nullptr;\n    } else if (pem.client_ca_pem && pem.client_ca_pem_len > 0) {\n      if (!load_ca_pem(ctx_, pem.client_ca_pem, pem.client_ca_pem_len)) {\n        last_ssl_error_ = static_cast<int>(get_error());\n        free_context(ctx_);\n        ctx_ = nullptr;\n      } else {\n        set_verify_client(ctx_, true);\n      }\n    }\n  }\n}\n\ninline SSLServer::SSLServer(const tls::ContextSetupCallback &setup_callback) {\n  using namespace tls;\n  ctx_ = create_server_context();\n  if (ctx_) {\n    if (!setup_callback(ctx_)) {\n      free_context(ctx_);\n      ctx_ = nullptr;\n    }\n  }\n}\n\ninline SSLServer::~SSLServer() {\n  if (ctx_) { tls::free_context(ctx_); }\n}\n\ninline bool SSLServer::is_valid() const { return ctx_ != nullptr; }\n\ninline bool SSLServer::process_and_close_socket(socket_t sock) {\n  using namespace tls;\n\n  // Create TLS session with mutex protection\n  session_t session = nullptr;\n  {\n    std::lock_guard<std::mutex> guard(ctx_mutex_);\n    session = create_session(static_cast<ctx_t>(ctx_), sock);\n  }\n\n  if (!session) {\n    last_ssl_error_ = static_cast<int>(get_error());\n    detail::shutdown_socket(sock);\n    detail::close_socket(sock);\n    return false;\n  }\n\n  // Use scope_exit to ensure cleanup on all paths (including exceptions)\n  bool handshake_done = false;\n  bool ret = false;\n  auto cleanup = detail::scope_exit([&] {\n    // Shutdown gracefully if handshake succeeded and processing was successful\n    if (handshake_done) { shutdown(session, ret); }\n    free_session(session);\n    detail::shutdown_socket(sock);\n    detail::close_socket(sock);\n  });\n\n  // Perform TLS accept handshake with timeout\n  TlsError tls_err;\n  if (!accept_nonblocking(session, sock, read_timeout_sec_, read_timeout_usec_,\n                          &tls_err)) {\n#ifdef CPPHTTPLIB_OPENSSL_SUPPORT\n    // Map TlsError to legacy ssl_error for backward compatibility\n    if (tls_err.code == ErrorCode::WantRead) {\n      last_ssl_error_ = SSL_ERROR_WANT_READ;\n    } else if (tls_err.code == ErrorCode::WantWrite) {\n      last_ssl_error_ = SSL_ERROR_WANT_WRITE;\n    } else {\n      last_ssl_error_ = SSL_ERROR_SSL;\n    }\n#else\n    last_ssl_error_ = static_cast<int>(get_error());\n#endif\n    return false;\n  }\n\n  handshake_done = true;\n\n  std::string remote_addr;\n  int remote_port = 0;\n  detail::get_remote_ip_and_port(sock, remote_addr, remote_port);\n\n  std::string local_addr;\n  int local_port = 0;\n  detail::get_local_ip_and_port(sock, local_addr, local_port);\n\n  ret = detail::process_server_socket_ssl(\n      svr_sock_, session, sock, keep_alive_max_count_, keep_alive_timeout_sec_,\n      read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,\n      write_timeout_usec_,\n      [&](Stream &strm, bool close_connection, bool &connection_closed) {\n        return process_request(strm, remote_addr, remote_port, local_addr,\n                               local_port, close_connection, connection_closed,\n                               [&](Request &req) { req.ssl = session; });\n      });\n\n  return ret;\n}\n\ninline bool SSLServer::update_certs_pem(const char *cert_pem,\n                                        const char *key_pem,\n                                        const char *client_ca_pem,\n                                        const char *password) {\n  if (!ctx_) { return false; }\n  std::lock_guard<std::mutex> guard(ctx_mutex_);\n  if (!tls::update_server_cert(ctx_, cert_pem, key_pem, password)) {\n    return false;\n  }\n  if (client_ca_pem) {\n    return tls::update_server_client_ca(ctx_, client_ca_pem);\n  }\n  return true;\n}\n\n// SSL HTTP client implementation\ninline SSLClient::~SSLClient() {\n  if (ctx_) { tls::free_context(ctx_); }\n  // Make sure to shut down SSL since shutdown_ssl will resolve to the\n  // base function rather than the derived function once we get to the\n  // base class destructor, and won't free the SSL (causing a leak).\n  shutdown_ssl_impl(socket_, true);\n}\n\ninline bool SSLClient::is_valid() const { return ctx_ != nullptr; }\n\ninline void SSLClient::shutdown_ssl(Socket &socket, bool shutdown_gracefully) {\n  shutdown_ssl_impl(socket, shutdown_gracefully);\n}\n\ninline void SSLClient::shutdown_ssl_impl(Socket &socket,\n                                         bool shutdown_gracefully) {\n  if (socket.sock == INVALID_SOCKET) {\n    assert(socket.ssl == nullptr);\n    return;\n  }\n  if (socket.ssl) {\n    tls::shutdown(socket.ssl, shutdown_gracefully);\n    {\n      std::lock_guard<std::mutex> guard(ctx_mutex_);\n      tls::free_session(socket.ssl);\n    }\n    socket.ssl = nullptr;\n  }\n  assert(socket.ssl == nullptr);\n}\n\ninline bool SSLClient::process_socket(\n    const Socket &socket,\n    std::chrono::time_point<std::chrono::steady_clock> start_time,\n    std::function<bool(Stream &strm)> callback) {\n  assert(socket.ssl);\n  return detail::process_client_socket_ssl(\n      socket.ssl, socket.sock, read_timeout_sec_, read_timeout_usec_,\n      write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time,\n      std::move(callback));\n}\n\ninline bool SSLClient::is_ssl() const { return true; }\n\ninline bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) {\n  if (!is_valid()) {\n    error = Error::SSLConnection;\n    return false;\n  }\n  return ClientImpl::create_and_connect_socket(socket, error);\n}\n\n// Assumes that socket_mutex_ is locked and that there are no requests in\n// flight\ninline bool SSLClient::connect_with_proxy(\n    Socket &socket,\n    std::chrono::time_point<std::chrono::steady_clock> start_time,\n    Response &res, bool &success, Error &error) {\n  success = true;\n  Response proxy_res;\n  if (!detail::process_client_socket(\n          socket.sock, read_timeout_sec_, read_timeout_usec_,\n          write_timeout_sec_, write_timeout_usec_, max_timeout_msec_,\n          start_time, [&](Stream &strm) {\n            Request req2;\n            req2.method = \"CONNECT\";\n            req2.path =\n                detail::make_host_and_port_string_always_port(host_, port_);\n            if (max_timeout_msec_ > 0) {\n              req2.start_time_ = std::chrono::steady_clock::now();\n            }\n            return process_request(strm, req2, proxy_res, false, error);\n          })) {\n    // Thread-safe to close everything because we are assuming there are no\n    // requests in flight\n    shutdown_ssl(socket, true);\n    shutdown_socket(socket);\n    close_socket(socket);\n    success = false;\n    return false;\n  }\n\n  if (proxy_res.status == StatusCode::ProxyAuthenticationRequired_407) {\n    if (!proxy_digest_auth_username_.empty() &&\n        !proxy_digest_auth_password_.empty()) {\n      std::map<std::string, std::string> auth;\n      if (detail::parse_www_authenticate(proxy_res, auth, true)) {\n        // Close the current socket and create a new one for the authenticated\n        // request\n        shutdown_ssl(socket, true);\n        shutdown_socket(socket);\n        close_socket(socket);\n\n        // Create a new socket for the authenticated CONNECT request\n        if (!ensure_socket_connection(socket, error)) {\n          success = false;\n          output_error_log(error, nullptr);\n          return false;\n        }\n\n        proxy_res = Response();\n        if (!detail::process_client_socket(\n                socket.sock, read_timeout_sec_, read_timeout_usec_,\n                write_timeout_sec_, write_timeout_usec_, max_timeout_msec_,\n                start_time, [&](Stream &strm) {\n                  Request req3;\n                  req3.method = \"CONNECT\";\n                  req3.path = detail::make_host_and_port_string_always_port(\n                      host_, port_);\n                  req3.headers.insert(detail::make_digest_authentication_header(\n                      req3, auth, 1, detail::random_string(10),\n                      proxy_digest_auth_username_, proxy_digest_auth_password_,\n                      true));\n                  if (max_timeout_msec_ > 0) {\n                    req3.start_time_ = std::chrono::steady_clock::now();\n                  }\n                  return process_request(strm, req3, proxy_res, false, error);\n                })) {\n          // Thread-safe to close everything because we are assuming there are\n          // no requests in flight\n          shutdown_ssl(socket, true);\n          shutdown_socket(socket);\n          close_socket(socket);\n          success = false;\n          return false;\n        }\n      }\n    }\n  }\n\n  // If status code is not 200, proxy request is failed.\n  // Set error to ProxyConnection and return proxy response\n  // as the response of the request\n  if (proxy_res.status != StatusCode::OK_200) {\n    error = Error::ProxyConnection;\n    output_error_log(error, nullptr);\n    res = std::move(proxy_res);\n    // Thread-safe to close everything because we are assuming there are\n    // no requests in flight\n    shutdown_ssl(socket, true);\n    shutdown_socket(socket);\n    close_socket(socket);\n    return false;\n  }\n\n  return true;\n}\n\ninline bool SSLClient::ensure_socket_connection(Socket &socket, Error &error) {\n  if (!ClientImpl::ensure_socket_connection(socket, error)) { return false; }\n\n  if (!proxy_host_.empty() && proxy_port_ != -1) { return true; }\n\n  if (!initialize_ssl(socket, error)) {\n    shutdown_socket(socket);\n    close_socket(socket);\n    return false;\n  }\n\n  return true;\n}\n\n// SSL HTTP client implementation\ninline SSLClient::SSLClient(const std::string &host)\n    : SSLClient(host, 443, std::string(), std::string()) {}\n\ninline SSLClient::SSLClient(const std::string &host, int port)\n    : SSLClient(host, port, std::string(), std::string()) {}\n\ninline SSLClient::SSLClient(const std::string &host, int port,\n                            const std::string &client_cert_path,\n                            const std::string &client_key_path,\n                            const std::string &private_key_password)\n    : ClientImpl(host, port, client_cert_path, client_key_path) {\n  ctx_ = tls::create_client_context();\n  if (!ctx_) { return; }\n\n  tls::set_min_version(ctx_, tls::Version::TLS1_2);\n\n  if (!client_cert_path.empty() && !client_key_path.empty()) {\n    const char *password =\n        private_key_password.empty() ? nullptr : private_key_password.c_str();\n    if (!tls::set_client_cert_file(ctx_, client_cert_path.c_str(),\n                                   client_key_path.c_str(), password)) {\n      last_backend_error_ = tls::get_error();\n      tls::free_context(ctx_);\n      ctx_ = nullptr;\n    }\n  }\n}\n\ninline SSLClient::SSLClient(const std::string &host, int port,\n                            const PemMemory &pem)\n    : ClientImpl(host, port) {\n  ctx_ = tls::create_client_context();\n  if (!ctx_) { return; }\n\n  tls::set_min_version(ctx_, tls::Version::TLS1_2);\n\n  if (pem.cert_pem && pem.key_pem) {\n    if (!tls::set_client_cert_pem(ctx_, pem.cert_pem, pem.key_pem,\n                                  pem.private_key_password)) {\n      last_backend_error_ = tls::get_error();\n      tls::free_context(ctx_);\n      ctx_ = nullptr;\n    }\n  }\n}\n\ninline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) {\n  if (ca_cert_store && ctx_) {\n    // set_ca_store takes ownership of ca_cert_store\n    tls::set_ca_store(ctx_, ca_cert_store);\n  } else if (ca_cert_store) {\n    tls::free_ca_store(ca_cert_store);\n  }\n}\n\ninline void\nSSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) {\n  if (!ctx_) { return; }\n  tls::set_verify_callback(ctx_, verifier);\n}\n\ninline void SSLClient::set_session_verifier(\n    std::function<SSLVerifierResponse(tls::session_t)> verifier) {\n  session_verifier_ = std::move(verifier);\n}\n\n#if defined(_WIN32) &&                                                         \\\n    !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)\ninline void SSLClient::enable_windows_certificate_verification(bool enabled) {\n  enable_windows_cert_verification_ = enabled;\n}\n#endif\n\ninline void SSLClient::load_ca_cert_store(const char *ca_cert,\n                                          std::size_t size) {\n  if (ctx_ && ca_cert && size > 0) {\n    ca_cert_pem_.assign(ca_cert, size); // Store for redirect transfer\n    tls::load_ca_pem(ctx_, ca_cert, size);\n  }\n}\n\ninline bool SSLClient::load_certs() {\n  auto ret = true;\n\n  std::call_once(initialize_cert_, [&]() {\n    std::lock_guard<std::mutex> guard(ctx_mutex_);\n\n    if (!ca_cert_file_path_.empty()) {\n      if (!tls::load_ca_file(ctx_, ca_cert_file_path_.c_str())) {\n        last_backend_error_ = tls::get_error();\n        ret = false;\n      }\n    } else if (!ca_cert_dir_path_.empty()) {\n      if (!tls::load_ca_dir(ctx_, ca_cert_dir_path_.c_str())) {\n        last_backend_error_ = tls::get_error();\n        ret = false;\n      }\n    } else if (ca_cert_pem_.empty()) {\n      if (!tls::load_system_certs(ctx_)) {\n        last_backend_error_ = tls::get_error();\n      }\n    }\n  });\n\n  return ret;\n}\n\ninline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {\n  using namespace tls;\n\n  // Load CA certificates if server verification is enabled\n  if (server_certificate_verification_) {\n    if (!load_certs()) {\n      error = Error::SSLLoadingCerts;\n      output_error_log(error, nullptr);\n      return false;\n    }\n  }\n\n  bool is_ip = detail::is_ip_address(host_);\n\n#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT\n  // MbedTLS needs explicit verification mode (OpenSSL uses SSL_VERIFY_NONE\n  // by default and performs all verification post-handshake).\n  // For IP addresses with verification enabled, use OPTIONAL mode since\n  // MbedTLS requires hostname for VERIFY_REQUIRED.\n  if (is_ip && server_certificate_verification_) {\n    set_verify_client(ctx_, false);\n  } else {\n    set_verify_client(ctx_, server_certificate_verification_);\n  }\n#endif\n\n  // Create TLS session\n  session_t session = nullptr;\n  {\n    std::lock_guard<std::mutex> guard(ctx_mutex_);\n    session = create_session(ctx_, socket.sock);\n  }\n\n  if (!session) {\n    error = Error::SSLConnection;\n    last_backend_error_ = get_error();\n    return false;\n  }\n\n  // Use scope_exit to ensure session is freed on error paths\n  bool success = false;\n  auto session_guard = detail::scope_exit([&] {\n    if (!success) { free_session(session); }\n  });\n\n  // Set SNI extension (skip for IP addresses per RFC 6066).\n  // On MbedTLS, set_sni also enables hostname verification internally.\n  // On OpenSSL, set_sni only sets SNI; verification is done post-handshake.\n  if (!is_ip) {\n    if (!set_sni(session, host_.c_str())) {\n      error = Error::SSLConnection;\n      last_backend_error_ = get_error();\n      return false;\n    }\n  }\n\n  // Perform non-blocking TLS handshake with timeout\n  TlsError tls_err;\n  if (!connect_nonblocking(session, socket.sock, connection_timeout_sec_,\n                           connection_timeout_usec_, &tls_err)) {\n    last_ssl_error_ = static_cast<int>(tls_err.code);\n    last_backend_error_ = tls_err.backend_code;\n    if (tls_err.code == ErrorCode::CertVerifyFailed) {\n      error = Error::SSLServerVerification;\n    } else if (tls_err.code == ErrorCode::HostnameMismatch) {\n      error = Error::SSLServerHostnameVerification;\n    } else {\n      error = Error::SSLConnection;\n    }\n    output_error_log(error, nullptr);\n    return false;\n  }\n\n  // Post-handshake session verifier callback\n  auto verification_status = SSLVerifierResponse::NoDecisionMade;\n  if (session_verifier_) { verification_status = session_verifier_(session); }\n\n  if (verification_status == SSLVerifierResponse::CertificateRejected) {\n    last_backend_error_ = get_error();\n    error = Error::SSLServerVerification;\n    output_error_log(error, nullptr);\n    return false;\n  }\n\n  // Default server certificate verification\n  if (verification_status == SSLVerifierResponse::NoDecisionMade &&\n      server_certificate_verification_) {\n    verify_result_ = tls::get_verify_result(session);\n    if (verify_result_ != 0) {\n      last_backend_error_ = static_cast<unsigned long>(verify_result_);\n      error = Error::SSLServerVerification;\n      output_error_log(error, nullptr);\n      return false;\n    }\n\n    auto server_cert = get_peer_cert(session);\n    if (!server_cert) {\n      last_backend_error_ = get_error();\n      error = Error::SSLServerVerification;\n      output_error_log(error, nullptr);\n      return false;\n    }\n    auto cert_guard = detail::scope_exit([&] { free_cert(server_cert); });\n\n    // Hostname verification (post-handshake for all cases).\n    // On OpenSSL, verification is always post-handshake (SSL_VERIFY_NONE).\n    // On MbedTLS, set_sni already enabled hostname verification during\n    // handshake for non-IP hosts, but this check is still needed for IP\n    // addresses where SNI is not set.\n    if (server_hostname_verification_) {\n      if (!verify_hostname(server_cert, host_.c_str())) {\n        last_backend_error_ = hostname_mismatch_code();\n        error = Error::SSLServerHostnameVerification;\n        output_error_log(error, nullptr);\n        return false;\n      }\n    }\n\n#if defined(_WIN32) &&                                                         \\\n    !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)\n    // Additional Windows Schannel verification.\n    // This provides real-time certificate validation with Windows Update\n    // integration, working with both OpenSSL and MbedTLS backends.\n    // Skip when a custom CA cert is specified, as the Windows certificate\n    // store would not know about user-provided CA certificates.\n    if (enable_windows_cert_verification_ && ca_cert_file_path_.empty() &&\n        ca_cert_dir_path_.empty() && ca_cert_pem_.empty()) {\n      std::vector<unsigned char> der;\n      if (get_cert_der(server_cert, der)) {\n        unsigned long wincrypt_error = 0;\n        if (!detail::verify_cert_with_windows_schannel(\n                der, host_, server_hostname_verification_, wincrypt_error)) {\n          last_backend_error_ = wincrypt_error;\n          error = Error::SSLServerVerification;\n          output_error_log(error, nullptr);\n          return false;\n        }\n      }\n    }\n#endif\n  }\n\n  success = true;\n  socket.ssl = session;\n  return true;\n}\n\ninline void Client::set_digest_auth(const std::string &username,\n                                    const std::string &password) {\n  cli_->set_digest_auth(username, password);\n}\n\ninline void Client::set_proxy_digest_auth(const std::string &username,\n                                          const std::string &password) {\n  cli_->set_proxy_digest_auth(username, password);\n}\n\ninline void Client::enable_server_certificate_verification(bool enabled) {\n  cli_->enable_server_certificate_verification(enabled);\n}\n\ninline void Client::enable_server_hostname_verification(bool enabled) {\n  cli_->enable_server_hostname_verification(enabled);\n}\n\n#if defined(_WIN32) &&                                                         \\\n    !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)\ninline void Client::enable_windows_certificate_verification(bool enabled) {\n  if (is_ssl_) {\n    static_cast<SSLClient &>(*cli_).enable_windows_certificate_verification(\n        enabled);\n  }\n}\n#endif\n\ninline void Client::set_ca_cert_path(const std::string &ca_cert_file_path,\n                                     const std::string &ca_cert_dir_path) {\n  cli_->set_ca_cert_path(ca_cert_file_path, ca_cert_dir_path);\n}\n\ninline void Client::set_ca_cert_store(tls::ca_store_t ca_cert_store) {\n  if (is_ssl_) {\n    static_cast<SSLClient &>(*cli_).set_ca_cert_store(ca_cert_store);\n  } else if (ca_cert_store) {\n    tls::free_ca_store(ca_cert_store);\n  }\n}\n\ninline void Client::load_ca_cert_store(const char *ca_cert, std::size_t size) {\n  set_ca_cert_store(tls::create_ca_store(ca_cert, size));\n}\n\ninline void\nClient::set_server_certificate_verifier(tls::VerifyCallback verifier) {\n  if (is_ssl_) {\n    static_cast<SSLClient &>(*cli_).set_server_certificate_verifier(\n        std::move(verifier));\n  }\n}\n\ninline void Client::set_session_verifier(\n    std::function<SSLVerifierResponse(tls::session_t)> verifier) {\n  if (is_ssl_) {\n    static_cast<SSLClient &>(*cli_).set_session_verifier(std::move(verifier));\n  }\n}\n\ninline tls::ctx_t Client::tls_context() const {\n  if (is_ssl_) { return static_cast<SSLClient &>(*cli_).tls_context(); }\n  return nullptr;\n}\n\n#endif // CPPHTTPLIB_SSL_ENABLED\n\n/*\n * Group 7: TLS abstraction layer - Common API\n */\n\n#ifdef CPPHTTPLIB_SSL_ENABLED\n\nnamespace tls {\n\n// Helper for PeerCert construction\ninline PeerCert get_peer_cert_from_session(const_session_t session) {\n  return PeerCert(get_peer_cert(session));\n}\n\nnamespace impl {\n\ninline VerifyCallback &get_verify_callback() {\n  static thread_local VerifyCallback callback;\n  return callback;\n}\n\ninline VerifyCallback &get_mbedtls_verify_callback() {\n  static thread_local VerifyCallback callback;\n  return callback;\n}\n\n} // namespace impl\n\ninline bool set_client_ca_file(ctx_t ctx, const char *ca_file,\n                               const char *ca_dir) {\n  if (!ctx) { return false; }\n\n  bool success = true;\n  if (ca_file && *ca_file) {\n    if (!load_ca_file(ctx, ca_file)) { success = false; }\n  }\n  if (ca_dir && *ca_dir) {\n    if (!load_ca_dir(ctx, ca_dir)) { success = false; }\n  }\n\n#ifdef CPPHTTPLIB_OPENSSL_SUPPORT\n  // Set CA list for client certificate request (CertificateRequest message)\n  if (ca_file && *ca_file) {\n    auto list = SSL_load_client_CA_file(ca_file);\n    if (list) { SSL_CTX_set_client_CA_list(static_cast<SSL_CTX *>(ctx), list); }\n  }\n#endif\n\n  return success;\n}\n\ninline bool set_server_cert_pem(ctx_t ctx, const char *cert, const char *key,\n                                const char *password) {\n  return set_client_cert_pem(ctx, cert, key, password);\n}\n\ninline bool set_server_cert_file(ctx_t ctx, const char *cert_path,\n                                 const char *key_path, const char *password) {\n  return set_client_cert_file(ctx, cert_path, key_path, password);\n}\n\n// PeerCert implementation\ninline PeerCert::PeerCert() = default;\n\ninline PeerCert::PeerCert(cert_t cert) : cert_(cert) {}\n\ninline PeerCert::PeerCert(PeerCert &&other) noexcept : cert_(other.cert_) {\n  other.cert_ = nullptr;\n}\n\ninline PeerCert &PeerCert::operator=(PeerCert &&other) noexcept {\n  if (this != &other) {\n    if (cert_) { free_cert(cert_); }\n    cert_ = other.cert_;\n    other.cert_ = nullptr;\n  }\n  return *this;\n}\n\ninline PeerCert::~PeerCert() {\n  if (cert_) { free_cert(cert_); }\n}\n\ninline PeerCert::operator bool() const { return cert_ != nullptr; }\n\ninline std::string PeerCert::subject_cn() const {\n  return cert_ ? get_cert_subject_cn(cert_) : std::string();\n}\n\ninline std::string PeerCert::issuer_name() const {\n  return cert_ ? get_cert_issuer_name(cert_) : std::string();\n}\n\ninline bool PeerCert::check_hostname(const char *hostname) const {\n  return cert_ ? verify_hostname(cert_, hostname) : false;\n}\n\ninline std::vector<SanEntry> PeerCert::sans() const {\n  std::vector<SanEntry> result;\n  if (cert_) { get_cert_sans(cert_, result); }\n  return result;\n}\n\ninline bool PeerCert::validity(time_t &not_before, time_t &not_after) const {\n  return cert_ ? get_cert_validity(cert_, not_before, not_after) : false;\n}\n\ninline std::string PeerCert::serial() const {\n  return cert_ ? get_cert_serial(cert_) : std::string();\n}\n\n// VerifyContext method implementations\ninline std::string VerifyContext::subject_cn() const {\n  return cert ? get_cert_subject_cn(cert) : std::string();\n}\n\ninline std::string VerifyContext::issuer_name() const {\n  return cert ? get_cert_issuer_name(cert) : std::string();\n}\n\ninline bool VerifyContext::check_hostname(const char *hostname) const {\n  return cert ? verify_hostname(cert, hostname) : false;\n}\n\ninline std::vector<SanEntry> VerifyContext::sans() const {\n  std::vector<SanEntry> result;\n  if (cert) { get_cert_sans(cert, result); }\n  return result;\n}\n\ninline bool VerifyContext::validity(time_t &not_before,\n                                    time_t &not_after) const {\n  return cert ? get_cert_validity(cert, not_before, not_after) : false;\n}\n\ninline std::string VerifyContext::serial() const {\n  return cert ? get_cert_serial(cert) : std::string();\n}\n\n// TlsError static method implementation\ninline std::string TlsError::verify_error_to_string(long error_code) {\n  return verify_error_string(error_code);\n}\n\n} // namespace tls\n\n// Request::peer_cert() implementation\ninline tls::PeerCert Request::peer_cert() const {\n  return tls::get_peer_cert_from_session(ssl);\n}\n\n// Request::sni() implementation\ninline std::string Request::sni() const {\n  if (!ssl) { return std::string(); }\n  const char *s = tls::get_sni(ssl);\n  return s ? std::string(s) : std::string();\n}\n\n#endif // CPPHTTPLIB_SSL_ENABLED\n\n/*\n * Group 8: TLS abstraction layer - OpenSSL backend\n */\n\n#ifdef CPPHTTPLIB_OPENSSL_SUPPORT\ninline SSL_CTX *Client::ssl_context() const {\n  if (is_ssl_) { return static_cast<SSLClient &>(*cli_).ssl_context(); }\n  return nullptr;\n}\n\ninline void Client::set_server_certificate_verifier(\n    std::function<SSLVerifierResponse(SSL *ssl)> verifier) {\n  cli_->set_server_certificate_verifier(verifier);\n}\n\ninline long Client::get_verify_result() const {\n  if (is_ssl_) { return static_cast<SSLClient &>(*cli_).get_verify_result(); }\n  return -1; // NOTE: -1 doesn't match any of X509_V_ERR_???\n}\n#endif // CPPHTTPLIB_OPENSSL_SUPPORT\n\n/*\n * OpenSSL Backend Implementation\n */\n\n#ifdef CPPHTTPLIB_OPENSSL_SUPPORT\nnamespace tls {\n\nnamespace impl {\n\n// OpenSSL-specific helpers for converting native types to PEM\ninline std::string x509_to_pem(X509 *cert) {\n  if (!cert) return {};\n  BIO *bio = BIO_new(BIO_s_mem());\n  if (!bio) return {};\n  if (PEM_write_bio_X509(bio, cert) != 1) {\n    BIO_free(bio);\n    return {};\n  }\n  char *data = nullptr;\n  long len = BIO_get_mem_data(bio, &data);\n  std::string pem(data, static_cast<size_t>(len));\n  BIO_free(bio);\n  return pem;\n}\n\ninline std::string evp_pkey_to_pem(EVP_PKEY *key) {\n  if (!key) return {};\n  BIO *bio = BIO_new(BIO_s_mem());\n  if (!bio) return {};\n  if (PEM_write_bio_PrivateKey(bio, key, nullptr, nullptr, 0, nullptr,\n                               nullptr) != 1) {\n    BIO_free(bio);\n    return {};\n  }\n  char *data = nullptr;\n  long len = BIO_get_mem_data(bio, &data);\n  std::string pem(data, static_cast<size_t>(len));\n  BIO_free(bio);\n  return pem;\n}\n\ninline std::string x509_store_to_pem(X509_STORE *store) {\n  if (!store) return {};\n  std::string pem;\n  auto objs = X509_STORE_get0_objects(store);\n  if (!objs) return {};\n  auto count = sk_X509_OBJECT_num(objs);\n  for (decltype(count) i = 0; i < count; i++) {\n    auto obj = sk_X509_OBJECT_value(objs, i);\n    if (X509_OBJECT_get_type(obj) == X509_LU_X509) {\n      auto cert = X509_OBJECT_get0_X509(obj);\n      if (cert) { pem += x509_to_pem(cert); }\n    }\n  }\n  return pem;\n}\n\n// Helper to map OpenSSL SSL_get_error to ErrorCode\ninline ErrorCode map_ssl_error(int ssl_error, int &out_errno) {\n  switch (ssl_error) {\n  case SSL_ERROR_NONE: return ErrorCode::Success;\n  case SSL_ERROR_WANT_READ: return ErrorCode::WantRead;\n  case SSL_ERROR_WANT_WRITE: return ErrorCode::WantWrite;\n  case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed;\n  case SSL_ERROR_SYSCALL: out_errno = errno; return ErrorCode::SyscallError;\n  case SSL_ERROR_SSL:\n  default: return ErrorCode::Fatal;\n  }\n}\n\n// Helper: Create client CA list from PEM string\n// Returns a new STACK_OF(X509_NAME)* or nullptr on failure\n// Caller takes ownership of returned list\ninline STACK_OF(X509_NAME) *\n    create_client_ca_list_from_pem(const char *ca_pem) {\n  if (!ca_pem) { return nullptr; }\n\n  auto ca_list = sk_X509_NAME_new_null();\n  if (!ca_list) { return nullptr; }\n\n  BIO *bio = BIO_new_mem_buf(ca_pem, -1);\n  if (!bio) {\n    sk_X509_NAME_pop_free(ca_list, X509_NAME_free);\n    return nullptr;\n  }\n\n  X509 *cert = nullptr;\n  while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) !=\n         nullptr) {\n    X509_NAME *name = X509_get_subject_name(cert);\n    if (name) { sk_X509_NAME_push(ca_list, X509_NAME_dup(name)); }\n    X509_free(cert);\n  }\n  BIO_free(bio);\n\n  return ca_list;\n}\n\n// Helper: Extract CA names from X509_STORE\n// Returns a new STACK_OF(X509_NAME)* or nullptr on failure\n// Caller takes ownership of returned list\ninline STACK_OF(X509_NAME) *\n    extract_client_ca_list_from_store(X509_STORE *store) {\n  if (!store) { return nullptr; }\n\n  auto ca_list = sk_X509_NAME_new_null();\n  if (!ca_list) { return nullptr; }\n\n  auto objs = X509_STORE_get0_objects(store);\n  if (!objs) {\n    sk_X509_NAME_free(ca_list);\n    return nullptr;\n  }\n\n  auto count = sk_X509_OBJECT_num(objs);\n  for (decltype(count) i = 0; i < count; i++) {\n    auto obj = sk_X509_OBJECT_value(objs, i);\n    if (X509_OBJECT_get_type(obj) == X509_LU_X509) {\n      auto cert = X509_OBJECT_get0_X509(obj);\n      if (cert) {\n        auto subject = X509_get_subject_name(cert);\n        if (subject) {\n          auto name_dup = X509_NAME_dup(subject);\n          if (name_dup) { sk_X509_NAME_push(ca_list, name_dup); }\n        }\n      }\n    }\n  }\n\n  if (sk_X509_NAME_num(ca_list) == 0) {\n    sk_X509_NAME_free(ca_list);\n    return nullptr;\n  }\n\n  return ca_list;\n}\n\n// OpenSSL verify callback wrapper\ninline int openssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) {\n  auto &callback = get_verify_callback();\n  if (!callback) { return preverify_ok; }\n\n  // Get SSL object from X509_STORE_CTX\n  auto ssl = static_cast<SSL *>(\n      X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()));\n  if (!ssl) { return preverify_ok; }\n\n  // Get current certificate and depth\n  auto cert = X509_STORE_CTX_get_current_cert(ctx);\n  int depth = X509_STORE_CTX_get_error_depth(ctx);\n  int error = X509_STORE_CTX_get_error(ctx);\n\n  // Build context\n  VerifyContext verify_ctx;\n  verify_ctx.session = static_cast<session_t>(ssl);\n  verify_ctx.cert = static_cast<cert_t>(cert);\n  verify_ctx.depth = depth;\n  verify_ctx.preverify_ok = (preverify_ok != 0);\n  verify_ctx.error_code = error;\n  verify_ctx.error_string =\n      (error != X509_V_OK) ? X509_verify_cert_error_string(error) : nullptr;\n\n  return callback(verify_ctx) ? 1 : 0;\n}\n\n} // namespace impl\n\ninline ctx_t create_client_context() {\n  SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());\n  if (ctx) {\n    // Disable auto-retry to properly handle non-blocking I/O\n    SSL_CTX_clear_mode(ctx, SSL_MODE_AUTO_RETRY);\n    // Set minimum TLS version\n    SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);\n  }\n  return static_cast<ctx_t>(ctx);\n}\n\ninline void free_context(ctx_t ctx) {\n  if (ctx) { SSL_CTX_free(static_cast<SSL_CTX *>(ctx)); }\n}\n\ninline bool set_min_version(ctx_t ctx, Version version) {\n  if (!ctx) return false;\n  return SSL_CTX_set_min_proto_version(static_cast<SSL_CTX *>(ctx),\n                                       static_cast<int>(version)) == 1;\n}\n\ninline bool load_ca_pem(ctx_t ctx, const char *pem, size_t len) {\n  if (!ctx || !pem || len == 0) return false;\n\n  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);\n  auto store = SSL_CTX_get_cert_store(ssl_ctx);\n  if (!store) return false;\n\n  auto bio = BIO_new_mem_buf(pem, static_cast<int>(len));\n  if (!bio) return false;\n\n  bool ok = true;\n  X509 *cert = nullptr;\n  while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) !=\n         nullptr) {\n    if (X509_STORE_add_cert(store, cert) != 1) {\n      // Ignore duplicate errors\n      auto err = ERR_peek_last_error();\n      if (ERR_GET_REASON(err) != X509_R_CERT_ALREADY_IN_HASH_TABLE) {\n        ok = false;\n      }\n    }\n    X509_free(cert);\n    if (!ok) break;\n  }\n  BIO_free(bio);\n\n  // Clear any \"no more certificates\" errors\n  ERR_clear_error();\n  return ok;\n}\n\ninline bool load_ca_file(ctx_t ctx, const char *file_path) {\n  if (!ctx || !file_path) return false;\n  return SSL_CTX_load_verify_locations(static_cast<SSL_CTX *>(ctx), file_path,\n                                       nullptr) == 1;\n}\n\ninline bool load_ca_dir(ctx_t ctx, const char *dir_path) {\n  if (!ctx || !dir_path) return false;\n  return SSL_CTX_load_verify_locations(static_cast<SSL_CTX *>(ctx), nullptr,\n                                       dir_path) == 1;\n}\n\ninline bool load_system_certs(ctx_t ctx) {\n  if (!ctx) return false;\n  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);\n\n#ifdef _WIN32\n  // Windows: Load from system certificate store (ROOT and CA)\n  auto store = SSL_CTX_get_cert_store(ssl_ctx);\n  if (!store) return false;\n\n  bool loaded_any = false;\n  static const wchar_t *store_names[] = {L\"ROOT\", L\"CA\"};\n  for (auto store_name : store_names) {\n    auto hStore = CertOpenSystemStoreW(NULL, store_name);\n    if (!hStore) continue;\n\n    PCCERT_CONTEXT pContext = nullptr;\n    while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) !=\n           nullptr) {\n      const unsigned char *data = pContext->pbCertEncoded;\n      auto x509 = d2i_X509(nullptr, &data, pContext->cbCertEncoded);\n      if (x509) {\n        if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; }\n        X509_free(x509);\n      }\n    }\n    CertCloseStore(hStore, 0);\n  }\n  return loaded_any;\n\n#elif defined(__APPLE__)\n#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN\n  // macOS: Load from Keychain\n  auto store = SSL_CTX_get_cert_store(ssl_ctx);\n  if (!store) return false;\n\n  CFArrayRef certs = nullptr;\n  if (SecTrustCopyAnchorCertificates(&certs) != errSecSuccess || !certs) {\n    return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;\n  }\n\n  bool loaded_any = false;\n  auto count = CFArrayGetCount(certs);\n  for (CFIndex i = 0; i < count; i++) {\n    auto cert = reinterpret_cast<SecCertificateRef>(\n        const_cast<void *>(CFArrayGetValueAtIndex(certs, i)));\n    CFDataRef der = SecCertificateCopyData(cert);\n    if (der) {\n      const unsigned char *data = CFDataGetBytePtr(der);\n      auto x509 = d2i_X509(nullptr, &data, CFDataGetLength(der));\n      if (x509) {\n        if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; }\n        X509_free(x509);\n      }\n      CFRelease(der);\n    }\n  }\n  CFRelease(certs);\n  return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;\n#else\n  return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;\n#endif\n\n#else\n  // Other Unix: use default verify paths\n  return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;\n#endif\n}\n\ninline bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key,\n                                const char *password) {\n  if (!ctx || !cert || !key) return false;\n\n  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);\n\n  // Load certificate\n  auto cert_bio = BIO_new_mem_buf(cert, -1);\n  if (!cert_bio) return false;\n\n  auto x509 = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr);\n  BIO_free(cert_bio);\n  if (!x509) return false;\n\n  auto cert_ok = SSL_CTX_use_certificate(ssl_ctx, x509) == 1;\n  X509_free(x509);\n  if (!cert_ok) return false;\n\n  // Load private key\n  auto key_bio = BIO_new_mem_buf(key, -1);\n  if (!key_bio) return false;\n\n  auto pkey = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr,\n                                      password ? const_cast<char *>(password)\n                                               : nullptr);\n  BIO_free(key_bio);\n  if (!pkey) return false;\n\n  auto key_ok = SSL_CTX_use_PrivateKey(ssl_ctx, pkey) == 1;\n  EVP_PKEY_free(pkey);\n\n  return key_ok && SSL_CTX_check_private_key(ssl_ctx) == 1;\n}\n\ninline bool set_client_cert_file(ctx_t ctx, const char *cert_path,\n                                 const char *key_path, const char *password) {\n  if (!ctx || !cert_path || !key_path) return false;\n\n  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);\n\n  if (password && password[0] != '\\0') {\n    SSL_CTX_set_default_passwd_cb_userdata(\n        ssl_ctx, reinterpret_cast<void *>(const_cast<char *>(password)));\n  }\n\n  return SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_path) == 1 &&\n         SSL_CTX_use_PrivateKey_file(ssl_ctx, key_path, SSL_FILETYPE_PEM) == 1;\n}\n\ninline ctx_t create_server_context() {\n  SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());\n  if (ctx) {\n    SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION |\n                                 SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION);\n    SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);\n  }\n  return static_cast<ctx_t>(ctx);\n}\n\ninline void set_verify_client(ctx_t ctx, bool require) {\n  if (!ctx) return;\n  SSL_CTX_set_verify(static_cast<SSL_CTX *>(ctx),\n                     require\n                         ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT)\n                         : SSL_VERIFY_NONE,\n                     nullptr);\n}\n\ninline session_t create_session(ctx_t ctx, socket_t sock) {\n  if (!ctx || sock == INVALID_SOCKET) return nullptr;\n\n  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);\n  SSL *ssl = SSL_new(ssl_ctx);\n  if (!ssl) return nullptr;\n\n  // Disable auto-retry for proper non-blocking I/O handling\n  SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY);\n\n  auto bio = BIO_new_socket(static_cast<int>(sock), BIO_NOCLOSE);\n  if (!bio) {\n    SSL_free(ssl);\n    return nullptr;\n  }\n\n  SSL_set_bio(ssl, bio, bio);\n  return static_cast<session_t>(ssl);\n}\n\ninline void free_session(session_t session) {\n  if (session) { SSL_free(static_cast<SSL *>(session)); }\n}\n\ninline bool set_sni(session_t session, const char *hostname) {\n  if (!session || !hostname) return false;\n\n  auto ssl = static_cast<SSL *>(session);\n\n  // Set SNI (Server Name Indication) only - does not enable verification\n#if defined(OPENSSL_IS_BORINGSSL)\n  return SSL_set_tlsext_host_name(ssl, hostname) == 1;\n#else\n  // Direct call instead of macro to suppress -Wold-style-cast warning\n  return SSL_ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name,\n                  static_cast<void *>(const_cast<char *>(hostname))) == 1;\n#endif\n}\n\ninline bool set_hostname(session_t session, const char *hostname) {\n  if (!session || !hostname) return false;\n\n  auto ssl = static_cast<SSL *>(session);\n\n  // Set SNI (Server Name Indication)\n  if (!set_sni(session, hostname)) { return false; }\n\n  // Enable hostname verification\n  auto param = SSL_get0_param(ssl);\n  if (!param) return false;\n\n  X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);\n  if (X509_VERIFY_PARAM_set1_host(param, hostname, 0) != 1) { return false; }\n\n  SSL_set_verify(ssl, SSL_VERIFY_PEER, nullptr);\n  return true;\n}\n\ninline TlsError connect(session_t session) {\n  if (!session) { return TlsError(); }\n\n  auto ssl = static_cast<SSL *>(session);\n  auto ret = SSL_connect(ssl);\n\n  TlsError err;\n  if (ret == 1) {\n    err.code = ErrorCode::Success;\n  } else {\n    auto ssl_err = SSL_get_error(ssl, ret);\n    err.code = impl::map_ssl_error(ssl_err, err.sys_errno);\n    err.backend_code = ERR_get_error();\n  }\n  return err;\n}\n\ninline TlsError accept(session_t session) {\n  if (!session) { return TlsError(); }\n\n  auto ssl = static_cast<SSL *>(session);\n  auto ret = SSL_accept(ssl);\n\n  TlsError err;\n  if (ret == 1) {\n    err.code = ErrorCode::Success;\n  } else {\n    auto ssl_err = SSL_get_error(ssl, ret);\n    err.code = impl::map_ssl_error(ssl_err, err.sys_errno);\n    err.backend_code = ERR_get_error();\n  }\n  return err;\n}\n\ninline bool connect_nonblocking(session_t session, socket_t sock,\n                                time_t timeout_sec, time_t timeout_usec,\n                                TlsError *err) {\n  if (!session) {\n    if (err) { err->code = ErrorCode::Fatal; }\n    return false;\n  }\n\n  auto ssl = static_cast<SSL *>(session);\n  auto bio = SSL_get_rbio(ssl);\n\n  // Set non-blocking mode for handshake\n  detail::set_nonblocking(sock, true);\n  if (bio) { BIO_set_nbio(bio, 1); }\n\n  auto cleanup = detail::scope_exit([&]() {\n    // Restore blocking mode after handshake\n    if (bio) { BIO_set_nbio(bio, 0); }\n    detail::set_nonblocking(sock, false);\n  });\n\n  auto res = 0;\n  while ((res = SSL_connect(ssl)) != 1) {\n    auto ssl_err = SSL_get_error(ssl, res);\n    switch (ssl_err) {\n    case SSL_ERROR_WANT_READ:\n      if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) {\n        continue;\n      }\n      break;\n    case SSL_ERROR_WANT_WRITE:\n      if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) {\n        continue;\n      }\n      break;\n    default: break;\n    }\n    if (err) {\n      err->code = impl::map_ssl_error(ssl_err, err->sys_errno);\n      err->backend_code = ERR_get_error();\n    }\n    return false;\n  }\n  if (err) { err->code = ErrorCode::Success; }\n  return true;\n}\n\ninline bool accept_nonblocking(session_t session, socket_t sock,\n                               time_t timeout_sec, time_t timeout_usec,\n                               TlsError *err) {\n  if (!session) {\n    if (err) { err->code = ErrorCode::Fatal; }\n    return false;\n  }\n\n  auto ssl = static_cast<SSL *>(session);\n  auto bio = SSL_get_rbio(ssl);\n\n  // Set non-blocking mode for handshake\n  detail::set_nonblocking(sock, true);\n  if (bio) { BIO_set_nbio(bio, 1); }\n\n  auto cleanup = detail::scope_exit([&]() {\n    // Restore blocking mode after handshake\n    if (bio) { BIO_set_nbio(bio, 0); }\n    detail::set_nonblocking(sock, false);\n  });\n\n  auto res = 0;\n  while ((res = SSL_accept(ssl)) != 1) {\n    auto ssl_err = SSL_get_error(ssl, res);\n    switch (ssl_err) {\n    case SSL_ERROR_WANT_READ:\n      if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) {\n        continue;\n      }\n      break;\n    case SSL_ERROR_WANT_WRITE:\n      if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) {\n        continue;\n      }\n      break;\n    default: break;\n    }\n    if (err) {\n      err->code = impl::map_ssl_error(ssl_err, err->sys_errno);\n      err->backend_code = ERR_get_error();\n    }\n    return false;\n  }\n  if (err) { err->code = ErrorCode::Success; }\n  return true;\n}\n\ninline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {\n  if (!session || !buf) {\n    err.code = ErrorCode::Fatal;\n    return -1;\n  }\n\n  auto ssl = static_cast<SSL *>(session);\n  constexpr auto max_len =\n      static_cast<size_t>((std::numeric_limits<int>::max)());\n  if (len > max_len) { len = max_len; }\n  auto ret = SSL_read(ssl, buf, static_cast<int>(len));\n\n  if (ret > 0) {\n    err.code = ErrorCode::Success;\n    return ret;\n  }\n\n  auto ssl_err = SSL_get_error(ssl, ret);\n  err.code = impl::map_ssl_error(ssl_err, err.sys_errno);\n  if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); }\n  return -1;\n}\n\ninline ssize_t write(session_t session, const void *buf, size_t len,\n                     TlsError &err) {\n  if (!session || !buf) {\n    err.code = ErrorCode::Fatal;\n    return -1;\n  }\n\n  auto ssl = static_cast<SSL *>(session);\n  auto ret = SSL_write(ssl, buf, static_cast<int>(len));\n\n  if (ret > 0) {\n    err.code = ErrorCode::Success;\n    return ret;\n  }\n\n  auto ssl_err = SSL_get_error(ssl, ret);\n  err.code = impl::map_ssl_error(ssl_err, err.sys_errno);\n  if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); }\n  return -1;\n}\n\ninline int pending(const_session_t session) {\n  if (!session) return 0;\n  return SSL_pending(static_cast<SSL *>(const_cast<void *>(session)));\n}\n\ninline void shutdown(session_t session, bool graceful) {\n  if (!session) return;\n\n  auto ssl = static_cast<SSL *>(session);\n  if (graceful) {\n    // First call sends close_notify\n    if (SSL_shutdown(ssl) == 0) {\n      // Second call waits for peer's close_notify\n      SSL_shutdown(ssl);\n    }\n  }\n}\n\ninline bool is_peer_closed(session_t session, socket_t sock) {\n  if (!session) return true;\n\n  // Temporarily set socket to non-blocking to avoid blocking on SSL_peek\n  detail::set_nonblocking(sock, true);\n  auto se = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); });\n\n  auto ssl = static_cast<SSL *>(session);\n  char buf;\n  auto ret = SSL_peek(ssl, &buf, 1);\n  if (ret > 0) return false;\n\n  auto err = SSL_get_error(ssl, ret);\n  return err == SSL_ERROR_ZERO_RETURN;\n}\n\ninline cert_t get_peer_cert(const_session_t session) {\n  if (!session) return nullptr;\n  return static_cast<cert_t>(SSL_get1_peer_certificate(\n      static_cast<SSL *>(const_cast<void *>(session))));\n}\n\ninline void free_cert(cert_t cert) {\n  if (cert) { X509_free(static_cast<X509 *>(cert)); }\n}\n\ninline bool verify_hostname(cert_t cert, const char *hostname) {\n  if (!cert || !hostname) return false;\n\n  auto x509 = static_cast<X509 *>(cert);\n\n  // Use X509_check_ip_asc for IP addresses, X509_check_host for DNS names\n  if (detail::is_ip_address(hostname)) {\n    return X509_check_ip_asc(x509, hostname, 0) == 1;\n  }\n  return X509_check_host(x509, hostname, strlen(hostname), 0, nullptr) == 1;\n}\n\ninline uint64_t hostname_mismatch_code() {\n  return static_cast<uint64_t>(X509_V_ERR_HOSTNAME_MISMATCH);\n}\n\ninline long get_verify_result(const_session_t session) {\n  if (!session) return X509_V_ERR_UNSPECIFIED;\n  return SSL_get_verify_result(static_cast<SSL *>(const_cast<void *>(session)));\n}\n\ninline std::string get_cert_subject_cn(cert_t cert) {\n  if (!cert) return \"\";\n  auto x509 = static_cast<X509 *>(cert);\n  auto subject_name = X509_get_subject_name(x509);\n  if (!subject_name) return \"\";\n\n  char buf[256];\n  auto len =\n      X509_NAME_get_text_by_NID(subject_name, NID_commonName, buf, sizeof(buf));\n  if (len < 0) return \"\";\n  return std::string(buf, static_cast<size_t>(len));\n}\n\ninline std::string get_cert_issuer_name(cert_t cert) {\n  if (!cert) return \"\";\n  auto x509 = static_cast<X509 *>(cert);\n  auto issuer_name = X509_get_issuer_name(x509);\n  if (!issuer_name) return \"\";\n\n  char buf[256];\n  X509_NAME_oneline(issuer_name, buf, sizeof(buf));\n  return std::string(buf);\n}\n\ninline bool get_cert_sans(cert_t cert, std::vector<SanEntry> &sans) {\n  sans.clear();\n  if (!cert) return false;\n  auto x509 = static_cast<X509 *>(cert);\n\n  auto names = static_cast<GENERAL_NAMES *>(\n      X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr));\n  if (!names) return true; // No SANs is valid\n\n  auto count = sk_GENERAL_NAME_num(names);\n  for (int i = 0; i < count; i++) {\n    auto gen = sk_GENERAL_NAME_value(names, i);\n    if (!gen) continue;\n\n    SanEntry entry;\n    switch (gen->type) {\n    case GEN_DNS:\n      entry.type = SanType::DNS;\n      if (gen->d.dNSName) {\n        entry.value = std::string(\n            reinterpret_cast<const char *>(\n                ASN1_STRING_get0_data(gen->d.dNSName)),\n            static_cast<size_t>(ASN1_STRING_length(gen->d.dNSName)));\n      }\n      break;\n    case GEN_IPADD:\n      entry.type = SanType::IP;\n      if (gen->d.iPAddress) {\n        auto data = ASN1_STRING_get0_data(gen->d.iPAddress);\n        auto len = ASN1_STRING_length(gen->d.iPAddress);\n        if (len == 4) {\n          // IPv4\n          char buf[INET_ADDRSTRLEN];\n          inet_ntop(AF_INET, data, buf, sizeof(buf));\n          entry.value = buf;\n        } else if (len == 16) {\n          // IPv6\n          char buf[INET6_ADDRSTRLEN];\n          inet_ntop(AF_INET6, data, buf, sizeof(buf));\n          entry.value = buf;\n        }\n      }\n      break;\n    case GEN_EMAIL:\n      entry.type = SanType::EMAIL;\n      if (gen->d.rfc822Name) {\n        entry.value = std::string(\n            reinterpret_cast<const char *>(\n                ASN1_STRING_get0_data(gen->d.rfc822Name)),\n            static_cast<size_t>(ASN1_STRING_length(gen->d.rfc822Name)));\n      }\n      break;\n    case GEN_URI:\n      entry.type = SanType::URI;\n      if (gen->d.uniformResourceIdentifier) {\n        entry.value = std::string(\n            reinterpret_cast<const char *>(\n                ASN1_STRING_get0_data(gen->d.uniformResourceIdentifier)),\n            static_cast<size_t>(\n                ASN1_STRING_length(gen->d.uniformResourceIdentifier)));\n      }\n      break;\n    default: entry.type = SanType::OTHER; break;\n    }\n\n    if (!entry.value.empty()) { sans.push_back(std::move(entry)); }\n  }\n\n  GENERAL_NAMES_free(names);\n  return true;\n}\n\ninline bool get_cert_validity(cert_t cert, time_t &not_before,\n                              time_t &not_after) {\n  if (!cert) return false;\n  auto x509 = static_cast<X509 *>(cert);\n\n  auto nb = X509_get0_notBefore(x509);\n  auto na = X509_get0_notAfter(x509);\n  if (!nb || !na) return false;\n\n  ASN1_TIME *epoch = ASN1_TIME_new();\n  if (!epoch) return false;\n  auto se = detail::scope_exit([&] { ASN1_TIME_free(epoch); });\n\n  if (!ASN1_TIME_set(epoch, 0)) return false;\n\n  int pday, psec;\n\n  if (!ASN1_TIME_diff(&pday, &psec, epoch, nb)) return false;\n  not_before = 86400 * (time_t)pday + psec;\n\n  if (!ASN1_TIME_diff(&pday, &psec, epoch, na)) return false;\n  not_after = 86400 * (time_t)pday + psec;\n\n  return true;\n}\n\ninline std::string get_cert_serial(cert_t cert) {\n  if (!cert) return \"\";\n  auto x509 = static_cast<X509 *>(cert);\n\n  auto serial = X509_get_serialNumber(x509);\n  if (!serial) return \"\";\n\n  auto bn = ASN1_INTEGER_to_BN(serial, nullptr);\n  if (!bn) return \"\";\n\n  auto hex = BN_bn2hex(bn);\n  BN_free(bn);\n  if (!hex) return \"\";\n\n  std::string result(hex);\n  OPENSSL_free(hex);\n  return result;\n}\n\ninline bool get_cert_der(cert_t cert, std::vector<unsigned char> &der) {\n  if (!cert) return false;\n  auto x509 = static_cast<X509 *>(cert);\n  auto len = i2d_X509(x509, nullptr);\n  if (len < 0) return false;\n  der.resize(static_cast<size_t>(len));\n  auto p = der.data();\n  i2d_X509(x509, &p);\n  return true;\n}\n\ninline const char *get_sni(const_session_t session) {\n  if (!session) return nullptr;\n  auto ssl = static_cast<SSL *>(const_cast<void *>(session));\n  return SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);\n}\n\ninline uint64_t peek_error() { return ERR_peek_last_error(); }\n\ninline uint64_t get_error() { return ERR_get_error(); }\n\ninline std::string error_string(uint64_t code) {\n  char buf[256];\n  ERR_error_string_n(static_cast<unsigned long>(code), buf, sizeof(buf));\n  return std::string(buf);\n}\n\ninline ca_store_t create_ca_store(const char *pem, size_t len) {\n  auto mem = BIO_new_mem_buf(pem, static_cast<int>(len));\n  if (!mem) { return nullptr; }\n  auto mem_guard = detail::scope_exit([&] { BIO_free_all(mem); });\n\n  auto inf = PEM_X509_INFO_read_bio(mem, nullptr, nullptr, nullptr);\n  if (!inf) { return nullptr; }\n\n  auto store = X509_STORE_new();\n  if (store) {\n    for (auto i = 0; i < static_cast<int>(sk_X509_INFO_num(inf)); i++) {\n      auto itmp = sk_X509_INFO_value(inf, i);\n      if (!itmp) { continue; }\n      if (itmp->x509) { X509_STORE_add_cert(store, itmp->x509); }\n      if (itmp->crl) { X509_STORE_add_crl(store, itmp->crl); }\n    }\n  }\n\n  sk_X509_INFO_pop_free(inf, X509_INFO_free);\n  return static_cast<ca_store_t>(store);\n}\n\ninline void free_ca_store(ca_store_t store) {\n  if (store) { X509_STORE_free(static_cast<X509_STORE *>(store)); }\n}\n\ninline bool set_ca_store(ctx_t ctx, ca_store_t store) {\n  if (!ctx || !store) { return false; }\n  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);\n  auto x509_store = static_cast<X509_STORE *>(store);\n\n  // Check if same store is already set\n  if (SSL_CTX_get_cert_store(ssl_ctx) == x509_store) { return true; }\n\n  // SSL_CTX_set_cert_store takes ownership and frees the old store\n  SSL_CTX_set_cert_store(ssl_ctx, x509_store);\n  return true;\n}\n\ninline size_t get_ca_certs(ctx_t ctx, std::vector<cert_t> &certs) {\n  certs.clear();\n  if (!ctx) { return 0; }\n  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);\n\n  auto store = SSL_CTX_get_cert_store(ssl_ctx);\n  if (!store) { return 0; }\n\n  auto objs = X509_STORE_get0_objects(store);\n  if (!objs) { return 0; }\n\n  auto count = sk_X509_OBJECT_num(objs);\n  for (decltype(count) i = 0; i < count; i++) {\n    auto obj = sk_X509_OBJECT_value(objs, i);\n    if (!obj) { continue; }\n    if (X509_OBJECT_get_type(obj) == X509_LU_X509) {\n      auto x509 = X509_OBJECT_get0_X509(obj);\n      if (x509) {\n        // Increment reference count so caller can free it\n        X509_up_ref(x509);\n        certs.push_back(static_cast<cert_t>(x509));\n      }\n    }\n  }\n  return certs.size();\n}\n\ninline std::vector<std::string> get_ca_names(ctx_t ctx) {\n  std::vector<std::string> names;\n  if (!ctx) { return names; }\n  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);\n\n  auto store = SSL_CTX_get_cert_store(ssl_ctx);\n  if (!store) { return names; }\n\n  auto objs = X509_STORE_get0_objects(store);\n  if (!objs) { return names; }\n\n  auto count = sk_X509_OBJECT_num(objs);\n  for (decltype(count) i = 0; i < count; i++) {\n    auto obj = sk_X509_OBJECT_value(objs, i);\n    if (!obj) { continue; }\n    if (X509_OBJECT_get_type(obj) == X509_LU_X509) {\n      auto x509 = X509_OBJECT_get0_X509(obj);\n      if (x509) {\n        auto subject = X509_get_subject_name(x509);\n        if (subject) {\n          char buf[512];\n          X509_NAME_oneline(subject, buf, sizeof(buf));\n          names.push_back(buf);\n        }\n      }\n    }\n  }\n  return names;\n}\n\ninline bool update_server_cert(ctx_t ctx, const char *cert_pem,\n                               const char *key_pem, const char *password) {\n  if (!ctx || !cert_pem || !key_pem) { return false; }\n  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);\n\n  // Load certificate from PEM\n  auto cert_bio = BIO_new_mem_buf(cert_pem, -1);\n  if (!cert_bio) { return false; }\n  auto cert = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr);\n  BIO_free(cert_bio);\n  if (!cert) { return false; }\n\n  // Load private key from PEM\n  auto key_bio = BIO_new_mem_buf(key_pem, -1);\n  if (!key_bio) {\n    X509_free(cert);\n    return false;\n  }\n  auto key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr,\n                                     password ? const_cast<char *>(password)\n                                              : nullptr);\n  BIO_free(key_bio);\n  if (!key) {\n    X509_free(cert);\n    return false;\n  }\n\n  // Update certificate and key\n  auto ret = SSL_CTX_use_certificate(ssl_ctx, cert) == 1 &&\n             SSL_CTX_use_PrivateKey(ssl_ctx, key) == 1;\n\n  X509_free(cert);\n  EVP_PKEY_free(key);\n  return ret;\n}\n\ninline bool update_server_client_ca(ctx_t ctx, const char *ca_pem) {\n  if (!ctx || !ca_pem) { return false; }\n  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);\n\n  // Create new X509_STORE from PEM\n  auto store = create_ca_store(ca_pem, strlen(ca_pem));\n  if (!store) { return false; }\n\n  // SSL_CTX_set_cert_store takes ownership\n  SSL_CTX_set_cert_store(ssl_ctx, static_cast<X509_STORE *>(store));\n\n  // Set client CA list for client certificate request\n  auto ca_list = impl::create_client_ca_list_from_pem(ca_pem);\n  if (ca_list) {\n    // SSL_CTX_set_client_CA_list takes ownership of ca_list\n    SSL_CTX_set_client_CA_list(ssl_ctx, ca_list);\n  }\n\n  return true;\n}\n\ninline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) {\n  if (!ctx) { return false; }\n  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);\n\n  impl::get_verify_callback() = std::move(callback);\n\n  if (impl::get_verify_callback()) {\n    SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, impl::openssl_verify_callback);\n  } else {\n    SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, nullptr);\n  }\n  return true;\n}\n\ninline long get_verify_error(const_session_t session) {\n  if (!session) { return -1; }\n  auto ssl = static_cast<SSL *>(const_cast<void *>(session));\n  return SSL_get_verify_result(ssl);\n}\n\ninline std::string verify_error_string(long error_code) {\n  if (error_code == X509_V_OK) { return \"\"; }\n  const char *str = X509_verify_cert_error_string(static_cast<int>(error_code));\n  return str ? str : \"unknown error\";\n}\n\nnamespace impl {\n\n// OpenSSL-specific helpers for public API wrappers\ninline ctx_t create_server_context_from_x509(X509 *cert, EVP_PKEY *key,\n                                             X509_STORE *client_ca_store,\n                                             int &out_error) {\n  out_error = 0;\n  auto cert_pem = x509_to_pem(cert);\n  auto key_pem = evp_pkey_to_pem(key);\n  if (cert_pem.empty() || key_pem.empty()) {\n    out_error = static_cast<int>(ERR_get_error());\n    return nullptr;\n  }\n\n  auto ctx = create_server_context();\n  if (!ctx) {\n    out_error = static_cast<int>(get_error());\n    return nullptr;\n  }\n\n  if (!set_server_cert_pem(ctx, cert_pem.c_str(), key_pem.c_str(), nullptr)) {\n    out_error = static_cast<int>(get_error());\n    free_context(ctx);\n    return nullptr;\n  }\n\n  if (client_ca_store) {\n    // Set cert store for verification (SSL_CTX_set_cert_store takes ownership)\n    SSL_CTX_set_cert_store(static_cast<SSL_CTX *>(ctx), client_ca_store);\n\n    // Extract and set client CA list directly from store (more efficient than\n    // PEM conversion)\n    auto ca_list = extract_client_ca_list_from_store(client_ca_store);\n    if (ca_list) {\n      SSL_CTX_set_client_CA_list(static_cast<SSL_CTX *>(ctx), ca_list);\n    }\n\n    set_verify_client(ctx, true);\n  }\n\n  return ctx;\n}\n\ninline void update_server_certs_from_x509(ctx_t ctx, X509 *cert, EVP_PKEY *key,\n                                          X509_STORE *client_ca_store) {\n  auto cert_pem = x509_to_pem(cert);\n  auto key_pem = evp_pkey_to_pem(key);\n\n  if (!cert_pem.empty() && !key_pem.empty()) {\n    update_server_cert(ctx, cert_pem.c_str(), key_pem.c_str(), nullptr);\n  }\n\n  if (client_ca_store) {\n    auto ca_pem = x509_store_to_pem(client_ca_store);\n    if (!ca_pem.empty()) { update_server_client_ca(ctx, ca_pem.c_str()); }\n    X509_STORE_free(client_ca_store);\n  }\n}\n\ninline ctx_t create_client_context_from_x509(X509 *cert, EVP_PKEY *key,\n                                             const char *password,\n                                             unsigned long &out_error) {\n  out_error = 0;\n  auto ctx = create_client_context();\n  if (!ctx) {\n    out_error = static_cast<unsigned long>(get_error());\n    return nullptr;\n  }\n\n  if (cert && key) {\n    auto cert_pem = x509_to_pem(cert);\n    auto key_pem = evp_pkey_to_pem(key);\n    if (cert_pem.empty() || key_pem.empty()) {\n      out_error = ERR_get_error();\n      free_context(ctx);\n      return nullptr;\n    }\n    if (!set_client_cert_pem(ctx, cert_pem.c_str(), key_pem.c_str(),\n                             password)) {\n      out_error = static_cast<unsigned long>(get_error());\n      free_context(ctx);\n      return nullptr;\n    }\n  }\n\n  return ctx;\n}\n\n} // namespace impl\n\n} // namespace tls\n\n// ClientImpl::set_ca_cert_store - defined here to use\n// tls::impl::x509_store_to_pem Deprecated: converts X509_STORE to PEM and\n// stores for redirect transfer\ninline void ClientImpl::set_ca_cert_store(X509_STORE *ca_cert_store) {\n  if (ca_cert_store) {\n    ca_cert_pem_ = tls::impl::x509_store_to_pem(ca_cert_store);\n  }\n}\n\ninline SSLServer::SSLServer(X509 *cert, EVP_PKEY *private_key,\n                            X509_STORE *client_ca_cert_store) {\n  ctx_ = tls::impl::create_server_context_from_x509(\n      cert, private_key, client_ca_cert_store, last_ssl_error_);\n}\n\ninline SSLServer::SSLServer(\n    const std::function<bool(SSL_CTX &ssl_ctx)> &setup_ssl_ctx_callback) {\n  // Use abstract API to create context\n  ctx_ = tls::create_server_context();\n  if (ctx_) {\n    // Pass to OpenSSL-specific callback (ctx_ is SSL_CTX* internally)\n    auto ssl_ctx = static_cast<SSL_CTX *>(ctx_);\n    if (!setup_ssl_ctx_callback(*ssl_ctx)) {\n      tls::free_context(ctx_);\n      ctx_ = nullptr;\n    }\n  }\n}\n\ninline SSL_CTX *SSLServer::ssl_context() const {\n  return static_cast<SSL_CTX *>(ctx_);\n}\n\ninline void SSLServer::update_certs(X509 *cert, EVP_PKEY *private_key,\n                                    X509_STORE *client_ca_cert_store) {\n  std::lock_guard<std::mutex> guard(ctx_mutex_);\n  tls::impl::update_server_certs_from_x509(ctx_, cert, private_key,\n                                           client_ca_cert_store);\n}\n\ninline SSLClient::SSLClient(const std::string &host, int port,\n                            X509 *client_cert, EVP_PKEY *client_key,\n                            const std::string &private_key_password)\n    : ClientImpl(host, port) {\n  const char *password =\n      private_key_password.empty() ? nullptr : private_key_password.c_str();\n  ctx_ = tls::impl::create_client_context_from_x509(\n      client_cert, client_key, password, last_backend_error_);\n}\n\ninline long SSLClient::get_verify_result() const { return verify_result_; }\n\ninline void SSLClient::set_server_certificate_verifier(\n    std::function<SSLVerifierResponse(SSL *ssl)> verifier) {\n  // Wrap SSL* callback into backend-independent session_verifier_\n  auto v = std::make_shared<std::function<SSLVerifierResponse(SSL *)>>(\n      std::move(verifier));\n  session_verifier_ = [v](tls::session_t session) {\n    return (*v)(static_cast<SSL *>(session));\n  };\n}\n\ninline SSL_CTX *SSLClient::ssl_context() const {\n  return static_cast<SSL_CTX *>(ctx_);\n}\n\ninline bool SSLClient::verify_host(X509 *server_cert) const {\n  /* Quote from RFC2818 section 3.1 \"Server Identity\"\n\n     If a subjectAltName extension of type dNSName is present, that MUST\n     be used as the identity. Otherwise, the (most specific) Common Name\n     field in the Subject field of the certificate MUST be used. Although\n     the use of the Common Name is existing practice, it is deprecated and\n     Certification Authorities are encouraged to use the dNSName instead.\n\n     Matching is performed using the matching rules specified by\n     [RFC2459].  If more than one identity of a given type is present in\n     the certificate (e.g., more than one dNSName name, a match in any one\n     of the set is considered acceptable.) Names may contain the wildcard\n     character * which is considered to match any single domain name\n     component or component fragment. E.g., *.a.com matches foo.a.com but\n     not bar.foo.a.com. f*.com matches foo.com but not bar.com.\n\n     In some cases, the URI is specified as an IP address rather than a\n     hostname. In this case, the iPAddress subjectAltName must be present\n     in the certificate and must exactly match the IP in the URI.\n\n  */\n  return verify_host_with_subject_alt_name(server_cert) ||\n         verify_host_with_common_name(server_cert);\n}\n\ninline bool\nSSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const {\n  auto ret = false;\n\n  auto type = GEN_DNS;\n\n  struct in6_addr addr6 = {};\n  struct in_addr addr = {};\n  size_t addr_len = 0;\n\n#ifndef __MINGW32__\n  if (inet_pton(AF_INET6, host_.c_str(), &addr6)) {\n    type = GEN_IPADD;\n    addr_len = sizeof(struct in6_addr);\n  } else if (inet_pton(AF_INET, host_.c_str(), &addr)) {\n    type = GEN_IPADD;\n    addr_len = sizeof(struct in_addr);\n  }\n#endif\n\n  auto alt_names = static_cast<const struct stack_st_GENERAL_NAME *>(\n      X509_get_ext_d2i(server_cert, NID_subject_alt_name, nullptr, nullptr));\n\n  if (alt_names) {\n    auto dsn_matched = false;\n    auto ip_matched = false;\n\n    auto count = sk_GENERAL_NAME_num(alt_names);\n\n    for (decltype(count) i = 0; i < count && !dsn_matched; i++) {\n      auto val = sk_GENERAL_NAME_value(alt_names, i);\n      if (!val || val->type != type) { continue; }\n\n      auto name =\n          reinterpret_cast<const char *>(ASN1_STRING_get0_data(val->d.ia5));\n      if (name == nullptr) { continue; }\n\n      auto name_len = static_cast<size_t>(ASN1_STRING_length(val->d.ia5));\n\n      switch (type) {\n      case GEN_DNS:\n        dsn_matched =\n            detail::match_hostname(std::string(name, name_len), host_);\n        break;\n\n      case GEN_IPADD:\n        if (!memcmp(&addr6, name, addr_len) || !memcmp(&addr, name, addr_len)) {\n          ip_matched = true;\n        }\n        break;\n      }\n    }\n\n    if (dsn_matched || ip_matched) { ret = true; }\n  }\n\n  GENERAL_NAMES_free(const_cast<STACK_OF(GENERAL_NAME) *>(\n      reinterpret_cast<const STACK_OF(GENERAL_NAME) *>(alt_names)));\n  return ret;\n}\n\ninline bool SSLClient::verify_host_with_common_name(X509 *server_cert) const {\n  const auto subject_name = X509_get_subject_name(server_cert);\n\n  if (subject_name != nullptr) {\n    char name[BUFSIZ];\n    auto name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName,\n                                              name, sizeof(name));\n\n    if (name_len != -1) {\n      return detail::match_hostname(\n          std::string(name, static_cast<size_t>(name_len)), host_);\n    }\n  }\n\n  return false;\n}\n\n#endif // CPPHTTPLIB_OPENSSL_SUPPORT\n\n/*\n * Group 9: TLS abstraction layer - Mbed TLS backend\n */\n\n/*\n * Mbed TLS Backend Implementation\n */\n\n#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT\nnamespace tls {\n\nnamespace impl {\n\n// Mbed TLS session wrapper\nstruct MbedTlsSession {\n  mbedtls_ssl_context ssl;\n  socket_t sock = INVALID_SOCKET;\n  std::string hostname;     // For client: set via set_sni\n  std::string sni_hostname; // For server: received from client via SNI callback\n\n  MbedTlsSession() { mbedtls_ssl_init(&ssl); }\n\n  ~MbedTlsSession() { mbedtls_ssl_free(&ssl); }\n\n  MbedTlsSession(const MbedTlsSession &) = delete;\n  MbedTlsSession &operator=(const MbedTlsSession &) = delete;\n};\n\n// Thread-local error code accessor for Mbed TLS (since it doesn't have an error\n// queue)\ninline int &mbedtls_last_error() {\n  static thread_local int err = 0;\n  return err;\n}\n\n// Helper to map Mbed TLS error to ErrorCode\ninline ErrorCode map_mbedtls_error(int ret, int &out_errno) {\n  if (ret == 0) { return ErrorCode::Success; }\n  if (ret == MBEDTLS_ERR_SSL_WANT_READ) { return ErrorCode::WantRead; }\n  if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { return ErrorCode::WantWrite; }\n  if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) {\n    return ErrorCode::PeerClosed;\n  }\n  if (ret == MBEDTLS_ERR_NET_CONN_RESET || ret == MBEDTLS_ERR_NET_SEND_FAILED ||\n      ret == MBEDTLS_ERR_NET_RECV_FAILED) {\n    out_errno = errno;\n    return ErrorCode::SyscallError;\n  }\n  if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) {\n    return ErrorCode::CertVerifyFailed;\n  }\n  return ErrorCode::Fatal;\n}\n\n// BIO-like send callback for Mbed TLS\ninline int mbedtls_net_send_cb(void *ctx, const unsigned char *buf,\n                               size_t len) {\n  auto sock = *static_cast<socket_t *>(ctx);\n#ifdef _WIN32\n  auto ret =\n      send(sock, reinterpret_cast<const char *>(buf), static_cast<int>(len), 0);\n  if (ret == SOCKET_ERROR) {\n    int err = WSAGetLastError();\n    if (err == WSAEWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_WRITE; }\n    return MBEDTLS_ERR_NET_SEND_FAILED;\n  }\n#else\n  auto ret = send(sock, buf, len, 0);\n  if (ret < 0) {\n    if (errno == EAGAIN || errno == EWOULDBLOCK) {\n      return MBEDTLS_ERR_SSL_WANT_WRITE;\n    }\n    return MBEDTLS_ERR_NET_SEND_FAILED;\n  }\n#endif\n  return static_cast<int>(ret);\n}\n\n// BIO-like recv callback for Mbed TLS\ninline int mbedtls_net_recv_cb(void *ctx, unsigned char *buf, size_t len) {\n  auto sock = *static_cast<socket_t *>(ctx);\n#ifdef _WIN32\n  auto ret =\n      recv(sock, reinterpret_cast<char *>(buf), static_cast<int>(len), 0);\n  if (ret == SOCKET_ERROR) {\n    int err = WSAGetLastError();\n    if (err == WSAEWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_READ; }\n    return MBEDTLS_ERR_NET_RECV_FAILED;\n  }\n#else\n  auto ret = recv(sock, buf, len, 0);\n  if (ret < 0) {\n    if (errno == EAGAIN || errno == EWOULDBLOCK) {\n      return MBEDTLS_ERR_SSL_WANT_READ;\n    }\n    return MBEDTLS_ERR_NET_RECV_FAILED;\n  }\n#endif\n  if (ret == 0) { return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; }\n  return static_cast<int>(ret);\n}\n\n// MbedTlsContext constructor/destructor implementations\ninline MbedTlsContext::MbedTlsContext() {\n  mbedtls_ssl_config_init(&conf);\n  mbedtls_entropy_init(&entropy);\n  mbedtls_ctr_drbg_init(&ctr_drbg);\n  mbedtls_x509_crt_init(&ca_chain);\n  mbedtls_x509_crt_init(&own_cert);\n  mbedtls_pk_init(&own_key);\n}\n\ninline MbedTlsContext::~MbedTlsContext() {\n  mbedtls_pk_free(&own_key);\n  mbedtls_x509_crt_free(&own_cert);\n  mbedtls_x509_crt_free(&ca_chain);\n  mbedtls_ctr_drbg_free(&ctr_drbg);\n  mbedtls_entropy_free(&entropy);\n  mbedtls_ssl_config_free(&conf);\n}\n\n// Thread-local storage for SNI captured during handshake\n// This is needed because the SNI callback doesn't have a way to pass\n// session-specific data before the session is fully set up\ninline std::string &mbedpending_sni() {\n  static thread_local std::string sni;\n  return sni;\n}\n\n// SNI callback for Mbed TLS server to capture client's SNI hostname\ninline int mbedtls_sni_callback(void *p_ctx, mbedtls_ssl_context *ssl,\n                                const unsigned char *name, size_t name_len) {\n  (void)p_ctx;\n  (void)ssl;\n\n  // Store SNI name in thread-local storage\n  // It will be retrieved and stored in the session after handshake\n  if (name && name_len > 0) {\n    mbedpending_sni().assign(reinterpret_cast<const char *>(name), name_len);\n  } else {\n    mbedpending_sni().clear();\n  }\n  return 0; // Accept any SNI\n}\n\ninline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt,\n                                   int cert_depth, uint32_t *flags);\n\n// Check if a string is an IPv4 address\ninline bool is_ipv4_address(const std::string &str) {\n  int dots = 0;\n  for (char c : str) {\n    if (c == '.') {\n      dots++;\n    } else if (!isdigit(static_cast<unsigned char>(c))) {\n      return false;\n    }\n  }\n  return dots == 3;\n}\n\n// Parse IPv4 address string to bytes\ninline bool parse_ipv4(const std::string &str, unsigned char *out) {\n  int parts[4];\n  if (sscanf(str.c_str(), \"%d.%d.%d.%d\", &parts[0], &parts[1], &parts[2],\n             &parts[3]) != 4) {\n    return false;\n  }\n  for (int i = 0; i < 4; i++) {\n    if (parts[i] < 0 || parts[i] > 255) return false;\n    out[i] = static_cast<unsigned char>(parts[i]);\n  }\n  return true;\n}\n\n// MbedTLS verify callback wrapper\ninline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt,\n                                   int cert_depth, uint32_t *flags) {\n  auto &callback = get_verify_callback();\n  if (!callback) { return 0; } // Continue with default verification\n\n  // data points to the MbedTlsSession\n  auto *session = static_cast<MbedTlsSession *>(data);\n\n  // Build context\n  VerifyContext verify_ctx;\n  verify_ctx.session = static_cast<session_t>(session);\n  verify_ctx.cert = static_cast<cert_t>(crt);\n  verify_ctx.depth = cert_depth;\n  verify_ctx.preverify_ok = (*flags == 0);\n  verify_ctx.error_code = static_cast<long>(*flags);\n\n  // Convert Mbed TLS flags to error string\n  static thread_local char error_buf[256];\n  if (*flags != 0) {\n    mbedtls_x509_crt_verify_info(error_buf, sizeof(error_buf), \"\", *flags);\n    verify_ctx.error_string = error_buf;\n  } else {\n    verify_ctx.error_string = nullptr;\n  }\n\n  bool accepted = callback(verify_ctx);\n\n  if (accepted) {\n    *flags = 0; // Clear all error flags\n    return 0;\n  }\n  return MBEDTLS_ERR_X509_CERT_VERIFY_FAILED;\n}\n\n} // namespace impl\n\ninline ctx_t create_client_context() {\n  auto ctx = new (std::nothrow) impl::MbedTlsContext();\n  if (!ctx) { return nullptr; }\n\n  ctx->is_server = false;\n\n  // Seed the random number generator\n  const char *pers = \"httplib_client\";\n  int ret = mbedtls_ctr_drbg_seed(\n      &ctx->ctr_drbg, mbedtls_entropy_func, &ctx->entropy,\n      reinterpret_cast<const unsigned char *>(pers), strlen(pers));\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    delete ctx;\n    return nullptr;\n  }\n\n  // Set up SSL config for client\n  ret = mbedtls_ssl_config_defaults(&ctx->conf, MBEDTLS_SSL_IS_CLIENT,\n                                    MBEDTLS_SSL_TRANSPORT_STREAM,\n                                    MBEDTLS_SSL_PRESET_DEFAULT);\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    delete ctx;\n    return nullptr;\n  }\n\n  // Set random number generator\n  mbedtls_ssl_conf_rng(&ctx->conf, mbedtls_ctr_drbg_random, &ctx->ctr_drbg);\n\n  // Default: verify peer certificate\n  mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED);\n\n  // Set minimum TLS version to 1.2\n#ifdef CPPHTTPLIB_MBEDTLS_V3\n  mbedtls_ssl_conf_min_tls_version(&ctx->conf, MBEDTLS_SSL_VERSION_TLS1_2);\n#else\n  mbedtls_ssl_conf_min_version(&ctx->conf, MBEDTLS_SSL_MAJOR_VERSION_3,\n                               MBEDTLS_SSL_MINOR_VERSION_3);\n#endif\n\n  return static_cast<ctx_t>(ctx);\n}\n\ninline ctx_t create_server_context() {\n  auto ctx = new (std::nothrow) impl::MbedTlsContext();\n  if (!ctx) { return nullptr; }\n\n  ctx->is_server = true;\n\n  // Seed the random number generator\n  const char *pers = \"httplib_server\";\n  int ret = mbedtls_ctr_drbg_seed(\n      &ctx->ctr_drbg, mbedtls_entropy_func, &ctx->entropy,\n      reinterpret_cast<const unsigned char *>(pers), strlen(pers));\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    delete ctx;\n    return nullptr;\n  }\n\n  // Set up SSL config for server\n  ret = mbedtls_ssl_config_defaults(&ctx->conf, MBEDTLS_SSL_IS_SERVER,\n                                    MBEDTLS_SSL_TRANSPORT_STREAM,\n                                    MBEDTLS_SSL_PRESET_DEFAULT);\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    delete ctx;\n    return nullptr;\n  }\n\n  // Set random number generator\n  mbedtls_ssl_conf_rng(&ctx->conf, mbedtls_ctr_drbg_random, &ctx->ctr_drbg);\n\n  // Default: don't verify client\n  mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_NONE);\n\n  // Set minimum TLS version to 1.2\n#ifdef CPPHTTPLIB_MBEDTLS_V3\n  mbedtls_ssl_conf_min_tls_version(&ctx->conf, MBEDTLS_SSL_VERSION_TLS1_2);\n#else\n  mbedtls_ssl_conf_min_version(&ctx->conf, MBEDTLS_SSL_MAJOR_VERSION_3,\n                               MBEDTLS_SSL_MINOR_VERSION_3);\n#endif\n\n  // Set SNI callback to capture client's SNI hostname\n  mbedtls_ssl_conf_sni(&ctx->conf, impl::mbedtls_sni_callback, nullptr);\n\n  return static_cast<ctx_t>(ctx);\n}\n\ninline void free_context(ctx_t ctx) {\n  if (ctx) { delete static_cast<impl::MbedTlsContext *>(ctx); }\n}\n\ninline bool set_min_version(ctx_t ctx, Version version) {\n  if (!ctx) { return false; }\n  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);\n\n#ifdef CPPHTTPLIB_MBEDTLS_V3\n  // Mbed TLS 3.x uses mbedtls_ssl_protocol_version enum\n  mbedtls_ssl_protocol_version min_ver = MBEDTLS_SSL_VERSION_TLS1_2;\n  if (version >= Version::TLS1_3) {\n#if defined(MBEDTLS_SSL_PROTO_TLS1_3)\n    min_ver = MBEDTLS_SSL_VERSION_TLS1_3;\n#endif\n  }\n  mbedtls_ssl_conf_min_tls_version(&mctx->conf, min_ver);\n#else\n  // Mbed TLS 2.x uses major/minor version numbers\n  int major = MBEDTLS_SSL_MAJOR_VERSION_3;\n  int minor = MBEDTLS_SSL_MINOR_VERSION_3; // TLS 1.2\n  if (version >= Version::TLS1_3) {\n#if defined(MBEDTLS_SSL_PROTO_TLS1_3)\n    minor = MBEDTLS_SSL_MINOR_VERSION_4; // TLS 1.3\n#else\n    minor = MBEDTLS_SSL_MINOR_VERSION_3; // Fall back to TLS 1.2\n#endif\n  }\n  mbedtls_ssl_conf_min_version(&mctx->conf, major, minor);\n#endif\n  return true;\n}\n\ninline bool load_ca_pem(ctx_t ctx, const char *pem, size_t len) {\n  if (!ctx || !pem) { return false; }\n  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);\n\n  // mbedtls_x509_crt_parse expects null-terminated string for PEM\n  // Add null terminator if not present\n  std::string pem_str(pem, len);\n  int ret = mbedtls_x509_crt_parse(\n      &mctx->ca_chain, reinterpret_cast<const unsigned char *>(pem_str.c_str()),\n      pem_str.size() + 1);\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr);\n  return true;\n}\n\ninline bool load_ca_file(ctx_t ctx, const char *file_path) {\n  if (!ctx || !file_path) { return false; }\n  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);\n\n  int ret = mbedtls_x509_crt_parse_file(&mctx->ca_chain, file_path);\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr);\n  return true;\n}\n\ninline bool load_ca_dir(ctx_t ctx, const char *dir_path) {\n  if (!ctx || !dir_path) { return false; }\n  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);\n\n  int ret = mbedtls_x509_crt_parse_path(&mctx->ca_chain, dir_path);\n  if (ret < 0) { // Returns number of certs on success, negative on error\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr);\n  return true;\n}\n\ninline bool load_system_certs(ctx_t ctx) {\n  if (!ctx) { return false; }\n  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);\n  bool loaded = false;\n\n#ifdef _WIN32\n  // Load from Windows certificate store (ROOT and CA)\n  static const wchar_t *store_names[] = {L\"ROOT\", L\"CA\"};\n  for (auto store_name : store_names) {\n    HCERTSTORE hStore = CertOpenSystemStoreW(0, store_name);\n    if (hStore) {\n      PCCERT_CONTEXT pContext = nullptr;\n      while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) !=\n             nullptr) {\n        int ret = mbedtls_x509_crt_parse_der(\n            &mctx->ca_chain, pContext->pbCertEncoded, pContext->cbCertEncoded);\n        if (ret == 0) { loaded = true; }\n      }\n      CertCloseStore(hStore, 0);\n    }\n  }\n#elif defined(__APPLE__) && defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)\n  // Load from macOS Keychain\n  CFArrayRef certs = nullptr;\n  OSStatus status = SecTrustCopyAnchorCertificates(&certs);\n  if (status == errSecSuccess && certs) {\n    CFIndex count = CFArrayGetCount(certs);\n    for (CFIndex i = 0; i < count; i++) {\n      SecCertificateRef cert =\n          (SecCertificateRef)CFArrayGetValueAtIndex(certs, i);\n      CFDataRef data = SecCertificateCopyData(cert);\n      if (data) {\n        int ret = mbedtls_x509_crt_parse_der(\n            &mctx->ca_chain, CFDataGetBytePtr(data),\n            static_cast<size_t>(CFDataGetLength(data)));\n        if (ret == 0) { loaded = true; }\n        CFRelease(data);\n      }\n    }\n    CFRelease(certs);\n  }\n#else\n  // Try common CA certificate locations on Linux/Unix\n  static const char *ca_paths[] = {\n      \"/etc/ssl/certs/ca-certificates.crt\", // Debian/Ubuntu\n      \"/etc/pki/tls/certs/ca-bundle.crt\",   // RHEL/CentOS\n      \"/etc/ssl/ca-bundle.pem\",             // OpenSUSE\n      \"/etc/pki/tls/cacert.pem\",            // OpenELEC\n      \"/etc/ssl/cert.pem\",                  // Alpine, FreeBSD\n      nullptr};\n\n  for (const char **path = ca_paths; *path; ++path) {\n    int ret = mbedtls_x509_crt_parse_file(&mctx->ca_chain, *path);\n    if (ret >= 0) {\n      loaded = true;\n      break;\n    }\n  }\n\n  // Also try the CA directory\n  if (!loaded) {\n    static const char *ca_dirs[] = {\"/etc/ssl/certs\",     // Debian/Ubuntu\n                                    \"/etc/pki/tls/certs\", // RHEL/CentOS\n                                    \"/usr/share/ca-certificates\", nullptr};\n\n    for (const char **dir = ca_dirs; *dir; ++dir) {\n      int ret = mbedtls_x509_crt_parse_path(&mctx->ca_chain, *dir);\n      if (ret >= 0) {\n        loaded = true;\n        break;\n      }\n    }\n  }\n#endif\n\n  if (loaded) {\n    mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr);\n  }\n  return loaded;\n}\n\ninline bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key,\n                                const char *password) {\n  if (!ctx || !cert || !key) { return false; }\n  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);\n\n  // Parse certificate\n  std::string cert_str(cert);\n  int ret = mbedtls_x509_crt_parse(\n      &mctx->own_cert,\n      reinterpret_cast<const unsigned char *>(cert_str.c_str()),\n      cert_str.size() + 1);\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  // Parse private key\n  std::string key_str(key);\n  const unsigned char *pwd =\n      password ? reinterpret_cast<const unsigned char *>(password) : nullptr;\n  size_t pwd_len = password ? strlen(password) : 0;\n\n#ifdef CPPHTTPLIB_MBEDTLS_V3\n  ret = mbedtls_pk_parse_key(\n      &mctx->own_key, reinterpret_cast<const unsigned char *>(key_str.c_str()),\n      key_str.size() + 1, pwd, pwd_len, mbedtls_ctr_drbg_random,\n      &mctx->ctr_drbg);\n#else\n  ret = mbedtls_pk_parse_key(\n      &mctx->own_key, reinterpret_cast<const unsigned char *>(key_str.c_str()),\n      key_str.size() + 1, pwd, pwd_len);\n#endif\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  ret = mbedtls_ssl_conf_own_cert(&mctx->conf, &mctx->own_cert, &mctx->own_key);\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  return true;\n}\n\ninline bool set_client_cert_file(ctx_t ctx, const char *cert_path,\n                                 const char *key_path, const char *password) {\n  if (!ctx || !cert_path || !key_path) { return false; }\n  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);\n\n  // Parse certificate file\n  int ret = mbedtls_x509_crt_parse_file(&mctx->own_cert, cert_path);\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  // Parse private key file\n#ifdef CPPHTTPLIB_MBEDTLS_V3\n  ret = mbedtls_pk_parse_keyfile(&mctx->own_key, key_path, password,\n                                 mbedtls_ctr_drbg_random, &mctx->ctr_drbg);\n#else\n  ret = mbedtls_pk_parse_keyfile(&mctx->own_key, key_path, password);\n#endif\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  ret = mbedtls_ssl_conf_own_cert(&mctx->conf, &mctx->own_cert, &mctx->own_key);\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  return true;\n}\n\ninline void set_verify_client(ctx_t ctx, bool require) {\n  if (!ctx) { return; }\n  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);\n  mctx->verify_client = require;\n  if (require) {\n    mbedtls_ssl_conf_authmode(&mctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED);\n  } else {\n    // If a verify callback is set, use OPTIONAL mode to ensure the callback\n    // is called (matching OpenSSL behavior). Otherwise use NONE.\n    mbedtls_ssl_conf_authmode(&mctx->conf, mctx->has_verify_callback\n                                               ? MBEDTLS_SSL_VERIFY_OPTIONAL\n                                               : MBEDTLS_SSL_VERIFY_NONE);\n  }\n}\n\ninline session_t create_session(ctx_t ctx, socket_t sock) {\n  if (!ctx || sock == INVALID_SOCKET) { return nullptr; }\n  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);\n\n  auto session = new (std::nothrow) impl::MbedTlsSession();\n  if (!session) { return nullptr; }\n\n  session->sock = sock;\n\n  int ret = mbedtls_ssl_setup(&session->ssl, &mctx->conf);\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    delete session;\n    return nullptr;\n  }\n\n  // Set BIO callbacks\n  mbedtls_ssl_set_bio(&session->ssl, &session->sock, impl::mbedtls_net_send_cb,\n                      impl::mbedtls_net_recv_cb, nullptr);\n\n  // Set per-session verify callback with session pointer if callback is\n  // registered\n  if (mctx->has_verify_callback) {\n    mbedtls_ssl_set_verify(&session->ssl, impl::mbedtls_verify_callback,\n                           session);\n  }\n\n  return static_cast<session_t>(session);\n}\n\ninline void free_session(session_t session) {\n  if (session) { delete static_cast<impl::MbedTlsSession *>(session); }\n}\n\ninline bool set_sni(session_t session, const char *hostname) {\n  if (!session || !hostname) { return false; }\n  auto msession = static_cast<impl::MbedTlsSession *>(session);\n\n  int ret = mbedtls_ssl_set_hostname(&msession->ssl, hostname);\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  msession->hostname = hostname;\n  return true;\n}\n\ninline bool set_hostname(session_t session, const char *hostname) {\n  // In Mbed TLS, set_hostname also sets up hostname verification\n  return set_sni(session, hostname);\n}\n\ninline TlsError connect(session_t session) {\n  TlsError err;\n  if (!session) {\n    err.code = ErrorCode::Fatal;\n    return err;\n  }\n\n  auto msession = static_cast<impl::MbedTlsSession *>(session);\n  int ret = mbedtls_ssl_handshake(&msession->ssl);\n\n  if (ret == 0) {\n    err.code = ErrorCode::Success;\n  } else {\n    err.code = impl::map_mbedtls_error(ret, err.sys_errno);\n    err.backend_code = static_cast<uint64_t>(-ret);\n    impl::mbedtls_last_error() = ret;\n  }\n\n  return err;\n}\n\ninline TlsError accept(session_t session) {\n  // Same as connect for Mbed TLS - handshake works for both client and server\n  auto result = connect(session);\n\n  // After successful handshake, capture SNI from thread-local storage\n  if (result.code == ErrorCode::Success && session) {\n    auto msession = static_cast<impl::MbedTlsSession *>(session);\n    msession->sni_hostname = std::move(impl::mbedpending_sni());\n    impl::mbedpending_sni().clear();\n  }\n\n  return result;\n}\n\ninline bool connect_nonblocking(session_t session, socket_t sock,\n                                time_t timeout_sec, time_t timeout_usec,\n                                TlsError *err) {\n  if (!session) {\n    if (err) { err->code = ErrorCode::Fatal; }\n    return false;\n  }\n\n  auto msession = static_cast<impl::MbedTlsSession *>(session);\n\n  // Set socket to non-blocking mode\n  detail::set_nonblocking(sock, true);\n  auto cleanup =\n      detail::scope_exit([&]() { detail::set_nonblocking(sock, false); });\n\n  int ret;\n  while ((ret = mbedtls_ssl_handshake(&msession->ssl)) != 0) {\n    if (ret == MBEDTLS_ERR_SSL_WANT_READ) {\n      if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) {\n        continue;\n      }\n    } else if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) {\n      if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) {\n        continue;\n      }\n    }\n\n    // TlsError or timeout\n    if (err) {\n      err->code = impl::map_mbedtls_error(ret, err->sys_errno);\n      err->backend_code = static_cast<uint64_t>(-ret);\n    }\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  if (err) { err->code = ErrorCode::Success; }\n  return true;\n}\n\ninline bool accept_nonblocking(session_t session, socket_t sock,\n                               time_t timeout_sec, time_t timeout_usec,\n                               TlsError *err) {\n  // Same implementation as connect for Mbed TLS\n  bool result =\n      connect_nonblocking(session, sock, timeout_sec, timeout_usec, err);\n\n  // After successful handshake, capture SNI from thread-local storage\n  if (result && session) {\n    auto msession = static_cast<impl::MbedTlsSession *>(session);\n    msession->sni_hostname = std::move(impl::mbedpending_sni());\n    impl::mbedpending_sni().clear();\n  }\n\n  return result;\n}\n\ninline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {\n  if (!session || !buf) {\n    err.code = ErrorCode::Fatal;\n    return -1;\n  }\n\n  auto msession = static_cast<impl::MbedTlsSession *>(session);\n  int ret =\n      mbedtls_ssl_read(&msession->ssl, static_cast<unsigned char *>(buf), len);\n\n  if (ret > 0) {\n    err.code = ErrorCode::Success;\n    return static_cast<ssize_t>(ret);\n  }\n\n  if (ret == 0) {\n    err.code = ErrorCode::PeerClosed;\n    return 0;\n  }\n\n  err.code = impl::map_mbedtls_error(ret, err.sys_errno);\n  err.backend_code = static_cast<uint64_t>(-ret);\n  impl::mbedtls_last_error() = ret;\n  return -1;\n}\n\ninline ssize_t write(session_t session, const void *buf, size_t len,\n                     TlsError &err) {\n  if (!session || !buf) {\n    err.code = ErrorCode::Fatal;\n    return -1;\n  }\n\n  auto msession = static_cast<impl::MbedTlsSession *>(session);\n  int ret = mbedtls_ssl_write(&msession->ssl,\n                              static_cast<const unsigned char *>(buf), len);\n\n  if (ret > 0) {\n    err.code = ErrorCode::Success;\n    return static_cast<ssize_t>(ret);\n  }\n\n  if (ret == 0) {\n    err.code = ErrorCode::PeerClosed;\n    return 0;\n  }\n\n  err.code = impl::map_mbedtls_error(ret, err.sys_errno);\n  err.backend_code = static_cast<uint64_t>(-ret);\n  impl::mbedtls_last_error() = ret;\n  return -1;\n}\n\ninline int pending(const_session_t session) {\n  if (!session) { return 0; }\n  auto msession =\n      static_cast<impl::MbedTlsSession *>(const_cast<void *>(session));\n  return static_cast<int>(mbedtls_ssl_get_bytes_avail(&msession->ssl));\n}\n\ninline void shutdown(session_t session, bool graceful) {\n  if (!session) { return; }\n  auto msession = static_cast<impl::MbedTlsSession *>(session);\n\n  if (graceful) {\n    // Try to send close_notify, but don't block forever\n    int ret;\n    int attempts = 0;\n    while ((ret = mbedtls_ssl_close_notify(&msession->ssl)) != 0 &&\n           attempts < 3) {\n      if (ret != MBEDTLS_ERR_SSL_WANT_READ &&\n          ret != MBEDTLS_ERR_SSL_WANT_WRITE) {\n        break;\n      }\n      attempts++;\n    }\n  }\n}\n\ninline bool is_peer_closed(session_t session, socket_t sock) {\n  if (!session || sock == INVALID_SOCKET) { return true; }\n  auto msession = static_cast<impl::MbedTlsSession *>(session);\n\n  // Check if there's already decrypted data available in the TLS buffer\n  // If so, the connection is definitely alive\n  if (mbedtls_ssl_get_bytes_avail(&msession->ssl) > 0) { return false; }\n\n  // Set socket to non-blocking to avoid blocking on read\n  detail::set_nonblocking(sock, true);\n  auto cleanup =\n      detail::scope_exit([&]() { detail::set_nonblocking(sock, false); });\n\n  // Try a 1-byte read to check connection status\n  // Note: This will consume the byte if data is available, but for the\n  // purpose of checking if peer is closed, this should be acceptable\n  // since we're only called when we expect the connection might be closing\n  unsigned char buf;\n  int ret = mbedtls_ssl_read(&msession->ssl, &buf, 1);\n\n  // If we got data or WANT_READ (would block), connection is alive\n  if (ret > 0 || ret == MBEDTLS_ERR_SSL_WANT_READ) { return false; }\n\n  // If we get a peer close notify or a connection reset, the peer is closed\n  return ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY ||\n         ret == MBEDTLS_ERR_NET_CONN_RESET || ret == 0;\n}\n\ninline cert_t get_peer_cert(const_session_t session) {\n  if (!session) { return nullptr; }\n  auto msession =\n      static_cast<impl::MbedTlsSession *>(const_cast<void *>(session));\n\n  // Mbed TLS returns a pointer to the internal peer cert chain.\n  // WARNING: This pointer is only valid while the session is active.\n  // Do not use the certificate after calling free_session().\n  const mbedtls_x509_crt *cert = mbedtls_ssl_get_peer_cert(&msession->ssl);\n  return const_cast<mbedtls_x509_crt *>(cert);\n}\n\ninline void free_cert(cert_t cert) {\n  // Mbed TLS: peer certificate is owned by the SSL context.\n  // No-op here, but callers should still call this for cross-backend\n  // portability.\n  (void)cert;\n}\n\ninline bool verify_hostname(cert_t cert, const char *hostname) {\n  if (!cert || !hostname) { return false; }\n  auto mcert = static_cast<const mbedtls_x509_crt *>(cert);\n  std::string host_str(hostname);\n\n  // Check if hostname is an IP address\n  bool is_ip = impl::is_ipv4_address(host_str);\n  unsigned char ip_bytes[4];\n  if (is_ip) { impl::parse_ipv4(host_str, ip_bytes); }\n\n  // Check Subject Alternative Names (SAN)\n  // In Mbed TLS 3.x, subject_alt_names contains raw values without ASN.1 tags\n  // - DNS names: raw string bytes\n  // - IP addresses: raw IP bytes (4 for IPv4, 16 for IPv6)\n  const mbedtls_x509_sequence *san = &mcert->subject_alt_names;\n  while (san != nullptr && san->buf.p != nullptr && san->buf.len > 0) {\n    const unsigned char *p = san->buf.p;\n    size_t len = san->buf.len;\n\n    if (is_ip) {\n      // Check if this SAN is an IPv4 address (4 bytes)\n      if (len == 4 && memcmp(p, ip_bytes, 4) == 0) { return true; }\n      // Check if this SAN is an IPv6 address (16 bytes) - skip for now\n    } else {\n      // Check if this SAN is a DNS name (printable ASCII string)\n      bool is_dns = len > 0;\n      for (size_t i = 0; i < len && is_dns; i++) {\n        if (p[i] < 32 || p[i] > 126) { is_dns = false; }\n      }\n      if (is_dns) {\n        std::string san_name(reinterpret_cast<const char *>(p), len);\n        if (detail::match_hostname(san_name, host_str)) { return true; }\n      }\n    }\n    san = san->next;\n  }\n\n  // Fallback: Check Common Name (CN) in subject\n  char cn[256];\n  int ret = mbedtls_x509_dn_gets(cn, sizeof(cn), &mcert->subject);\n  if (ret > 0) {\n    std::string cn_str(cn);\n\n    // Look for \"CN=\" in the DN string\n    size_t cn_pos = cn_str.find(\"CN=\");\n    if (cn_pos != std::string::npos) {\n      size_t start = cn_pos + 3;\n      size_t end = cn_str.find(',', start);\n      std::string cn_value =\n          cn_str.substr(start, end == std::string::npos ? end : end - start);\n\n      if (detail::match_hostname(cn_value, host_str)) { return true; }\n    }\n  }\n\n  return false;\n}\n\ninline uint64_t hostname_mismatch_code() {\n  return static_cast<uint64_t>(MBEDTLS_X509_BADCERT_CN_MISMATCH);\n}\n\ninline long get_verify_result(const_session_t session) {\n  if (!session) { return -1; }\n  auto msession =\n      static_cast<impl::MbedTlsSession *>(const_cast<void *>(session));\n  uint32_t flags = mbedtls_ssl_get_verify_result(&msession->ssl);\n  // Return 0 (X509_V_OK equivalent) if verification passed\n  return flags == 0 ? 0 : static_cast<long>(flags);\n}\n\ninline std::string get_cert_subject_cn(cert_t cert) {\n  if (!cert) return \"\";\n  auto x509 = static_cast<mbedtls_x509_crt *>(cert);\n\n  // Find the CN in the subject\n  const mbedtls_x509_name *name = &x509->subject;\n  while (name != nullptr) {\n    if (MBEDTLS_OID_CMP(MBEDTLS_OID_AT_CN, &name->oid) == 0) {\n      return std::string(reinterpret_cast<const char *>(name->val.p),\n                         name->val.len);\n    }\n    name = name->next;\n  }\n  return \"\";\n}\n\ninline std::string get_cert_issuer_name(cert_t cert) {\n  if (!cert) return \"\";\n  auto x509 = static_cast<mbedtls_x509_crt *>(cert);\n\n  // Build a human-readable issuer name string\n  char buf[512];\n  int ret = mbedtls_x509_dn_gets(buf, sizeof(buf), &x509->issuer);\n  if (ret < 0) return \"\";\n  return std::string(buf);\n}\n\ninline bool get_cert_sans(cert_t cert, std::vector<SanEntry> &sans) {\n  sans.clear();\n  if (!cert) return false;\n  auto x509 = static_cast<mbedtls_x509_crt *>(cert);\n\n  // Parse the Subject Alternative Name extension\n  const mbedtls_x509_sequence *cur = &x509->subject_alt_names;\n  while (cur != nullptr) {\n    if (cur->buf.len > 0) {\n      // Mbed TLS stores SAN as ASN.1 sequences\n      // The tag byte indicates the type\n      const unsigned char *p = cur->buf.p;\n      size_t len = cur->buf.len;\n\n      // First byte is the tag\n      unsigned char tag = *p;\n      p++;\n      len--;\n\n      // Parse length (simple single-byte length assumed)\n      if (len > 0 && *p < 0x80) {\n        size_t value_len = *p;\n        p++;\n        len--;\n\n        if (value_len <= len) {\n          SanEntry entry;\n          // ASN.1 context tags for GeneralName\n          switch (tag & 0x1F) {\n          case 2: // dNSName\n            entry.type = SanType::DNS;\n            entry.value =\n                std::string(reinterpret_cast<const char *>(p), value_len);\n            break;\n          case 7: // iPAddress\n            entry.type = SanType::IP;\n            if (value_len == 4) {\n              // IPv4\n              char buf[16];\n              snprintf(buf, sizeof(buf), \"%d.%d.%d.%d\", p[0], p[1], p[2], p[3]);\n              entry.value = buf;\n            } else if (value_len == 16) {\n              // IPv6\n              char buf[64];\n              snprintf(buf, sizeof(buf),\n                       \"%02x%02x:%02x%02x:%02x%02x:%02x%02x:\"\n                       \"%02x%02x:%02x%02x:%02x%02x:%02x%02x\",\n                       p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8],\n                       p[9], p[10], p[11], p[12], p[13], p[14], p[15]);\n              entry.value = buf;\n            }\n            break;\n          case 1: // rfc822Name (email)\n            entry.type = SanType::EMAIL;\n            entry.value =\n                std::string(reinterpret_cast<const char *>(p), value_len);\n            break;\n          case 6: // uniformResourceIdentifier\n            entry.type = SanType::URI;\n            entry.value =\n                std::string(reinterpret_cast<const char *>(p), value_len);\n            break;\n          default: entry.type = SanType::OTHER; break;\n          }\n\n          if (!entry.value.empty()) { sans.push_back(std::move(entry)); }\n        }\n      }\n    }\n    cur = cur->next;\n  }\n  return true;\n}\n\ninline bool get_cert_validity(cert_t cert, time_t &not_before,\n                              time_t &not_after) {\n  if (!cert) return false;\n  auto x509 = static_cast<mbedtls_x509_crt *>(cert);\n\n  // Convert mbedtls_x509_time to time_t\n  auto to_time_t = [](const mbedtls_x509_time &t) -> time_t {\n    struct tm tm_time = {};\n    tm_time.tm_year = t.year - 1900;\n    tm_time.tm_mon = t.mon - 1;\n    tm_time.tm_mday = t.day;\n    tm_time.tm_hour = t.hour;\n    tm_time.tm_min = t.min;\n    tm_time.tm_sec = t.sec;\n#ifdef _WIN32\n    return _mkgmtime(&tm_time);\n#else\n    return timegm(&tm_time);\n#endif\n  };\n\n  not_before = to_time_t(x509->valid_from);\n  not_after = to_time_t(x509->valid_to);\n  return true;\n}\n\ninline std::string get_cert_serial(cert_t cert) {\n  if (!cert) return \"\";\n  auto x509 = static_cast<mbedtls_x509_crt *>(cert);\n\n  // Convert serial number to hex string\n  std::string result;\n  result.reserve(x509->serial.len * 2);\n  for (size_t i = 0; i < x509->serial.len; i++) {\n    char hex[3];\n    snprintf(hex, sizeof(hex), \"%02X\", x509->serial.p[i]);\n    result += hex;\n  }\n  return result;\n}\n\ninline bool get_cert_der(cert_t cert, std::vector<unsigned char> &der) {\n  if (!cert) return false;\n  auto crt = static_cast<mbedtls_x509_crt *>(cert);\n  if (!crt->raw.p || crt->raw.len == 0) return false;\n  der.assign(crt->raw.p, crt->raw.p + crt->raw.len);\n  return true;\n}\n\ninline const char *get_sni(const_session_t session) {\n  if (!session) return nullptr;\n  auto msession = static_cast<const impl::MbedTlsSession *>(session);\n\n  // For server: return SNI received from client during handshake\n  if (!msession->sni_hostname.empty()) {\n    return msession->sni_hostname.c_str();\n  }\n\n  // For client: return the hostname set via set_sni\n  if (!msession->hostname.empty()) { return msession->hostname.c_str(); }\n\n  return nullptr;\n}\n\ninline uint64_t peek_error() {\n  // Mbed TLS doesn't have an error queue, return the last error\n  return static_cast<uint64_t>(-impl::mbedtls_last_error());\n}\n\ninline uint64_t get_error() {\n  // Mbed TLS doesn't have an error queue, return and clear the last error\n  uint64_t err = static_cast<uint64_t>(-impl::mbedtls_last_error());\n  impl::mbedtls_last_error() = 0;\n  return err;\n}\n\ninline std::string error_string(uint64_t code) {\n  char buf[256];\n  mbedtls_strerror(-static_cast<int>(code), buf, sizeof(buf));\n  return std::string(buf);\n}\n\ninline ca_store_t create_ca_store(const char *pem, size_t len) {\n  auto *ca_chain = new (std::nothrow) mbedtls_x509_crt;\n  if (!ca_chain) { return nullptr; }\n\n  mbedtls_x509_crt_init(ca_chain);\n\n  // mbedtls_x509_crt_parse expects null-terminated PEM\n  int ret = mbedtls_x509_crt_parse(ca_chain,\n                                   reinterpret_cast<const unsigned char *>(pem),\n                                   len + 1); // +1 for null terminator\n  if (ret != 0) {\n    // Try without +1 in case PEM is already null-terminated\n    ret = mbedtls_x509_crt_parse(\n        ca_chain, reinterpret_cast<const unsigned char *>(pem), len);\n    if (ret != 0) {\n      mbedtls_x509_crt_free(ca_chain);\n      delete ca_chain;\n      return nullptr;\n    }\n  }\n\n  return static_cast<ca_store_t>(ca_chain);\n}\n\ninline void free_ca_store(ca_store_t store) {\n  if (store) {\n    auto *ca_chain = static_cast<mbedtls_x509_crt *>(store);\n    mbedtls_x509_crt_free(ca_chain);\n    delete ca_chain;\n  }\n}\n\ninline bool set_ca_store(ctx_t ctx, ca_store_t store) {\n  if (!ctx || !store) { return false; }\n  auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);\n  auto *ca_chain = static_cast<mbedtls_x509_crt *>(store);\n\n  // Free existing CA chain\n  mbedtls_x509_crt_free(&mbed_ctx->ca_chain);\n  mbedtls_x509_crt_init(&mbed_ctx->ca_chain);\n\n  // Copy the CA chain (deep copy)\n  // Parse from the raw data of the source cert\n  mbedtls_x509_crt *src = ca_chain;\n  while (src != nullptr) {\n    int ret = mbedtls_x509_crt_parse_der(&mbed_ctx->ca_chain, src->raw.p,\n                                         src->raw.len);\n    if (ret != 0) { return false; }\n    src = src->next;\n  }\n\n  // Update the SSL config to use the new CA chain\n  mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->ca_chain, nullptr);\n  return true;\n}\n\ninline size_t get_ca_certs(ctx_t ctx, std::vector<cert_t> &certs) {\n  certs.clear();\n  if (!ctx) { return 0; }\n  auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);\n\n  // Iterate through the CA chain\n  mbedtls_x509_crt *cert = &mbed_ctx->ca_chain;\n  while (cert != nullptr && cert->raw.len > 0) {\n    // Create a copy of the certificate for the caller\n    auto *copy = new mbedtls_x509_crt;\n    mbedtls_x509_crt_init(copy);\n    int ret = mbedtls_x509_crt_parse_der(copy, cert->raw.p, cert->raw.len);\n    if (ret == 0) {\n      certs.push_back(static_cast<cert_t>(copy));\n    } else {\n      mbedtls_x509_crt_free(copy);\n      delete copy;\n    }\n    cert = cert->next;\n  }\n  return certs.size();\n}\n\ninline std::vector<std::string> get_ca_names(ctx_t ctx) {\n  std::vector<std::string> names;\n  if (!ctx) { return names; }\n  auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);\n\n  // Iterate through the CA chain\n  mbedtls_x509_crt *cert = &mbed_ctx->ca_chain;\n  while (cert != nullptr && cert->raw.len > 0) {\n    char buf[512];\n    int ret = mbedtls_x509_dn_gets(buf, sizeof(buf), &cert->subject);\n    if (ret > 0) { names.push_back(buf); }\n    cert = cert->next;\n  }\n  return names;\n}\n\ninline bool update_server_cert(ctx_t ctx, const char *cert_pem,\n                               const char *key_pem, const char *password) {\n  if (!ctx || !cert_pem || !key_pem) { return false; }\n  auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);\n\n  // Free existing certificate and key\n  mbedtls_x509_crt_free(&mbed_ctx->own_cert);\n  mbedtls_pk_free(&mbed_ctx->own_key);\n  mbedtls_x509_crt_init(&mbed_ctx->own_cert);\n  mbedtls_pk_init(&mbed_ctx->own_key);\n\n  // Parse certificate PEM\n  int ret = mbedtls_x509_crt_parse(\n      &mbed_ctx->own_cert, reinterpret_cast<const unsigned char *>(cert_pem),\n      strlen(cert_pem) + 1);\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  // Parse private key PEM\n#ifdef CPPHTTPLIB_MBEDTLS_V3\n  ret = mbedtls_pk_parse_key(\n      &mbed_ctx->own_key, reinterpret_cast<const unsigned char *>(key_pem),\n      strlen(key_pem) + 1,\n      password ? reinterpret_cast<const unsigned char *>(password) : nullptr,\n      password ? strlen(password) : 0, mbedtls_ctr_drbg_random,\n      &mbed_ctx->ctr_drbg);\n#else\n  ret = mbedtls_pk_parse_key(\n      &mbed_ctx->own_key, reinterpret_cast<const unsigned char *>(key_pem),\n      strlen(key_pem) + 1,\n      password ? reinterpret_cast<const unsigned char *>(password) : nullptr,\n      password ? strlen(password) : 0);\n#endif\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  // Configure SSL to use the new certificate and key\n  ret = mbedtls_ssl_conf_own_cert(&mbed_ctx->conf, &mbed_ctx->own_cert,\n                                  &mbed_ctx->own_key);\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  return true;\n}\n\ninline bool update_server_client_ca(ctx_t ctx, const char *ca_pem) {\n  if (!ctx || !ca_pem) { return false; }\n  auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);\n\n  // Free existing CA chain\n  mbedtls_x509_crt_free(&mbed_ctx->ca_chain);\n  mbedtls_x509_crt_init(&mbed_ctx->ca_chain);\n\n  // Parse CA PEM\n  int ret = mbedtls_x509_crt_parse(\n      &mbed_ctx->ca_chain, reinterpret_cast<const unsigned char *>(ca_pem),\n      strlen(ca_pem) + 1);\n  if (ret != 0) {\n    impl::mbedtls_last_error() = ret;\n    return false;\n  }\n\n  // Update SSL config to use new CA chain\n  mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->ca_chain, nullptr);\n  return true;\n}\n\ninline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) {\n  if (!ctx) { return false; }\n  auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);\n\n  impl::get_verify_callback() = std::move(callback);\n  mbed_ctx->has_verify_callback =\n      static_cast<bool>(impl::get_verify_callback());\n\n  if (mbed_ctx->has_verify_callback) {\n    // Set OPTIONAL mode to ensure callback is called even when verification\n    // is disabled (matching OpenSSL behavior where SSL_VERIFY_PEER is set)\n    mbedtls_ssl_conf_authmode(&mbed_ctx->conf, MBEDTLS_SSL_VERIFY_OPTIONAL);\n    mbedtls_ssl_conf_verify(&mbed_ctx->conf, impl::mbedtls_verify_callback,\n                            nullptr);\n  } else {\n    mbedtls_ssl_conf_verify(&mbed_ctx->conf, nullptr, nullptr);\n  }\n  return true;\n}\n\ninline long get_verify_error(const_session_t session) {\n  if (!session) { return -1; }\n  auto *msession =\n      static_cast<impl::MbedTlsSession *>(const_cast<void *>(session));\n  return static_cast<long>(mbedtls_ssl_get_verify_result(&msession->ssl));\n}\n\ninline std::string verify_error_string(long error_code) {\n  if (error_code == 0) { return \"\"; }\n  char buf[256];\n  mbedtls_x509_crt_verify_info(buf, sizeof(buf), \"\",\n                               static_cast<uint32_t>(error_code));\n  // Remove trailing newline if present\n  std::string result(buf);\n  while (!result.empty() && (result.back() == '\\n' || result.back() == ' ')) {\n    result.pop_back();\n  }\n  return result;\n}\n\n} // namespace tls\n\n#endif // CPPHTTPLIB_MBEDTLS_SUPPORT\n\n// ----------------------------------------------------------------------------\n\n} // namespace httplib\n\n#endif // CPPHTTPLIB_HTTPLIB_H\n"
  },
  {
    "path": "src/libs/CMakeLists.txt",
    "content": "add_subdirectory(browser)\nadd_subdirectory(core)\nadd_subdirectory(registry)\nadd_subdirectory(sidebar)\nadd_subdirectory(ui)\nadd_subdirectory(util)\n"
  },
  {
    "path": "src/libs/browser/CMakeLists.txt",
    "content": "add_library(Browser STATIC\n    searchtoolbar.cpp\n    settings.cpp\n    urlrequestinterceptor.cpp\n    webbridge.cpp\n    webcontrol.cpp\n    webpage.cpp\n    webview.cpp\n)\n\nfind_package(Qt${QT_VERSION_MAJOR} COMPONENTS WebChannel WebEngineWidgets REQUIRED)\ntarget_link_libraries(Browser PRIVATE Qt${QT_VERSION_MAJOR}::WebChannel Qt${QT_VERSION_MAJOR}::WebEngineWidgets)\n"
  },
  {
    "path": "src/libs/browser/searchtoolbar.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"searchtoolbar.h\"\n\n#include <QAction>\n#include <QApplication>\n#include <QHBoxLayout>\n#include <QKeyEvent>\n#include <QLineEdit>\n#include <QStyle>\n#include <QToolButton>\n#include <QWebEnginePage>\n#include <QWebEngineView>\n\nusing namespace Zeal::Browser;\n\nSearchToolBar::SearchToolBar(QWebEngineView *webView, QWidget *parent)\n    : QWidget(parent)\n    , m_webView(webView)\n{\n    auto layout = new QHBoxLayout(this);\n    layout->setContentsMargins(4, 4, 4, 4);\n    layout->setSpacing(4);\n\n    m_lineEdit = new QLineEdit();\n    m_lineEdit->installEventFilter(this);\n    m_lineEdit->setMaximumWidth(200);\n    m_lineEdit->setPlaceholderText(tr(\"Find in page\"));\n    connect(m_lineEdit, &QLineEdit::textChanged, this, &SearchToolBar::findNext);\n    connect(m_lineEdit, &QLineEdit::textChanged, this, &SearchToolBar::updateHighlight);\n    layout->addWidget(m_lineEdit);\n\n    m_findPreviousButton = new QToolButton();\n    m_findPreviousButton->setAutoRaise(true);\n    m_findPreviousButton->setIcon(qApp->style()->standardIcon(QStyle::SP_ArrowBack));\n    m_findPreviousButton->setToolTip(tr(\"Previous result\"));\n    connect(m_findPreviousButton, &QToolButton::clicked, this, &SearchToolBar::findPrevious);\n    layout->addWidget(m_findPreviousButton);\n\n    // A workaround for QAbstractButton lacking support for multiple shortcuts.\n    auto action = new QAction(m_findPreviousButton);\n    action->setShortcuts(QKeySequence::FindPrevious);\n    connect(action, &QAction::triggered, this, [this]() { m_findPreviousButton->animateClick(); });\n    addAction(action);\n\n    m_findNextButton = new QToolButton();\n    m_findNextButton->setAutoRaise(true);\n    m_findNextButton->setIcon(qApp->style()->standardIcon(QStyle::SP_ArrowForward));\n    m_findNextButton->setToolTip(tr(\"Next result\"));\n    connect(m_findNextButton, &QToolButton::clicked, this, &SearchToolBar::findNext);\n    layout->addWidget(m_findNextButton);\n\n    action = new QAction(m_findNextButton);\n    action->setShortcuts(QKeySequence::FindNext);\n    connect(action, &QAction::triggered, this, [this]() { m_findNextButton->animateClick(); });\n    addAction(action);\n\n    m_highlightAllButton = new QToolButton();\n    m_highlightAllButton->setAutoRaise(true);\n    m_highlightAllButton->setCheckable(true);\n    m_highlightAllButton->setText(tr(\"High&light All\"));\n    connect(m_highlightAllButton, &QToolButton::toggled, this, &SearchToolBar::updateHighlight);\n    layout->addWidget(m_highlightAllButton);\n\n    m_matchCaseButton = new QToolButton();\n    m_matchCaseButton->setAutoRaise(true);\n    m_matchCaseButton->setCheckable(true);\n    m_matchCaseButton->setText(tr(\"Mat&ch Case\"));\n    connect(m_matchCaseButton, &QToolButton::toggled, this, &SearchToolBar::updateHighlight);\n    layout->addWidget(m_matchCaseButton);\n\n    layout->addStretch();\n\n    auto closeButton = new QToolButton();\n    closeButton->setAutoRaise(true);\n    closeButton->setIcon(qApp->style()->standardIcon(QStyle::SP_TitleBarCloseButton));\n    closeButton->setToolTip(tr(\"Close find bar\"));\n    connect(closeButton, &QToolButton::clicked, this, &QWidget::hide);\n    layout->addWidget(closeButton);\n\n    setLayout(layout);\n\n    setMaximumHeight(sizeHint().height());\n    setMinimumWidth(sizeHint().width());\n}\n\nvoid SearchToolBar::setText(const QString &text)\n{\n    m_lineEdit->setText(text);\n}\n\nvoid SearchToolBar::activate()\n{\n    show();\n\n    m_lineEdit->selectAll();\n    m_lineEdit->setFocus();\n}\n\nbool SearchToolBar::eventFilter(QObject *object, QEvent *event)\n{\n    if (object == m_lineEdit && event->type() == QEvent::KeyPress) {\n        auto keyEvent = static_cast<QKeyEvent *>(event);\n\n        switch (keyEvent->key()) {\n        case Qt::Key_Enter:\n        case Qt::Key_Return:\n            if (keyEvent->modifiers().testFlag(Qt::ControlModifier)) {\n                m_highlightAllButton->toggle();\n            } else if (keyEvent->modifiers().testFlag(Qt::ShiftModifier)) {\n                findPrevious();\n            } else {\n                findNext();\n            }\n            return true;\n        case Qt::Key_Down:\n        case Qt::Key_Up:\n        case Qt::Key_PageDown:\n        case Qt::Key_PageUp:\n            QCoreApplication::sendEvent(m_webView->focusProxy(), event);\n            return true;\n        default:\n            break;\n        }\n    }\n\n    return QWidget::eventFilter(object, event);\n}\n\nvoid SearchToolBar::hideEvent(QHideEvent *event)\n{\n    hideHighlight();\n    m_webView->setFocus();\n    QWidget::hideEvent(event);\n}\n\nvoid SearchToolBar::showEvent(QShowEvent *event)\n{\n    activate();\n\n    QWidget::showEvent(event);\n}\n\nvoid SearchToolBar::keyPressEvent(QKeyEvent *event)\n{\n    if (event->key() == Qt::Key_Escape) {\n        hide();\n    }\n}\n\nvoid SearchToolBar::findNext()\n{\n    if (!isVisible()) {\n        return;\n    }\n\n    QWebEnginePage::FindFlags ff;\n    ff.setFlag(QWebEnginePage::FindCaseSensitively, m_matchCaseButton->isChecked());\n    m_webView->findText(m_lineEdit->text(), ff);\n}\n\nvoid SearchToolBar::findPrevious()\n{\n    if (!isVisible()) {\n        return;\n    }\n\n    QWebEnginePage::FindFlags ff;\n    ff.setFlag(QWebEnginePage::FindCaseSensitively, m_matchCaseButton->isChecked());\n    ff.setFlag(QWebEnginePage::FindBackward);\n    m_webView->findText(m_lineEdit->text(), ff);\n}\n\nvoid SearchToolBar::hideHighlight()\n{\n    m_webView->findText(QString());\n}\n\nvoid SearchToolBar::updateHighlight()\n{\n    hideHighlight();\n\n    if (m_highlightAllButton->isChecked()) {\n        QWebEnginePage::FindFlags ff;\n        ff.setFlag(QWebEnginePage::FindCaseSensitively, m_matchCaseButton->isChecked());\n        m_webView->findText(m_lineEdit->text(), ff);\n    }\n}\n"
  },
  {
    "path": "src/libs/browser/searchtoolbar.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_BROWSER_SEARCHTOOLBAR_H\n#define ZEAL_BROWSER_SEARCHTOOLBAR_H\n\n#include <QWidget>\n\nclass QLineEdit;\nclass QToolButton;\nclass QWebEngineView;\n\nnamespace Zeal {\nnamespace Browser {\n\nclass SearchToolBar final : public QWidget\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(SearchToolBar)\npublic:\n    explicit SearchToolBar(QWebEngineView *webView, QWidget *parent = nullptr);\n\n    void setText(const QString &text);\n    void activate();\n\n    bool eventFilter(QObject *object, QEvent *event) override;\n\nprotected:\n    void hideEvent(QHideEvent *event) override;\n    void showEvent(QShowEvent *event) override;\n\n    void keyPressEvent(QKeyEvent *event) override;\n\nprivate:\n    void findNext();\n    void findPrevious();\n\n    void hideHighlight();\n    void updateHighlight();\n\n    QLineEdit *m_lineEdit = nullptr;\n    QToolButton *m_findNextButton = nullptr;\n    QToolButton *m_findPreviousButton = nullptr;\n    QToolButton *m_highlightAllButton = nullptr;\n    QToolButton *m_matchCaseButton = nullptr;\n\n    QWebEngineView *m_webView = nullptr;\n};\n\n} // namespace Browser\n} // namespace Zeal\n\n#endif // ZEAL_BROWSER_SEARCHTOOLBAR_H\n"
  },
  {
    "path": "src/libs/browser/settings.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"settings.h\"\n\n#include \"urlrequestinterceptor.h\"\n\n#include <core/settings.h>\n\n#include <QFileInfo>\n#include <QLoggingCategory>\n#include <QWebEngineProfile>\n#include <QWebEngineSettings>\n#include <QWebEngineScript>\n#include <QWebEngineScriptCollection>\n\nnamespace {\nconstexpr char HighlightOnNavigateCssPath[] = \":/browser/assets/css/highlight.css\";\n}\n\nusing namespace Zeal;\nusing namespace Zeal::Browser;\n\nstatic Q_LOGGING_CATEGORY(log, \"zeal.browser.settings\")\n\nQWebEngineProfile *Settings::m_webProfile = nullptr;\n\nSettings::Settings(Core::Settings *appSettings, QObject *parent)\n    : QObject(parent)\n    , m_appSettings(appSettings)\n{\n    Q_ASSERT(!m_webProfile);\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n    // Create a new off-the-record profile.\n    m_webProfile = new QWebEngineProfile(this);\n#else\n    // Default profile for Qt 6 is off-the-record.\n    m_webProfile = QWebEngineProfile::defaultProfile();\n#endif\n\n    // Setup URL interceptor.\n    m_webProfile->setUrlRequestInterceptor(new UrlRequestInterceptor(this));\n\n    // Listen to settings changes.\n    connect(m_appSettings, &Core::Settings::updated, this, &Settings::applySettings);\n    applySettings();\n}\n\nvoid Settings::applySettings()\n{\n    m_webProfile->settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled,\n                                           m_appSettings->isSmoothScrollingEnabled);\n\n    // Qt 6.7+ does not require restart to enable dark mode.\n#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)\n    m_webProfile->settings()->setAttribute(QWebEngineSettings::ForceDarkMode,\n                                           m_appSettings->isDarkModeEnabled());\n#endif\n\n    // Apply custom CSS.\n    // TODO: Apply to all open pages.\n    m_webProfile->scripts()->clear(); // Remove all scripts first.\n\n    if (m_appSettings->isHighlightOnNavigateEnabled) {\n        setCustomStyleSheet(QStringLiteral(\"_zeal_highlightstylesheet\"), HighlightOnNavigateCssPath);\n    }\n\n    if (QFileInfo::exists(m_appSettings->customCssFile)) {\n        setCustomStyleSheet(QStringLiteral(\"_zeal_userstylesheet\"), m_appSettings->customCssFile);\n    }\n}\n\nQWebEngineProfile *Settings::defaultProfile()\n{\n    Q_ASSERT(m_webProfile);\n    return m_webProfile;\n}\n\nvoid Settings::setCustomStyleSheet(const QString &name, const QString &path)\n{\n    // Read the stylesheet file content.\n    QFile file(path);\n    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {\n        qCWarning(log, \"Failed to read stylesheet file '%s'.\", qPrintable(path));\n        return;\n    }\n\n    QString stylesheet = QString::fromUtf8(file.readAll());\n    file.close();\n\n    // Escape single quotes and newlines for JavaScript string literal.\n    stylesheet.replace(QLatin1String(\"'\"), QLatin1String(\"\\\\'\"));\n    stylesheet.replace(QLatin1String(\"\\n\"), QLatin1String(\"\\\\n\"));\n\n    QString cssInjectCode = QLatin1String(\n        \"(() => {\"\n            \"const head = document.getElementsByTagName('head')[0];\"\n            \"if (!head) { console.error('Cannot set custom stylesheet.'); return; }\"\n            \"const stylesheet = document.createElement('style');\"\n            \"stylesheet.setAttribute('type', 'text/css');\"\n            \"stylesheet.textContent = '%1';\"\n            \"head.appendChild(stylesheet);\"\n        \"})()\"\n    );\n\n    QWebEngineScript script;\n    script.setName(name);\n    script.setSourceCode(cssInjectCode.arg(stylesheet));\n    script.setInjectionPoint(QWebEngineScript::DocumentReady);\n    script.setRunsOnSubFrames(true);\n    script.setWorldId(QWebEngineScript::ApplicationWorld);\n    m_webProfile->scripts()->insert(script);\n}\n"
  },
  {
    "path": "src/libs/browser/settings.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_BROWSER_SETTINGS_H\n#define ZEAL_BROWSER_SETTINGS_H\n\n#include <QObject>\n\nclass QWebEngineProfile;\n\nnamespace Zeal {\n\nnamespace Core {\nclass Settings;\n}\n\nnamespace Browser {\n\nclass Settings final : public QObject\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(Settings)\npublic:\n    explicit Settings(Core::Settings *appSettings, QObject *parent = nullptr);\n\n    static QWebEngineProfile *defaultProfile();\n\nprivate slots:\n    void applySettings();\n\nprivate:\n    void setCustomStyleSheet(const QString &name, const QString &cssUrl);\n\n    Core::Settings *m_appSettings = nullptr;\n    static QWebEngineProfile *m_webProfile;\n};\n\n} // namespace Browser\n} // namespace Zeal\n\n#endif // ZEAL_BROWSER_SETTINGS_H\n"
  },
  {
    "path": "src/libs/browser/urlrequestinterceptor.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2019 Kay Gawlik\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"urlrequestinterceptor.h\"\n\n#include <core/networkaccessmanager.h>\n\n#include <QLoggingCategory>\n\nusing namespace Zeal::Browser;\n\nstatic Q_LOGGING_CATEGORY(log, \"zeal.browser.urlrequestinterceptor\")\n\nUrlRequestInterceptor::UrlRequestInterceptor(QObject *parent)\n    : QWebEngineUrlRequestInterceptor(parent)\n{\n}\n\nvoid UrlRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo &info)\n{\n    const QUrl requestUrl = info.requestUrl();\n    const QUrl firstPartyUrl = info.firstPartyUrl();\n\n    // Block invalid requests.\n    if (!requestUrl.isValid() || !firstPartyUrl.isValid()) {\n        blockRequest(info);\n        return;\n    }\n\n    // Direct links are controlled in the WebPage\n    if (info.resourceType() == QWebEngineUrlRequestInfo::ResourceTypeMainFrame) {\n        return;\n    }\n\n    bool isFirstPartyUrlLocal = Core::NetworkAccessManager::isLocalUrl(firstPartyUrl);\n    bool isRequestUrlLocal = Core::NetworkAccessManager::isLocalUrl(requestUrl);\n\n    // Allow local resources on local pages and external resources on external pages.\n    if (isFirstPartyUrlLocal == isRequestUrlLocal) {\n        return;\n    }\n\n    blockRequest(info);\n}\n\nvoid UrlRequestInterceptor::blockRequest(QWebEngineUrlRequestInfo &info)\n{\n    qCDebug(log, \"Blocked request: %s '%s' (resource_type=%d, navigation_type=%d).\",\n            info.requestMethod().data(),\n            qPrintable(info.requestUrl().toString()),\n            info.resourceType(), info.navigationType());\n\n    info.block(true);\n}\n"
  },
  {
    "path": "src/libs/browser/urlrequestinterceptor.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2019 Kay Gawlik\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_BROWSER_URLREQUESTINTERCEPTOR_H\n#define ZEAL_BROWSER_URLREQUESTINTERCEPTOR_H\n\n#include <QWebEngineUrlRequestInterceptor>\n\nnamespace Zeal {\nnamespace Browser {\n\nclass UrlRequestInterceptor final : public QWebEngineUrlRequestInterceptor\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(UrlRequestInterceptor)\npublic:\n    UrlRequestInterceptor(QObject *parent = nullptr);\n    void interceptRequest(QWebEngineUrlRequestInfo &info) override;\n\nprivate:\n    void blockRequest(QWebEngineUrlRequestInfo &info);\n};\n\n} // namespace Browser\n} // namespace Zeal\n\n#endif // ZEAL_BROWSER_URLREQUESTINTERCEPTOR_H\n"
  },
  {
    "path": "src/libs/browser/webbridge.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"webbridge.h\"\n\n#include <core/application.h>\n\n#include <QDesktopServices>\n#include <QUrl>\n\nusing namespace Zeal::Browser;\n\nWebBridge::WebBridge(QObject *parent)\n    : QObject(parent)\n{\n}\n\nvoid WebBridge::openShortUrl(const QString &key)\n{\n    QDesktopServices::openUrl(QUrl(QStringLiteral(\"https://go.zealdocs.org/l/\") + key));\n}\n\nvoid WebBridge::triggerAction(const QString &action)\n{\n    emit actionTriggered(action);\n}\n\nQString WebBridge::appVersion() const\n{\n    return Core::Application::versionString();\n}\n"
  },
  {
    "path": "src/libs/browser/webbridge.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_BROWSER_WEBBRIDGE_H\n#define ZEAL_BROWSER_WEBBRIDGE_H\n\n#include <QObject>\n\nnamespace Zeal {\nnamespace Browser {\n\nclass WebBridge final : public QObject\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(WebBridge)\n    Q_PROPERTY(QString AppVersion READ appVersion CONSTANT)\npublic:\n    explicit WebBridge(QObject *parent = nullptr);\n\nsignals:\n    void actionTriggered(const QString &action);\n\npublic slots:\n    Q_INVOKABLE void openShortUrl(const QString &key);\n    Q_INVOKABLE void triggerAction(const QString &action);\n\nprivate:\n    QString appVersion() const;\n};\n\n} // namespace Browser\n} // namespace Zeal\n\n#endif // ZEAL_BROWSER_WEBBRIDGE_H\n"
  },
  {
    "path": "src/libs/browser/webcontrol.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"webcontrol.h\"\n\n#include \"searchtoolbar.h\"\n#include \"webview.h\"\n\n#include <core/networkaccessmanager.h>\n\n#include <QDataStream>\n#include <QKeyEvent>\n#include <QVBoxLayout>\n#include <QWebChannel>\n#include <QWebEngineHistory>\n#include <QWebEnginePage>\n#include <QWebEngineSettings>\n\nusing namespace Zeal::Browser;\n\nWebControl::WebControl(QWidget *parent)\n    : QWidget(parent)\n{\n    auto layout = new QVBoxLayout(this);\n    layout->setContentsMargins(0, 0, 0, 0);\n    layout->setSpacing(0);\n\n    m_webView = new WebView();\n    setFocusProxy(m_webView);\n\n    connect(m_webView->page(), &QWebEnginePage::linkHovered, this, [this](const QString &link) {\n        if (Core::NetworkAccessManager::isLocalUrl(QUrl(link))) {\n            return;\n        }\n\n        m_webView->setToolTip(link);\n    });\n    connect(m_webView, &QWebEngineView::titleChanged, this, &WebControl::titleChanged);\n    connect(m_webView, &QWebEngineView::urlChanged, this, &WebControl::urlChanged);\n\n    layout->addWidget(m_webView);\n\n    setLayout(layout);\n}\n\nvoid WebControl::focus()\n{\n    m_webView->setFocus();\n}\n\nint WebControl::zoomLevel() const\n{\n    return m_webView->zoomLevel();\n}\n\nvoid WebControl::setZoomLevel(int level)\n{\n    m_webView->setZoomLevel(level);\n}\n\nvoid WebControl::zoomIn()\n{\n    m_webView->zoomIn();\n}\n\nvoid WebControl::zoomOut()\n{\n    m_webView->zoomOut();\n}\n\nvoid WebControl::resetZoom()\n{\n    m_webView->resetZoom();\n}\n\nvoid WebControl::setJavaScriptEnabled(bool enabled)\n{\n    m_webView->page()->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, enabled);\n}\n\nvoid WebControl::setWebBridgeObject(const QString &name, QObject *object)\n{\n    QWebEnginePage *page = m_webView->page();\n\n    QWebChannel *channel = new QWebChannel(page);\n    channel->registerObject(name, object);\n\n    page->setWebChannel(channel);\n}\n\nvoid WebControl::load(const QUrl &url)\n{\n    m_webView->load(url);\n}\n\nvoid WebControl::activateSearchBar()\n{\n    if (m_searchToolBar == nullptr) {\n        m_searchToolBar = new SearchToolBar(m_webView);\n        layout()->addWidget(m_searchToolBar);\n    }\n\n    if (m_webView->hasSelection()) {\n        const QString selectedText = m_webView->selectedText().simplified();\n        if (!selectedText.isEmpty()) {\n            m_searchToolBar->setText(selectedText);\n        }\n    }\n\n    m_searchToolBar->activate();\n}\n\nvoid WebControl::back()\n{\n    m_webView->back();\n}\n\nvoid WebControl::forward()\n{\n    m_webView->forward();\n}\n\nbool WebControl::canGoBack() const\n{\n    return m_webView->history()->canGoBack();\n}\n\nbool WebControl::canGoForward() const\n{\n    return m_webView->history()->canGoForward();\n}\n\nQString WebControl::title() const\n{\n    return m_webView->title();\n}\n\nQUrl WebControl::url() const\n{\n    return m_webView->url();\n}\n\nQWebEngineHistory *WebControl::history() const\n{\n    return m_webView->history();\n}\n\nvoid WebControl::restoreHistory(const QByteArray &array)\n{\n    QDataStream stream(array);\n    stream >> *m_webView->history();\n}\n\nQByteArray WebControl::saveHistory() const\n{\n    QByteArray array;\n    QDataStream stream(&array, QIODevice::WriteOnly);\n    stream << *m_webView->history();\n    return array;\n}\n\nvoid WebControl::keyPressEvent(QKeyEvent *event)\n{\n    switch (event->key()) {\n    case Qt::Key_Slash:\n        activateSearchBar();\n        break;\n    default:\n        event->ignore();\n        break;\n    }\n}\n"
  },
  {
    "path": "src/libs/browser/webcontrol.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_BROWSER_WEBCONTROL_H\n#define ZEAL_BROWSER_WEBCONTROL_H\n\n#include <QWidget>\n\nclass QWebEngineHistory;\n\nnamespace Zeal {\nnamespace Browser {\n\nclass SearchToolBar;\nclass WebView;\n\nclass WebControl final : public QWidget\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(WebControl)\npublic:\n    explicit WebControl(QWidget *parent = nullptr);\n\n    void focus();\n    void load(const QUrl &url);\n    bool canGoBack() const;\n    bool canGoForward() const;\n\n    QString title() const;\n    QUrl url() const;\n\n    QWebEngineHistory *history() const;\n    void restoreHistory(const QByteArray &array);\n    QByteArray saveHistory() const;\n\n    int zoomLevel() const;\n    void setZoomLevel(int level);\n    void setJavaScriptEnabled(bool enabled);\n\n    void setWebBridgeObject(const QString &name, QObject *object);\n\nsignals:\n    void titleChanged(const QString &title);\n    void urlChanged(const QUrl &url);\n\npublic slots:\n    void activateSearchBar();\n    void back();\n    void forward();\n\n    void zoomIn();\n    void zoomOut();\n    void resetZoom();\n\nprotected:\n    void keyPressEvent(QKeyEvent *event) override;\n\nprivate:\n    friend class WebView;\n\n    WebView *m_webView = nullptr;\n    SearchToolBar *m_searchToolBar = nullptr;\n};\n\n} // namespace Browser\n} // namespace Zeal\n\n#endif // ZEAL_BROWSER_WEBCONTROL_H\n"
  },
  {
    "path": "src/libs/browser/webpage.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2019 Kay Gawlik\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"webpage.h\"\n\n#include \"settings.h\"\n\n#include <core/application.h>\n#include <core/networkaccessmanager.h>\n#include <core/settings.h>\n\n#include <QCheckBox>\n#include <QDesktopServices>\n#include <QFileInfo>\n#include <QLoggingCategory>\n#include <QMessageBox>\n#include <QPushButton>\n\nusing namespace Zeal::Browser;\n\nstatic Q_LOGGING_CATEGORY(log, \"zeal.browser.webpage\")\n\nWebPage::WebPage(QObject *parent)\n    : QWebEnginePage(Settings::defaultProfile(), parent)\n{\n}\n\nbool WebPage::acceptNavigationRequest(const QUrl &requestUrl, QWebEnginePage::NavigationType type, bool isMainFrame)\n{\n    Q_UNUSED(type)\n\n    // Local elements are always allowed.\n    if (Core::NetworkAccessManager::isLocalUrl(requestUrl)) {\n        return true;\n    }\n\n    // Allow external resources if already on an external page.\n    const QUrl pageUrl = url();\n    if (pageUrl.isValid() && !Core::NetworkAccessManager::isLocalUrl(pageUrl)) {\n        return true;\n    }\n\n    // Block external elements on local pages.\n    if (!isMainFrame) {\n        qCDebug(log, \"Blocked request to '%s'.\", qPrintable(requestUrl.toString()));\n        return false;\n    }\n\n    auto appSettings = Core::Application::instance()->settings();\n\n    // TODO: [C++20] using enum Core::Settings::ExternalLinkPolicy;\n    typedef Core::Settings::ExternalLinkPolicy ExternalLinkPolicy;\n\n    switch (appSettings->externalLinkPolicy) {\n    case ExternalLinkPolicy::Open:\n        return true;\n    case ExternalLinkPolicy::Ask: {\n        QMessageBox mb;\n        mb.setIcon(QMessageBox::Question);\n        mb.setText(tr(\"How do you want to open the external link?<br>URL: <b>%1</b>\")\n                   .arg(requestUrl.toString()));\n\n        QCheckBox *checkBox = new QCheckBox(\"Do &not ask again\");\n        mb.setCheckBox(checkBox);\n\n        QPushButton *openInBrowserButton = mb.addButton(tr(\"Open in &Desktop Browser\"), QMessageBox::ActionRole);\n        QPushButton *openInZealButton = mb.addButton(tr(\"Open in &Zeal\"), QMessageBox::ActionRole);\n        mb.addButton(QMessageBox::Cancel);\n\n        mb.setDefaultButton(openInBrowserButton);\n\n        if (mb.exec() == QMessageBox::Cancel) {\n            qCDebug(log, \"Blocked request to '%s'.\", qPrintable(requestUrl.toString()));\n            return false;\n        }\n\n        if (mb.clickedButton() == openInZealButton) {\n            if (checkBox->isChecked()) {\n                appSettings->externalLinkPolicy = ExternalLinkPolicy::Open;\n                appSettings->save();\n            }\n\n            return true;\n        }\n\n        if (mb.clickedButton() == openInBrowserButton) {\n            if (checkBox->isChecked()) {\n                appSettings->externalLinkPolicy = ExternalLinkPolicy::OpenInSystemBrowser;\n                appSettings->save();\n            }\n\n            QDesktopServices::openUrl(requestUrl);\n            return false;\n        }\n\n        break;\n    }\n    case ExternalLinkPolicy::OpenInSystemBrowser:\n        QDesktopServices::openUrl(requestUrl);\n        return false;\n    }\n\n    // This code should not be reachable so log a warning.\n    qCWarning(log, \"Blocked request to '%s'.\", qPrintable(requestUrl.toString()));\n\n    return false;\n}\n\nvoid WebPage::javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level,\n                                       const QString &message,\n                                       int lineNumber,\n                                       const QString &sourceId)\n{\n    qCDebug(log,\n            \"%s [%s:%d] %s\",\n            qPrintable(QVariant::fromValue(level).toString()),\n            qPrintable(sourceId),\n            lineNumber,\n            qPrintable(message));\n}\n"
  },
  {
    "path": "src/libs/browser/webpage.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2019 Kay Gawlik\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_BROWSER_WEBPAGE_H\n#define ZEAL_BROWSER_WEBPAGE_H\n\n#include <QWebEnginePage>\n\nnamespace Zeal {\nnamespace Browser {\n\nclass WebPage final : public QWebEnginePage\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(WebPage)\npublic:\n    explicit WebPage(QObject *parent = nullptr);\n\nprotected:\n    bool acceptNavigationRequest(const QUrl &requestUrl, NavigationType type, bool isMainFrame) override;\n    void javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message, int lineNumber, const QString &sourceId) override;\n};\n\n} // namespace Browser\n} // namespace Zeal\n\n#endif // ZEAL_BROWSER_WEBPAGE_H\n"
  },
  {
    "path": "src/libs/browser/webview.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"webview.h\"\n\n#include \"webcontrol.h\"\n#include \"webpage.h\"\n\n#include <core/application.h>\n#include <core/settings.h>\n#include <ui/browsertab.h>\n#include <ui/mainwindow.h>\n\n#include <QApplication>\n#include <QCheckBox>\n#include <QDesktopServices>\n#include <QMenu>\n#include <QMessageBox>\n#include <QPushButton>\n#include <QVector>\n#include <QWebEngineProfile>\n#include <QWebEngineSettings>\n#include <QWheelEvent>\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n#include <QWebEngineContextMenuData>\n#else\n#include <QWebEngineContextMenuRequest>\n#endif\n\n#include <algorithm>\n\nusing namespace Zeal::Browser;\n\nWebView::WebView(QWidget *parent)\n    : QWebEngineView(parent)\n{\n    setPage(new WebPage(this));\n    setZoomLevel(defaultZoomLevel());\n\n    // Enable plugins for PDF support.\n    settings()->setAttribute(QWebEngineSettings::PluginsEnabled, true);\n\n    settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false);\n\n    QApplication::instance()->installEventFilter(this);\n}\n\nint WebView::zoomLevel() const\n{\n    return m_zoomLevel;\n}\n\nvoid WebView::setZoomLevel(int level)\n{\n    if (level == m_zoomLevel) {\n        return;\n    }\n\n    level = qMax(0, level);\n    level = qMin(level, availableZoomLevels().size() - 1);\n\n    m_zoomLevel = level;\n\n    // Scale the webview relative to the DPI of the screen.\n    // Only scale up for HiDPI displays (>96 DPI), never scale down.\n    const double dpiZoomFactor = std::max(1.0, logicalDpiY() / 96.0);\n\n    setZoomFactor(availableZoomLevels().at(level) / 100.0 * dpiZoomFactor);\n    emit zoomLevelChanged();\n}\n\nconst QVector<int> &WebView::availableZoomLevels()\n{\n    static const QVector<int> zoomLevels = {30, 40, 50, 67, 80, 90, 100,\n                                            110, 120, 133, 150, 170, 200,\n                                            220, 233, 250, 270, 285, 300};\n    return zoomLevels;\n}\n\nint WebView::defaultZoomLevel()\n{\n    static const int level = availableZoomLevels().indexOf(100);\n    return level;\n}\n\nvoid WebView::zoomIn()\n{\n    setZoomLevel(m_zoomLevel + 1);\n}\n\nvoid WebView::zoomOut()\n{\n    setZoomLevel(m_zoomLevel - 1);\n}\n\nvoid WebView::resetZoom()\n{\n    setZoomLevel(defaultZoomLevel());\n}\n\nQWebEngineView *WebView::createWindow(QWebEnginePage::WebWindowType type)\n{\n    Q_UNUSED(type)\n    return Core::Application::instance()->mainWindow()->createTab()->webControl()->m_webView;\n}\n\nvoid WebView::contextMenuEvent(QContextMenuEvent *event)\n{\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n    const QWebEngineContextMenuData& contextData = page()->contextMenuData();\n\n    if (!contextData.isValid()) {\n        QWebEngineView::contextMenuEvent(event);\n        return;\n    }\n#else\n    QWebEngineContextMenuRequest *contextMenuRequest = lastContextMenuRequest();\n    if (contextMenuRequest == nullptr) {\n        QWebEngineView::contextMenuEvent(event);\n        return;\n    }\n#endif\n\n    event->accept();\n\n    if (m_contextMenu) {\n        m_contextMenu->deleteLater();\n    }\n\n    m_contextMenu = new QMenu(this);\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n    QUrl linkUrl = contextData.linkUrl();\n#else\n    QUrl linkUrl = contextMenuRequest->linkUrl();\n#endif\n\n    if (linkUrl.isValid()) {\n        const QString scheme = linkUrl.scheme();\n\n        if (scheme != QLatin1String(\"javascript\")) {\n            m_contextMenu->addAction(tr(\"Open Link in New Tab\"), this, [this]() {\n                triggerPageAction(QWebEnginePage::WebAction::OpenLinkInNewWindow);\n            });\n        }\n\n        if (scheme != QLatin1String(\"qrc\")) {\n            if (scheme != QLatin1String(\"javascript\")) {\n                m_contextMenu->addAction(tr(\"Open Link in Desktop Browser\"), this, [linkUrl]() {\n                    QDesktopServices::openUrl(linkUrl);\n                });\n            }\n\n            m_contextMenu->addAction(pageAction(QWebEnginePage::CopyLinkToClipboard));\n        }\n    }\n\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n    const QString selectedText = contextData.selectedText();\n#else\n    const QString selectedText = contextMenuRequest->selectedText();\n#endif\n\n    if (!selectedText.isEmpty()) {\n        if (!m_contextMenu->isEmpty()) {\n            m_contextMenu->addSeparator();\n        }\n\n        m_contextMenu->addAction(pageAction(QWebEnginePage::Copy));\n    }\n\n    if (!linkUrl.isValid() && url().scheme() != QLatin1String(\"qrc\")) {\n        if (!m_contextMenu->isEmpty()) {\n            m_contextMenu->addSeparator();\n        }\n\n        m_contextMenu->addAction(pageAction(QWebEnginePage::Back));\n        m_contextMenu->addAction(pageAction(QWebEnginePage::Forward));\n        m_contextMenu->addSeparator();\n\n        m_contextMenu->addAction(tr(\"Open Page in Desktop Browser\"), this, [this]() {\n            QDesktopServices::openUrl(url());\n        });\n    }\n\n    if (m_contextMenu->isEmpty()) {\n        return;\n    }\n\n    m_contextMenu->popup(event->globalPos());\n}\n\nbool WebView::handleMouseReleaseEvent(QMouseEvent *event)\n{\n    switch (event->button()) {\n    case Qt::BackButton:\n        // Check if cursor is still inside webview.\n        if (rect().contains(event->pos())) {\n            back();\n        }\n\n        event->accept();\n        return true;\n\n    case Qt::ForwardButton:\n        if (rect().contains(event->pos())) {\n            forward();\n        }\n\n        event->accept();\n        return true;\n\n    default:\n        break;\n    }\n\n    return false;\n}\n\nbool WebView::handleWheelEvent(QWheelEvent *event)\n{\n    if (event->modifiers() & Qt::ControlModifier) {\n        const QPoint angleDelta = event->angleDelta();\n        int delta = qAbs(angleDelta.x()) > qAbs(angleDelta.y()) ? angleDelta.x() : angleDelta.y();\n        const int direction = delta > 0 ? 1 : -1;\n\n        int levelDelta = 0;\n        while (delta * direction >= 120) {\n            levelDelta += direction;\n            delta -= 120 * direction;\n        }\n\n        setZoomLevel(m_zoomLevel + levelDelta);\n        event->accept();\n        return true;\n    }\n\n    return false;\n}\n\nbool WebView::eventFilter(QObject *watched, QEvent *event)\n{\n    if (watched->parent() == this) {\n        switch (event->type()) {\n        case QEvent::MouseButtonRelease:\n            if (handleMouseReleaseEvent(static_cast<QMouseEvent *>(event))) {\n                return true;\n            }\n\n            break;\n\n        case QEvent::Wheel:\n            if (handleWheelEvent(static_cast<QWheelEvent *>(event))) {\n                return true;\n            }\n\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    return QWebEngineView::eventFilter(watched, event);\n}\n"
  },
  {
    "path": "src/libs/browser/webview.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_BROWSER_WEBVIEW_H\n#define ZEAL_BROWSER_WEBVIEW_H\n\n#include <QWebEngineView>\n\nnamespace Zeal {\nnamespace Browser {\n\nclass WebView final : public QWebEngineView\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(WebView)\npublic:\n    explicit WebView(QWidget *parent = nullptr);\n\n    int zoomLevel() const;\n    void setZoomLevel(int level);\n\n    bool eventFilter(QObject *watched, QEvent *event) override;\n\n    static const QVector<int> &availableZoomLevels();\n    static int defaultZoomLevel();\n\npublic slots:\n    void zoomIn();\n    void zoomOut();\n    void resetZoom();\n\nsignals:\n    void zoomLevelChanged();\n\nprotected:\n    QWebEngineView *createWindow(QWebEnginePage::WebWindowType type) override;\n    void contextMenuEvent(QContextMenuEvent *event) override;\n\nprivate:\n    bool handleMouseReleaseEvent(QMouseEvent *event);\n    bool handleWheelEvent(QWheelEvent *event);\n\n    QMenu *m_contextMenu = nullptr;\n    QUrl m_clickedLink;\n    int m_zoomLevel = 0;\n};\n\n} // namespace Browser\n} // namespace Zeal\n\n#endif // ZEAL_BROWSER_WEBVIEW_H\n"
  },
  {
    "path": "src/libs/core/CMakeLists.txt",
    "content": "add_library(Core STATIC\n    application.cpp\n    applicationsingleton.cpp\n    extractor.cpp\n    filemanager.cpp\n    httpserver.cpp\n    networkaccessmanager.cpp\n    settings.cpp\n)\n\ntarget_link_libraries(Core PRIVATE Registry Ui)\n\nfind_package(Qt${QT_VERSION_MAJOR} COMPONENTS Network WebEngineCore Widgets REQUIRED)\ntarget_link_libraries(Core PRIVATE Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::WebEngineCore Qt${QT_VERSION_MAJOR}::Widgets)\n\nif(QT_VERSION_MAJOR EQUAL 6)\n    find_package(Qt6 COMPONENTS WebEngineCore REQUIRED)\n    target_link_libraries(Core PRIVATE Qt6::WebEngineCore)\nelse()\n    find_package(Qt5 COMPONENTS WebEngineWidgets REQUIRED)\n    target_link_libraries(Core PRIVATE Qt5::WebEngineWidgets)\nendif()\n\nfind_package(LibArchive QUIET)\nif(NOT LibArchive_FOUND)\n    find_path(LibArchive_INCLUDE_DIRS archive.h\n        PATHS /opt/homebrew/opt/libarchive/include /usr/local/opt/libarchive/include\n        REQUIRED\n    )\n    find_library(LibArchive_LIBRARIES\n        NAMES archive libarchive\n        PATHS /opt/homebrew/opt/libarchive/lib /usr/local/opt/libarchive/lib\n        REQUIRED\n        NO_DEFAULT_PATH\n    )\nendif()\n\nif((CMAKE_VERSION VERSION_GREATER_EQUAL 3.17.0) AND (TARGET LibArchive::LibArchive))\n    target_link_libraries(Core PRIVATE LibArchive::LibArchive)\nelse()\n    include_directories(${LibArchive_INCLUDE_DIRS})\n    target_link_libraries(Core PRIVATE ${LibArchive_LIBRARIES})\nendif()\n\n# Configure cpp-httplib.\nadd_definitions(-DCPPHTTPLIB_USE_POLL)\n\nfind_package(httplib CONFIG QUIET)\nif(httplib_FOUND)\n    target_link_libraries(Core PRIVATE httplib::httplib)\nelse()\n    # Use bundled version of cpp-httplib if not found.\n    # TODO: Replace with QHttpServer once Qt 5 is dropped.\n    include_directories(\"${CMAKE_SOURCE_DIR}/src/contrib/cpp-httplib\")\nendif()\n\n# Required by cpp-httplib.\nif(NOT WIN32)\n    set(THREADS_PREFER_PTHREAD_FLAG ON)\n    find_package(Threads REQUIRED)\n    target_link_libraries(Core PRIVATE Threads::Threads)\nendif()\n"
  },
  {
    "path": "src/libs/core/application.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"application.h\"\n\n#include \"extractor.h\"\n#include \"filemanager.h\"\n#include \"httpserver.h\"\n#include \"networkaccessmanager.h\"\n#include \"settings.h\"\n\n#include <registry/docsetregistry.h>\n#include <registry/searchquery.h>\n#include <ui/mainwindow.h>\n\n#include <QCoreApplication>\n#include <QJsonArray>\n#include <QJsonDocument>\n#include <QJsonObject>\n#include <QMetaObject>\n#include <QNetworkProxy>\n#include <QNetworkReply>\n#include <QScopedPointer>\n#include <QStandardPaths>\n#include <QSysInfo>\n#include <QThread>\n\nusing namespace Zeal;\nusing namespace Zeal::Core;\n\nnamespace {\nconstexpr char ReleasesApiUrl[] = \"https://api.zealdocs.org/v1/releases\";\n} // namespace\n\nApplication *Application::m_instance = nullptr;\n\nApplication::Application(QObject *parent)\n    : QObject(parent)\n{\n    // Ensure only one instance of Application\n    Q_ASSERT(!m_instance);\n    m_instance = this;\n\n    m_settings = new Settings(this);\n    m_networkManager = new NetworkAccessManager(this);\n\n    m_fileManager = new FileManager(this);\n    m_httpServer = new HttpServer(this);\n\n    connect(m_networkManager, &QNetworkAccessManager::sslErrors,\n            this, [this](QNetworkReply *reply, const QList<QSslError> &errors) {\n        Q_UNUSED(errors);\n        if (m_settings->isIgnoreSslErrorsEnabled) {\n            reply->ignoreSslErrors();\n        }\n    });\n\n    // Extractor setup\n    m_extractorThread = new QThread(this);\n    m_extractor = new Extractor();\n    m_extractor->moveToThread(m_extractorThread);\n    m_extractorThread->start();\n    connect(m_extractor, &Extractor::completed, this, &Application::extractionCompleted);\n    connect(m_extractor, &Extractor::error, this, &Application::extractionError);\n    connect(m_extractor, &Extractor::progress, this, &Application::extractionProgress);\n\n    m_docsetRegistry = new Registry::DocsetRegistry();\n\n    connect(m_settings, &Settings::updated, this, &Application::applySettings);\n    applySettings();\n\n    m_mainWindow = new WidgetUi::MainWindow(this);\n}\n\nApplication::~Application()\n{\n    m_extractorThread->quit();\n    m_extractorThread->wait();\n    delete m_extractor;\n    delete m_mainWindow;\n    delete m_docsetRegistry;\n}\n\n/*!\n * \\internal\n * \\brief Returns a pointer to the Core::Application instance.\n * \\return A pointer or \\c nullptr, if no instance has been created.\n */\nApplication *Application::instance()\n{\n    return m_instance;\n}\n\nWidgetUi::MainWindow *Application::mainWindow() const\n{\n    return m_mainWindow;\n}\n\nvoid Application::showMainWindow(bool forceMinimized)\n{\n    if (m_mainWindow->isVisible()) {\n        return;\n    }\n\n    if (forceMinimized || m_settings->startMinimized) {\n        if (m_settings->showSystrayIcon && m_settings->minimizeToSystray) {\n            return;\n        }\n\n        m_mainWindow->showMinimized();\n    } else {\n        m_mainWindow->show();\n    }\n}\n\nQNetworkAccessManager *Application::networkManager() const\n{\n    return m_networkManager;\n}\n\nSettings *Application::settings() const\n{\n    return m_settings;\n}\n\nRegistry::DocsetRegistry *Application::docsetRegistry()\n{\n    return m_docsetRegistry;\n}\n\nFileManager *Application::fileManager() const\n{\n    return m_fileManager;\n}\n\nHttpServer *Application::httpServer() const\n{\n    return m_httpServer;\n}\n\nQString Application::cacheLocation()\n{\n#ifndef PORTABLE_BUILD\n    return QStandardPaths::writableLocation(QStandardPaths::CacheLocation);\n#else\n    return QCoreApplication::applicationDirPath() + QLatin1String(\"/cache\");\n#endif\n}\n\nQString Application::configLocation()\n{\n#ifndef PORTABLE_BUILD\n    // TODO: Replace 'Zeal/Zeal' with 'zeal'.\n    return QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);\n#else\n    return QCoreApplication::applicationDirPath() + QLatin1String(\"/config\");\n#endif\n}\n\nQVersionNumber Application::version()\n{\n    static const auto vn = QVersionNumber::fromString(QCoreApplication::applicationVersion());\n    return vn;\n}\n\nQString Application::versionString()\n{\n    static const auto v = QStringLiteral(\"v%1\").arg(QCoreApplication::applicationVersion());\n    return v;\n}\n\nvoid Application::executeQuery(const Registry::SearchQuery &query, bool preventActivation)\n{\n    m_mainWindow->search(query);\n\n    if (preventActivation)\n        return;\n\n    m_mainWindow->bringToFront();\n}\n\nvoid Application::extract(const QString &filePath, const QString &destination, const QString &root)\n{\n    QMetaObject::invokeMethod(m_extractor, \"extract\", Qt::QueuedConnection,\n                              Q_ARG(QString, filePath), Q_ARG(QString, destination),\n                              Q_ARG(QString, root));\n}\n\nQNetworkReply *Application::download(const QUrl &url)\n{\n    static const QString ua = userAgent();\n    static const QByteArray uaJson = userAgentJson().toUtf8();\n\n    QNetworkRequest request(url);\n    request.setHeader(QNetworkRequest::UserAgentHeader, ua);\n\n    if (url.host().endsWith(QLatin1String(\".zealdocs.org\", Qt::CaseInsensitive)))\n        request.setRawHeader(\"X-Zeal-User-Agent\", uaJson);\n\n    return m_networkManager->get(request);\n}\n\n/*!\n  \\internal\n\n  Performs a check whether a new Zeal version is available. Setting \\a quiet to true suppresses\n  error and \"you are using the latest version\" message boxes.\n*/\nvoid Application::checkForUpdates(bool quiet)\n{\n    QNetworkReply *reply = download(QUrl(ReleasesApiUrl));\n    connect(reply, &QNetworkReply::finished, this, [this, quiet]() {\n        QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(\n                    qobject_cast<QNetworkReply *>(sender()));\n\n        if (reply->error() != QNetworkReply::NoError) {\n            if (!quiet)\n                emit updateCheckError(reply->errorString());\n            return;\n        }\n\n        QJsonParseError jsonError;\n        const QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &jsonError);\n\n        if (jsonError.error != QJsonParseError::NoError) {\n            if (!quiet)\n                emit updateCheckError(jsonError.errorString());\n            return;\n        }\n\n        const QJsonObject versionInfo = jsonDoc.array().first().toObject(); // Latest is the first.\n        const auto latestVersion\n                = QVersionNumber::fromString(versionInfo[QLatin1String(\"version\")].toString());\n        if (latestVersion > version()) {\n            emit updateCheckDone(latestVersion.toString());\n        } else if (!quiet) {\n            emit updateCheckDone();\n        }\n    });\n}\n\nvoid Application::applySettings()\n{\n    m_docsetRegistry->setFuzzySearchEnabled(m_settings->isFuzzySearchEnabled);\n    m_docsetRegistry->setStoragePath(m_settings->docsetPath);\n\n    // HTTP Proxy Settings\n    switch (m_settings->proxyType) {\n    case Settings::ProxyType::None:\n        QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy);\n        break;\n\n    case Settings::ProxyType::System:\n        QNetworkProxyFactory::setUseSystemConfiguration(true);\n        break;\n\n    case Settings::ProxyType::Http:\n    case Settings::ProxyType::Socks5: {\n        const QNetworkProxy::ProxyType type = m_settings->proxyType == Settings::ProxyType::Socks5\n                ? QNetworkProxy::Socks5Proxy\n                : QNetworkProxy::HttpProxy;\n\n        QNetworkProxy proxy(type, m_settings->proxyHost, m_settings->proxyPort);\n        if (m_settings->proxyAuthenticate) {\n            proxy.setUser(m_settings->proxyUserName);\n            proxy.setPassword(m_settings->proxyPassword);\n        }\n\n        QNetworkProxy::setApplicationProxy(proxy);\n\n        break;\n    }\n    }\n\n    // Force NM to pick up changes.\n    m_networkManager->clearAccessCache();\n}\n\nQString Application::userAgent()\n{\n    return QStringLiteral(\"Zeal/%1\").arg(QCoreApplication::applicationVersion());\n}\n\nQString Application::userAgentJson() const\n{\n    QJsonObject app = {\n        {QStringLiteral(\"version\"), QCoreApplication::applicationVersion()},\n        {QStringLiteral(\"qt_version\"), qVersion()},\n        {QStringLiteral(\"install_id\"), m_settings->installId}\n    };\n\n    QJsonObject os = {\n        {QStringLiteral(\"arch\"), QSysInfo::currentCpuArchitecture()},\n        {QStringLiteral(\"name\"), QSysInfo::prettyProductName()},\n        {QStringLiteral(\"product_type\"), QSysInfo::productType()},\n        {QStringLiteral(\"product_version\"), QSysInfo::productVersion()},\n        {QStringLiteral(\"kernel_type\"), QSysInfo::kernelType()},\n        {QStringLiteral(\"kernel_version\"), QSysInfo::kernelVersion()},\n        {QStringLiteral(\"locale\"), QLocale::system().name()}\n    };\n\n    QJsonObject ua = {\n        {QStringLiteral(\"app\"), app},\n        {QStringLiteral(\"os\"), os}\n    };\n\n    return QString::fromUtf8(QJsonDocument(ua).toJson(QJsonDocument::Compact));\n}\n"
  },
  {
    "path": "src/libs/core/application.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_CORE_APPLICATION_H\n#define ZEAL_CORE_APPLICATION_H\n\n#include <QObject>\n#include <QVersionNumber>\n\nclass QNetworkAccessManager;\nclass QNetworkReply;\nclass QThread;\n\nnamespace Zeal {\n\nnamespace Registry {\nclass DocsetRegistry;\nclass SearchQuery;\n} // namespace Registry\n\nnamespace WidgetUi {\nclass MainWindow;\n} // namespace WidgetUi\n\nnamespace Core {\n\nclass Extractor;\nclass FileManager;\nclass HttpServer;\nclass Settings;\n\nclass Application final : public QObject\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(Application)\npublic:\n    explicit Application(QObject *parent = nullptr);\n    ~Application() override;\n\n    static Application *instance();\n\n    WidgetUi::MainWindow *mainWindow() const;\n    void showMainWindow(bool forceMinimized = false);\n\n    QNetworkAccessManager *networkManager() const;\n    Settings *settings() const;\n\n    Registry::DocsetRegistry *docsetRegistry();\n    FileManager *fileManager() const;\n    HttpServer *httpServer() const;\n\n    static QString cacheLocation();\n    static QString configLocation();\n    static QVersionNumber version();\n    static QString versionString();\n\npublic slots:\n    void executeQuery(const Registry::SearchQuery &query, bool preventActivation);\n    void extract(const QString &filePath, const QString &destination, const QString &root = QString());\n    QNetworkReply *download(const QUrl &url);\n    void checkForUpdates(bool quiet = false);\n\nsignals:\n    void extractionCompleted(const QString &filePath);\n    void extractionError(const QString &filePath, const QString &errorString);\n    void extractionProgress(const QString &filePath, qint64 extracted, qint64 total);\n    void updateCheckDone(const QString &version = QString());\n    void updateCheckError(const QString &message);\n\nprivate slots:\n    void applySettings();\n\nprivate:\n    static inline QString userAgent();\n    QString userAgentJson() const;\n\n    static Application *m_instance;\n\n    Settings *m_settings = nullptr;\n\n    QNetworkAccessManager *m_networkManager = nullptr;\n\n    FileManager *m_fileManager = nullptr;\n    HttpServer *m_httpServer = nullptr;\n\n    QThread *m_extractorThread = nullptr;\n    Extractor *m_extractor = nullptr;\n\n    Registry::DocsetRegistry *m_docsetRegistry = nullptr;\n\n    WidgetUi::MainWindow *m_mainWindow = nullptr;\n};\n\n} // namespace Core\n} // namespace Zeal\n\n#endif // ZEAL_CORE_APPLICATION_H\n"
  },
  {
    "path": "src/libs/core/applicationsingleton.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"applicationsingleton.h\"\n\n#include <QCoreApplication>\n#include <QCryptographicHash>\n#include <QDir>\n#include <QLocalServer>\n#include <QLocalSocket>\n#include <QLoggingCategory>\n#include <QScopedPointer>\n#include <QSharedMemory>\n\nusing namespace Zeal::Core;\n\nstatic Q_LOGGING_CATEGORY(log, \"zeal.core.applicationsingleton\")\n\nstruct SharedData\n{\n    qint64 primaryPid;\n};\n\nApplicationSingleton::ApplicationSingleton(QObject *parent)\n    : QObject(parent)\n{\n    if (QCoreApplication::instance() == nullptr) {\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n        qCFatal(log, \"QCoreApplication (or derived type) must be created before ApplicationSingleton.\");\n#else\n        qFatal(\"QCoreApplication (or derived type) must be created before ApplicationSingleton.\");\n#endif\n    }\n\n    m_id = computeId();\n    qCDebug(log, \"Singleton ID: %s\", qPrintable(m_id));\n\n    m_sharedMemory = new QSharedMemory(m_id, this);\n\n    m_isPrimary = m_sharedMemory->create(sizeof(SharedData));\n    if (m_isPrimary) {\n        setupPrimary();\n        return;\n    }\n\n#ifdef Q_OS_UNIX\n    // Verify it's not a segment that survived an application crash.\n    m_sharedMemory->attach();\n    m_sharedMemory->detach();\n\n    m_isPrimary = m_sharedMemory->create(sizeof(SharedData));\n    if (m_isPrimary) {\n        setupPrimary();\n        return;\n    }\n#endif\n\n    if (!m_sharedMemory->attach(QSharedMemory::ReadOnly)) {\n        qCWarning(log) << \"Cannot attach to the shared memory segment:\"\n                       << m_sharedMemory->errorString();\n        return;\n    }\n\n    setupSecondary();\n}\n\nbool ApplicationSingleton::isPrimary() const\n{\n    return m_isPrimary;\n}\n\nbool ApplicationSingleton::isSecondary() const\n{\n    return !m_isPrimary;\n}\n\nqint64 ApplicationSingleton::primaryPid() const\n{\n    return m_primaryPid;\n}\n\nbool ApplicationSingleton::sendMessage(QByteArray &data, int timeout)\n{\n    // No support for primary to secondary communication.\n    if (m_isPrimary) {\n        return false;\n    }\n\n    QScopedPointer<QLocalSocket, QScopedPointerDeleteLater> socket(new QLocalSocket);\n    socket->connectToServer(m_id);\n    if (!socket->waitForConnected(timeout)) {\n        qCWarning(log) << \"Cannot connect to the local service:\"\n                       << socket->errorString();\n        return false;\n    }\n\n    socket->write(data);\n    socket->flush(); // Required for Linux.\n    return socket->waitForBytesWritten(timeout);\n}\n\nvoid ApplicationSingleton::setupPrimary()\n{\n    m_primaryPid = QCoreApplication::applicationPid();\n\n    qCInfo(log, \"Starting as a primary instance. (PID: %lld)\", m_primaryPid);\n\n    m_sharedMemory->lock();\n    auto sd = static_cast<SharedData *>(m_sharedMemory->data());\n    sd->primaryPid = m_primaryPid;\n    m_sharedMemory->unlock();\n\n    QLocalServer::removeServer(m_id);\n\n    m_localServer = new QLocalServer(this);\n    m_localServer->setSocketOptions(QLocalServer::UserAccessOption);\n\n    connect(m_localServer, &QLocalServer::newConnection, this, [this] {\n        QLocalSocket *socket = m_localServer->nextPendingConnection();\n        connect(socket, &QLocalSocket::readyRead, this, [this, socket] {\n            QByteArray data = socket->readAll();\n            emit messageReceived(data);\n            socket->deleteLater();\n        });\n    });\n\n    if (!m_localServer->listen(m_id)) {\n        qCWarning(log) << \"Cannot start the local service:\"\n                       << m_localServer->errorString();\n        return;\n    }\n}\n\nvoid ApplicationSingleton::setupSecondary()\n{\n    m_sharedMemory->lock();\n    auto sd = static_cast<SharedData *>(m_sharedMemory->data());\n    m_primaryPid = sd->primaryPid;\n    m_sharedMemory->unlock();\n\n    qCInfo(log, \"Starting as a secondary instance. (Primary PID: %lld)\", m_primaryPid);\n}\n\nQString ApplicationSingleton::computeId()\n{\n    // Make sure the result can be used as a name for the local socket.\n    static const QByteArray::Base64Options base64Options\n            = QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals;\n\n    QCryptographicHash hash(QCryptographicHash::Sha256);\n    hash.addData(QCoreApplication::applicationName().toUtf8());\n    hash.addData(QCoreApplication::organizationName().toUtf8());\n    hash.addData(QCoreApplication::organizationDomain().toUtf8());\n    // Support multi-user setup.\n    hash.addData(QDir::homePath().toUtf8());\n\n    return QString::fromLatin1(hash.result().toBase64(base64Options));\n}\n"
  },
  {
    "path": "src/libs/core/applicationsingleton.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_CORE_APPLICATIONSINGLETON_H\n#define ZEAL_CORE_APPLICATIONSINGLETON_H\n\n#include <QObject>\n\nclass QLocalServer;\nclass QSharedMemory;\n\nnamespace Zeal {\nnamespace Core {\n\nclass ApplicationSingleton final : public QObject\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(ApplicationSingleton)\npublic:\n    explicit ApplicationSingleton(QObject *parent = nullptr);\n\n    bool isPrimary() const;\n    bool isSecondary() const;\n    qint64 primaryPid() const;\n\n    bool sendMessage(QByteArray &data, int timeout = 500);\n\nsignals:\n    void messageReceived(const QByteArray &data);\n\nprivate:\n    void setupPrimary();\n    void setupSecondary();\n\n    static QString computeId();\n\n    QString m_id;\n\n    bool m_isPrimary = false;\n    qint64 m_primaryPid = 0;\n\n    QSharedMemory *m_sharedMemory = nullptr;\n    QLocalServer *m_localServer = nullptr;\n};\n\n} // namespace Core\n} // namespace Zeal\n\n#endif // ZEAL_CORE_APPLICATIONSINGLETON_H\n"
  },
  {
    "path": "src/libs/core/extractor.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"extractor.h\"\n\n#include <QDir>\n#include <QLoggingCategory>\n\n#include <archive.h>\n#include <archive_entry.h>\n\n#include <sys/stat.h>\n\nusing namespace Zeal::Core;\n\nstatic Q_LOGGING_CATEGORY(log, \"zeal.core.extractor\")\n\nExtractor::Extractor(QObject *parent)\n    : QObject(parent)\n{\n}\n\nvoid Extractor::extract(const QString &sourceFile, const QString &destination, const QString &root)\n{\n    ExtractInfo info = {\n        archive_read_new(),           // archiveHandle\n        sourceFile,                   // filePath\n        QFileInfo(sourceFile).size(), // totalBytes\n        0                             // extractedBytes\n    };\n\n    archive_read_support_filter_all(info.archiveHandle);\n    archive_read_support_format_all(info.archiveHandle);\n\n    int r = archive_read_open_filename(info.archiveHandle, qPrintable(sourceFile), 10240);\n    if (r) {\n        emit error(sourceFile, QString::fromLocal8Bit(archive_error_string(info.archiveHandle)));\n        archive_read_free(info.archiveHandle);\n\n        return;\n    }\n\n    QDir destinationDir(destination);\n    if (!root.isEmpty()) {\n        destinationDir.setPath(destinationDir.filePath(root));\n    }\n\n    // Destination directory must be created before any other files.\n    destinationDir.mkpath(QLatin1String(\".\"));\n\n    // TODO: Do not strip root directory in archive if it equals to 'root'\n    archive_entry *entry;\n    while (archive_read_next_header(info.archiveHandle, &entry) == ARCHIVE_OK) {\n        // See https://github.com/libarchive/libarchive/issues/587 for more on UTF-8.\n        QString pathname = QString::fromUtf8(archive_entry_pathname_utf8(entry));\n\n        if (!root.isEmpty()) {\n            pathname.remove(0, pathname.indexOf(QLatin1String(\"/\")) + 1);\n        }\n\n        const QString filePath = destinationDir.absoluteFilePath(pathname);\n\n        const auto filetype = archive_entry_filetype(entry);\n        if (filetype == S_IFDIR) {\n            QDir().mkpath(QFileInfo(filePath).absolutePath());\n            continue;\n        }\n\n        if (filetype != S_IFREG) {\n            qCWarning(log, \"Unsupported filetype %d at '%s'.\", filetype, qPrintable(pathname));\n            continue;\n        }\n\n        QScopedPointer<QFile> file(new QFile(filePath));\n        if (!file->open(QIODevice::WriteOnly)) {\n            qCWarning(log, \"Cannot open file for writing at '%s'.\", qPrintable(pathname));\n            continue;\n        }\n\n        const void *buffer;\n        size_t size;\n        std::int64_t offset;\n        for (;;) {\n            int rc = archive_read_data_block(info.archiveHandle, &buffer, &size, &offset);\n            if (rc != ARCHIVE_OK) {\n                if (rc == ARCHIVE_EOF) {\n                    break;\n                }\n\n                qCWarning(log, \"Cannot read from archive: %s.\", archive_error_string(info.archiveHandle));\n                emit error(sourceFile, QString::fromLocal8Bit(archive_error_string(info.archiveHandle)));\n\n                archive_read_free(info.archiveHandle);\n\n                return;\n            }\n\n            file->write(static_cast<const char *>(buffer), size);\n        }\n\n        emitProgress(info);\n    }\n\n    emit completed(sourceFile);\n    archive_read_free(info.archiveHandle);\n}\n\nvoid Extractor::emitProgress(ExtractInfo &info)\n{\n    const qint64 extractedBytes = archive_filter_bytes(info.archiveHandle, -1);\n    if (extractedBytes == info.extractedBytes) {\n        return;\n    }\n\n    info.extractedBytes = extractedBytes;\n\n    emit progress(info.filePath, extractedBytes, info.totalBytes);\n}\n"
  },
  {
    "path": "src/libs/core/extractor.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_CORE_EXTRACTOR_H\n#define ZEAL_CORE_EXTRACTOR_H\n\n#include <QObject>\n\nstruct archive;\n\nnamespace Zeal {\nnamespace Core {\n\nclass Extractor final : public QObject\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(Extractor)\npublic:\n    explicit Extractor(QObject *parent = nullptr);\n\npublic slots:\n    void extract(const QString &sourceFile,\n                 const QString &destination,\n                 const QString &root = QString());\n\nsignals:\n    void error(const QString &filePath, const QString &message);\n    void completed(const QString &filePath);\n    void progress(const QString &filePath, qint64 extracted, qint64 total);\n\nprivate:\n    struct ExtractInfo {\n        archive *archiveHandle;\n        QString filePath;\n        qint64 totalBytes;\n        qint64 extractedBytes;\n    };\n\n    void emitProgress(ExtractInfo &info);\n};\n\n} // namespace Core\n} // namespace Zeal\n\n#endif // ZEAL_CORE_EXTRACTOR_H\n"
  },
  {
    "path": "src/libs/core/filemanager.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"filemanager.h\"\n\n#include \"application.h\"\n\n#include <QCoreApplication>\n#include <QDateTime>\n#include <QDir>\n#include <QFileInfo>\n#include <QFutureWatcher>\n#include <QLoggingCategory>\n\n#include <future>\n\nusing namespace Zeal::Core;\n\nstatic Q_LOGGING_CATEGORY(log, \"zeal.core.filemanager\")\n\nFileManager::FileManager(QObject *parent)\n    : QObject(parent)\n{\n    // Ensure that cache location exists.\n    if (!QDir().mkpath(Application::cacheLocation())) {\n        qCWarning(log, \"Failed to create cache directory '%s'.\", qPrintable(Application::cacheLocation()));\n    }\n}\n\nbool FileManager::removeRecursively(const QString &path)\n{\n    qCDebug(log, \"Removing '%s'...\", qPrintable(path));\n\n    if (!QFileInfo(path).isDir()) {\n        qCWarning(log, \"'%s' is not a directory.\", qPrintable(path));\n        return false;\n    }\n\n    const QString deletePath = QStringLiteral(\"%1.%2.deleteme\")\n            .arg(path, QString::number(QDateTime::currentMSecsSinceEpoch()));\n\n    if (!QDir().rename(path, deletePath)) {\n        qCWarning(log, \"Failed to rename '%s' to '%s'.\", qPrintable(path), qPrintable(deletePath));\n        return false;\n    }\n\n    qCDebug(log, \"Renamed '%s' to '%s'.\", qPrintable(path), qPrintable(deletePath));\n\n    std::future<bool> f = std::async(std::launch::async, [deletePath](){\n        return QDir(deletePath).removeRecursively();\n    });\n\n    f.wait();\n\n    if (!f.get()) {\n        qCWarning(log, \"Failed to remove '%s'.\", qPrintable(deletePath));\n    } else {\n        qCDebug(log, \"Removed '%s'.\", qPrintable(deletePath));\n    }\n\n    return true;\n}\n"
  },
  {
    "path": "src/libs/core/filemanager.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_CORE_FILEMANAGER_H\n#define ZEAL_CORE_FILEMANAGER_H\n\n#include <QObject>\n\nnamespace Zeal {\nnamespace Core {\n\nclass FileManager final : public QObject\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(FileManager)\npublic:\n    explicit FileManager(QObject *parent = nullptr);\n\n    bool removeRecursively(const QString &path);\n};\n\n} // namespace Core\n} // namespace Zeal\n\n#endif // ZEAL_CORE_FILEMANAGER_H\n"
  },
  {
    "path": "src/libs/core/httpserver.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"httpserver.h\"\n\n#include \"application.h\"\n\n#include <httplib.h>\n\n#include <QLoggingCategory>\n#include <QRegularExpression>\n\nusing namespace Zeal::Core;\n\nnamespace {\nconstexpr char LocalHttpServerHost[] = \"127.0.0.1\"; // macOS only routes 127.0.0.1 by default.\n} // namespace\n\nstatic Q_LOGGING_CATEGORY(log, \"zeal.core.httpserver\")\n\nHttpServer::HttpServer(QObject *parent)\n    : QObject(parent)\n{\n    m_server = std::make_unique<httplib::Server>();\n\n    const int port = m_server->bind_to_any_port(LocalHttpServerHost);\n\n    m_baseUrl.setScheme(QStringLiteral(\"http\"));\n    m_baseUrl.setHost(LocalHttpServerHost);\n    m_baseUrl.setPort(port);\n\n    m_server->set_error_handler([](const auto& req, auto& res) {\n        const QString html = QStringLiteral(\"<b>ERROR %1</b><br><pre>Request path: %2</pre>\")\n                .arg(res.status)\n                .arg(QString::fromStdString(req.path));\n        res.set_content(html.toUtf8().data(), \"text/html\");\n    });\n\n    m_future = std::async(std::launch::async, &httplib::Server::listen_after_bind, m_server.get());\n\n    qCDebug(log, \"Listening on %s...\", qPrintable(m_baseUrl.toString()));\n}\n\nHttpServer::~HttpServer()\n{\n    m_server->stop();\n\n    auto status = m_future.wait_for(std::chrono::seconds(2));\n    if (status != std::future_status::ready) {\n        qCWarning(log) << \"Failed to stop server within timeout.\";\n    }\n}\n\nQUrl HttpServer::baseUrl() const\n{\n    return m_baseUrl;\n}\n\nQUrl HttpServer::mount(const QString &prefix, const QString &path)\n{\n    const QString pfx = sanitizePrefix(prefix);\n    const bool ok = m_server->set_mount_point(pfx.toStdString(), path.toStdString());\n    if (!ok) {\n        qCWarning(log, \"Failed to mount '%s' to '%s'.\", qPrintable(path), qPrintable(pfx));\n        return QUrl();\n    }\n\n    qCDebug(log, \"Mounted '%s' to '%s'.\", qPrintable(path), qPrintable(pfx));\n\n    QUrl mountUrl = m_baseUrl;\n    mountUrl.setPath(m_baseUrl.path() + pfx);\n    return mountUrl;\n}\n\nbool HttpServer::unmount(const QString &prefix)\n{\n    const QString pfx = sanitizePrefix(prefix);\n    const bool ok = m_server->remove_mount_point(pfx.toStdString());\n    if (!ok) {\n        qCWarning(log, \"Failed to unmount '%s' to '%s'.\", qPrintable(prefix), qPrintable(pfx));\n    }\n\n    qCDebug(log, \"Unmounted prefix '%s' ('%s').\", qPrintable(prefix), qPrintable(pfx));\n\n    return ok;\n}\n\nQString HttpServer::sanitizePrefix(const QString &prefix)\n{\n    QString pfx = (prefix.startsWith(QLatin1String(\"/\")) ? prefix.right(1) : prefix).toLower();\n    pfx.replace(QRegularExpression(QStringLiteral(\"[^a-zA-Z0-9-_]\")), QStringLiteral(\"_\"));\n    pfx.prepend(QLatin1Char('/'));\n\n    return pfx;\n}\n"
  },
  {
    "path": "src/libs/core/httpserver.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_CORE_HTTPSERVER_H\n#define ZEAL_CORE_HTTPSERVER_H\n\n#include <QObject>\n#include <QUrl>\n\n#include <future>\n#include <memory>\n\nnamespace httplib {\nclass Server;\n} // namespace httplib\n\nnamespace Zeal {\nnamespace Core {\n\nclass HttpServer : public QObject\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(HttpServer)\npublic:\n    explicit HttpServer(QObject *parent = nullptr);\n    ~HttpServer() override;\n\n    QUrl baseUrl() const;\n\n    QUrl mount(const QString &prefix, const QString &path);\n    bool unmount(const QString &prefix);\n\nprivate:\n    static QString sanitizePrefix(const QString &prefix);\n\n    std::unique_ptr<httplib::Server> m_server;\n    std::future<bool> m_future;\n\n    QUrl m_baseUrl;\n};\n\n} // namespace Core\n} // namespace Zeal\n\n#endif // ZEAL_CORE_HTTPSERVER_H\n"
  },
  {
    "path": "src/libs/core/networkaccessmanager.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"networkaccessmanager.h\"\n\n#include \"application.h\"\n#include \"httpserver.h\"\n\n#include <QNetworkRequest>\n\nusing namespace Zeal::Core;\n\nNetworkAccessManager::NetworkAccessManager(QObject *parent)\n    : QNetworkAccessManager(parent)\n{\n}\n\nbool NetworkAccessManager::isLocalFile(const QUrl &url)\n{\n    return url.isLocalFile() || url.scheme() == QLatin1String(\"qrc\");\n}\n\nbool NetworkAccessManager::isLocalUrl(const QUrl &url)\n{\n    if (isLocalFile(url)) {\n        return true;\n    }\n\n    const QUrl &baseUrl = Application::instance()->httpServer()->baseUrl();\n    if (baseUrl.isParentOf(url)) {\n        return true;\n    }\n\n    return false;\n}\n\nQNetworkReply *NetworkAccessManager::createRequest(QNetworkAccessManager::Operation op,\n                                                   const QNetworkRequest &request,\n                                                   QIODevice *outgoingData)\n{\n    QNetworkRequest overrideRequest(request);\n    overrideRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, true);\n\n    // Forward all non-local schemaless URLs via HTTPS.\n    const QUrl url = request.url();\n    if (isLocalFile(url) && !url.host().isEmpty()) {\n        QUrl overrideUrl(url);\n        overrideUrl.setScheme(QStringLiteral(\"https\"));\n\n        overrideRequest.setUrl(overrideUrl);\n\n        op = QNetworkAccessManager::GetOperation;\n    }\n\n    QSslConfiguration sslConfig = overrideRequest.sslConfiguration();\n    sslConfig.setCaCertificates(QSslConfiguration::systemCaCertificates());\n    overrideRequest.setSslConfiguration(sslConfig);\n\n    return QNetworkAccessManager::createRequest(op, overrideRequest, outgoingData);\n}\n"
  },
  {
    "path": "src/libs/core/networkaccessmanager.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_CORE_NETWORKACCESSMANAGER_H\n#define ZEAL_CORE_NETWORKACCESSMANAGER_H\n\n#include <QNetworkAccessManager>\n\nnamespace Zeal {\nnamespace Core {\n\nclass NetworkAccessManager final : public QNetworkAccessManager\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(NetworkAccessManager)\npublic:\n    NetworkAccessManager(QObject *parent = nullptr);\n\n    static bool isLocalFile(const QUrl &url);\n    static bool isLocalUrl(const QUrl &url);\n\nprotected:\n    QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,\n                                 QIODevice *outgoingData = nullptr) override;\n};\n\n} // namespace Core\n} // namespace Zeal\n\n#endif // ZEAL_CORE_NETWORKACCESSMANAGER_H\n"
  },
  {
    "path": "src/libs/core/settings.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"settings.h\"\n\n#include \"application.h\"\n\n#include <QApplication>\n#include <QDir>\n#include <QFileInfo>\n#include <QLoggingCategory>\n#include <QSettings>\n#include <QStandardPaths>\n#include <QUrl>\n#include <QUuid>\n#include <QWebEngineProfile>\n#include <QWebEngineSettings>\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n#include <QStyle>\n#include <QStyleFactory>\n#include <QStyleHints>\n#else\n#include <QPalette>\n#endif\n\nnamespace {\n// Configuration file groups\nconstexpr char GroupUI[] = \"ui\";\nconstexpr char GroupContent[] = \"content\";\nconstexpr char GroupDocsets[] = \"docsets\";\nconstexpr char GroupGlobalShortcuts[] = \"global_shortcuts\";\nconstexpr char GroupSearch[] = \"search\";\nconstexpr char GroupTabs[] = \"tabs\";\nconstexpr char GroupInternal[] = \"internal\";\nconstexpr char GroupState[] = \"state\";\nconstexpr char GroupProxy[] = \"proxy\";\n} // namespace\n\nusing namespace Zeal::Core;\n\nstatic Q_LOGGING_CATEGORY(log, \"zeal.core.settings\")\n\nSettings::Settings(QObject *parent)\n    : QObject(parent)\n{\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n    qRegisterMetaTypeStreamOperators<ContentAppearance>(\"ContentAppearance\");\n    qRegisterMetaTypeStreamOperators<ExternalLinkPolicy>(\"ExternalLinkPolicy\");\n#else\n    qRegisterMetaType<ContentAppearance>(\"ContentAppearance\");\n    qRegisterMetaType<ExternalLinkPolicy>(\"ExternalLinkPolicy\");\n#endif\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n    // When the OS color scheme changes, reapply the color scheme.\n    connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged,\n            this, [this]() {\n        if (contentAppearance == ContentAppearance::Automatic) {\n            applyColorScheme();\n        }\n    });\n#endif\n\n    load();\n}\n\nSettings::~Settings()\n{\n    save();\n}\n\nbool Settings::isDarkModeEnabled() const\n{\n    if (contentAppearance == ContentAppearance::Dark) {\n        return true;\n    }\n\n    if (contentAppearance == ContentAppearance::Automatic && colorScheme() == ColorScheme::Dark) {\n        return true;\n    }\n\n    return false;\n}\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\nvoid Settings::applyColorScheme()\n{\n    Qt::ColorScheme scheme = Qt::ColorScheme::Unknown;\n    switch (contentAppearance) {\n    case ContentAppearance::Light:\n        scheme = Qt::ColorScheme::Light;\n        break;\n    case ContentAppearance::Dark:\n        scheme = Qt::ColorScheme::Dark;\n        break;\n    default:\n        break;\n    }\n\n    qApp->styleHints()->setColorScheme(scheme);\n    // setColorScheme() alone doesn't reliably update existing widgets:\n    //  - Widgets with stylesheets (QStyleSheetStyle) only update on polish().\n    //  - Direct palette consumers (QTreeView, QLineEdit) only update when the\n    //    application palette changes.\n    // Re-instantiating the style triggers the full unpolish-polish cycle for\n    // QStyleSheetStyle widgets and also updates the application palette via\n    // the new style's standardPalette().\n    QStyle *newStyle = QStyleFactory::create(qApp->style()->name());\n    if (newStyle != nullptr) {\n        qApp->setStyle(newStyle);\n    }\n}\n#endif\n\nSettings::ColorScheme Settings::colorScheme()\n{\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n    return QApplication::styleHints()->colorScheme();\n#else\n    // Pre Qt 6.5 detection from https://www.qt.io/blog/dark-mode-on-windows-11-with-qt-6.5.\n    const QPalette p;\n    if (p.color(QPalette::WindowText).lightness() > p.color(QPalette::Window).lightness()) {\n        return ColorScheme::Dark;\n    }\n\n    return ColorScheme::Light;\n#endif\n}\n\nvoid Settings::load()\n{\n    QScopedPointer<QSettings> settings(qsettings());\n    migrate(settings.data());\n\n    // TODO: Put everything in groups\n    startMinimized = settings->value(QStringLiteral(\"start_minimized\"), false).toBool();\n    checkForUpdate = settings->value(QStringLiteral(\"check_for_update\"), true).toBool();\n\n    showSystrayIcon = settings->value(QStringLiteral(\"show_systray_icon\"), true).toBool();\n    minimizeToSystray = settings->value(QStringLiteral(\"minimize_to_systray\"), false).toBool();\n    hideOnClose = settings->value(QStringLiteral(\"hide_on_close\"), false).toBool();\n\n    settings->beginGroup(GroupUI);\n    hideMenuBar = settings->value(QStringLiteral(\"hide_menu_bar\"), false).toBool();\n    settings->endGroup();\n\n    settings->beginGroup(GroupGlobalShortcuts);\n    showShortcut = settings->value(QStringLiteral(\"show\")).value<QKeySequence>();\n    settings->endGroup();\n\n    settings->beginGroup(GroupTabs);\n    openNewTabAfterActive = settings->value(QStringLiteral(\"open_new_tab_after_active\"), false).toBool();\n    settings->endGroup();\n\n    settings->beginGroup(GroupSearch);\n    isFuzzySearchEnabled = settings->value(QStringLiteral(\"fuzzy_search_enabled\"), false).toBool();\n    settings->endGroup();\n\n    settings->beginGroup(GroupContent);\n\n    contentAppearance = settings->value(QStringLiteral(\"appearance\"),\n                                        QVariant::fromValue(ContentAppearance::Automatic)).value<ContentAppearance>();\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)\n    // Dark mode needs to be applied before Qt WebEngine is initialized.\n    if (isDarkModeEnabled()) {\n        qputenv(\"QTWEBENGINE_CHROMIUM_FLAGS\", \"--blink-settings=forceDarkModeEnabled=true,darkModeInversionAlgorithm=4\");\n    }\n#endif\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n    applyColorScheme();\n#endif\n\n    // Fonts\n    QWebEngineSettings *webSettings = QWebEngineProfile::defaultProfile()->settings();\n    serifFontFamily = settings->value(QStringLiteral(\"serif_font_family\"),\n                                      webSettings->fontFamily(QWebEngineSettings::SerifFont)).toString();\n    sansSerifFontFamily = settings->value(QStringLiteral(\"sans_serif_font_family\"),\n                                          webSettings->fontFamily(QWebEngineSettings::SansSerifFont)).toString();\n    fixedFontFamily = settings->value(QStringLiteral(\"fixed_font_family\"),\n                                      webSettings->fontFamily(QWebEngineSettings::FixedFont)).toString();\n\n    static const QMap<QString, QWebEngineSettings::FontFamily> fontFamilies = {\n        {QStringLiteral(\"sans-serif\"), QWebEngineSettings::SansSerifFont},\n        {QStringLiteral(\"serif\"), QWebEngineSettings::SerifFont},\n        {QStringLiteral(\"monospace\"), QWebEngineSettings::FixedFont}\n    };\n\n    defaultFontFamily = settings->value(QStringLiteral(\"default_font_family\"),\n                                        QStringLiteral(\"serif\")).toString();\n\n    // Fallback to the serif font family.\n    if (!fontFamilies.contains(defaultFontFamily)) {\n        defaultFontFamily = QStringLiteral(\"serif\");\n    }\n\n    webSettings->setFontFamily(QWebEngineSettings::SansSerifFont, sansSerifFontFamily);\n    webSettings->setFontFamily(QWebEngineSettings::SerifFont, serifFontFamily);\n    webSettings->setFontFamily(QWebEngineSettings::FixedFont, fixedFontFamily);\n\n    const QString defaultFontFamilyResolved = webSettings->fontFamily(fontFamilies.value(defaultFontFamily));\n    webSettings->setFontFamily(QWebEngineSettings::StandardFont, defaultFontFamilyResolved);\n\n    defaultFontSize = settings->value(QStringLiteral(\"default_font_size\"),\n                                      webSettings->fontSize(QWebEngineSettings::DefaultFontSize)).toInt();\n    defaultFixedFontSize = settings->value(QStringLiteral(\"default_fixed_font_size\"),\n                                           webSettings->fontSize(QWebEngineSettings::DefaultFixedFontSize)).toInt();\n    minimumFontSize = settings->value(QStringLiteral(\"minimum_font_size\"),\n                                      webSettings->fontSize(QWebEngineSettings::MinimumFontSize)).toInt();\n\n    webSettings->setFontSize(QWebEngineSettings::DefaultFontSize, defaultFontSize);\n    webSettings->setFontSize(QWebEngineSettings::DefaultFixedFontSize, defaultFixedFontSize);\n    webSettings->setFontSize(QWebEngineSettings::MinimumFontSize, minimumFontSize);\n\n    isHighlightOnNavigateEnabled = settings->value(QStringLiteral(\"highlight_on_navigate\"), true).toBool();\n    customCssFile = settings->value(QStringLiteral(\"custom_css_file\")).toString();\n    externalLinkPolicy = settings->value(QStringLiteral(\"external_link_policy\"),\n                                         QVariant::fromValue(ExternalLinkPolicy::Ask)).value<ExternalLinkPolicy>();\n    isSmoothScrollingEnabled = settings->value(QStringLiteral(\"smooth_scrolling\"), true).toBool();\n    settings->endGroup();\n\n    settings->beginGroup(GroupProxy);\n    proxyType = static_cast<ProxyType>(settings->value(QStringLiteral(\"type\"),\n                                                       ProxyType::System).toUInt());\n    proxyHost = settings->value(QStringLiteral(\"host\")).toString();\n    proxyPort = static_cast<quint16>(settings->value(QStringLiteral(\"port\"), 0).toUInt());\n    proxyAuthenticate = settings->value(QStringLiteral(\"authenticate\"), false).toBool();\n    proxyUserName = settings->value(QStringLiteral(\"username\")).toString();\n    proxyPassword = settings->value(QStringLiteral(\"password\")).toString();\n    isIgnoreSslErrorsEnabled = settings->value(QStringLiteral(\"ignore_ssl_errors\"), false).toBool();\n    settings->endGroup();\n\n    settings->beginGroup(GroupDocsets);\n    if (settings->contains(QStringLiteral(\"path\"))) {\n        docsetPath = settings->value(QStringLiteral(\"path\")).toString();\n    } else {\n#ifndef PORTABLE_BUILD\n        docsetPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)\n                + QLatin1String(\"/docsets\");\n#else\n        docsetPath = QStringLiteral(\"docsets\");\n#endif\n    }\n    settings->endGroup();\n\n    // Create the docset storage directory if it doesn't exist.\n    const QFileInfo fi(docsetPath);\n    if (!fi.exists()) {\n        const QString path = fi.isRelative()\n                ? QCoreApplication::applicationDirPath() + QLatin1String(\"/\") + docsetPath\n                : docsetPath;\n        if (!QDir().mkpath(path)) {\n            qCWarning(log, \"Failed to create docset storage directory '%s'.\", qPrintable(path));\n        }\n    }\n\n    settings->beginGroup(GroupState);\n    windowGeometry = settings->value(QStringLiteral(\"window_geometry\")).toByteArray();\n    verticalSplitterGeometry = settings->value(QStringLiteral(\"splitter_geometry\")).toByteArray();\n    tocSplitterState = settings->value(QStringLiteral(\"toc_splitter_state\")).toByteArray();\n    settings->endGroup();\n\n    settings->beginGroup(GroupInternal);\n    installId = settings->value(QStringLiteral(\"install_id\"),\n                                // Avoid curly braces (QTBUG-885)\n                                QUuid::createUuid().toString().mid(1, 36)).toString();\n    settings->endGroup();\n}\n\nvoid Settings::save()\n{\n    QScopedPointer<QSettings> settings(qsettings());\n\n    // TODO: Put everything in groups\n    settings->setValue(QStringLiteral(\"start_minimized\"), startMinimized);\n    settings->setValue(QStringLiteral(\"check_for_update\"), checkForUpdate);\n\n    settings->setValue(QStringLiteral(\"show_systray_icon\"), showSystrayIcon);\n    settings->setValue(QStringLiteral(\"minimize_to_systray\"), minimizeToSystray);\n    settings->setValue(QStringLiteral(\"hide_on_close\"), hideOnClose);\n\n    settings->beginGroup(GroupUI);\n    settings->setValue(QStringLiteral(\"hide_menu_bar\"), hideMenuBar);\n    settings->endGroup();\n\n    settings->beginGroup(GroupGlobalShortcuts);\n    settings->setValue(QStringLiteral(\"show\"), showShortcut);\n    settings->endGroup();\n\n    settings->beginGroup(GroupTabs);\n    settings->setValue(QStringLiteral(\"open_new_tab_after_active\"), openNewTabAfterActive);\n    settings->endGroup();\n\n    settings->beginGroup(GroupSearch);\n    settings->setValue(QStringLiteral(\"fuzzy_search_enabled\"), isFuzzySearchEnabled);\n    settings->endGroup();\n\n    settings->beginGroup(GroupContent);\n    settings->setValue(QStringLiteral(\"default_font_family\"), defaultFontFamily);\n    settings->setValue(QStringLiteral(\"serif_font_family\"), serifFontFamily);\n    settings->setValue(QStringLiteral(\"sans_serif_font_family\"), sansSerifFontFamily);\n    settings->setValue(QStringLiteral(\"fixed_font_family\"), fixedFontFamily);\n\n    settings->setValue(QStringLiteral(\"default_font_size\"), defaultFontSize);\n    settings->setValue(QStringLiteral(\"default_fixed_font_size\"), defaultFixedFontSize);\n    settings->setValue(QStringLiteral(\"minimum_font_size\"), minimumFontSize);\n\n    settings->setValue(QStringLiteral(\"appearance\"), QVariant::fromValue(contentAppearance));\n    settings->setValue(QStringLiteral(\"highlight_on_navigate\"), isHighlightOnNavigateEnabled);\n    settings->setValue(QStringLiteral(\"custom_css_file\"), customCssFile);\n    settings->setValue(QStringLiteral(\"external_link_policy\"), QVariant::fromValue(externalLinkPolicy));\n    settings->setValue(QStringLiteral(\"smooth_scrolling\"), isSmoothScrollingEnabled);\n    settings->endGroup();\n\n    settings->beginGroup(GroupProxy);\n    settings->setValue(QStringLiteral(\"type\"), proxyType);\n    settings->setValue(QStringLiteral(\"host\"), proxyHost);\n    settings->setValue(QStringLiteral(\"port\"), proxyPort);\n    settings->setValue(QStringLiteral(\"authenticate\"), proxyAuthenticate);\n    settings->setValue(QStringLiteral(\"username\"), proxyUserName);\n    settings->setValue(QStringLiteral(\"password\"), proxyPassword);\n    settings->setValue(QStringLiteral(\"ignore_ssl_errors\"), isIgnoreSslErrorsEnabled);\n    settings->endGroup();\n\n    settings->beginGroup(GroupDocsets);\n    settings->setValue(QStringLiteral(\"path\"), docsetPath);\n    settings->endGroup();\n\n    settings->beginGroup(GroupState);\n    settings->setValue(QStringLiteral(\"window_geometry\"), windowGeometry);\n    settings->setValue(QStringLiteral(\"splitter_geometry\"), verticalSplitterGeometry);\n    settings->setValue(QStringLiteral(\"toc_splitter_state\"), tocSplitterState);\n    settings->endGroup();\n\n    settings->beginGroup(GroupInternal);\n    settings->setValue(QStringLiteral(\"install_id\"), installId);\n    // Version of configuration file format, should match Zeal version. Used for migration rules.\n    settings->setValue(QStringLiteral(\"version\"), Application::version().toString());\n    settings->endGroup();\n\n    settings->sync();\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n    applyColorScheme();\n#endif\n\n    emit updated();\n}\n\n/*!\n * \\internal\n * \\brief Migrates settings from older application versions.\n * \\param settings QSettings object to update.\n *\n * The settings migration process relies on 'internal/version' option, that was introduced in the\n * release 0.2.0, so a missing option indicates pre-0.2 release.\n */\nvoid Settings::migrate(QSettings *settings) const\n{\n    settings->beginGroup(GroupInternal);\n    const auto version = QVersionNumber::fromString(settings->value(QStringLiteral(\"version\")).toString());\n    settings->endGroup();\n\n    //\n    // 0.6.0\n    //\n\n    // Unset content.default_fixed_font_size.\n    // The causing bug was 0.6.1 (#903), but the incorrect setting still comes to haunt us (#1054).\n    if (version == QVersionNumber(0, 6, 0)) {\n        settings->beginGroup(GroupContent);\n        settings->remove(QStringLiteral(\"default_fixed_font_size\"));\n        settings->endGroup();\n    }\n\n    //\n    // Pre 0.4\n    //\n\n    // Rename 'browser' group into 'content'.\n    if (version < QVersionNumber(0, 4, 0)) {\n        settings->beginGroup(QStringLiteral(\"browser\"));\n        const QVariant tmpMinimumFontSize = settings->value(QStringLiteral(\"minimum_font_size\"));\n        settings->endGroup();\n        settings->remove(QStringLiteral(\"browser\"));\n\n        if (tmpMinimumFontSize.isValid()) {\n            settings->beginGroup(GroupContent);\n            settings->setValue(QStringLiteral(\"minimum_font_size\"), tmpMinimumFontSize);\n            settings->endGroup();\n        }\n    }\n\n    //\n    // Pre 0.3\n    //\n\n    // Unset 'state/splitter_geometry', because custom styles were removed.\n    if (version < QVersionNumber(0, 3, 0)) {\n        settings->beginGroup(GroupState);\n        settings->remove(QStringLiteral(\"splitter_geometry\"));\n        settings->endGroup();\n    }\n}\n\n/*!\n * \\internal\n * \\brief Returns an initialized QSettings object.\n * \\param parent Optional parent object.\n * \\return QSettings object.\n *\n * QSettings is initialized according to build options, e.g. standard vs portable.\n * Caller is responsible for deleting the returned object.\n */\nQSettings *Settings::qsettings(QObject *parent)\n{\n#ifndef PORTABLE_BUILD\n    return new QSettings(parent);\n#else\n    return new QSettings(QCoreApplication::applicationDirPath() + QLatin1String(\"/zeal.ini\"),\n                         QSettings::IniFormat, parent);\n#endif\n}\n\nQDataStream &operator<<(QDataStream &out, Settings::ContentAppearance policy)\n{\n    out << static_cast<std::underlying_type_t<Settings::ContentAppearance>>(policy);\n    return out;\n}\n\nQDataStream &operator>>(QDataStream &in, Settings::ContentAppearance &policy)\n{\n    std::underlying_type_t<Settings::ContentAppearance> value;\n    in >> value;\n    policy = static_cast<Settings::ContentAppearance>(value);\n    return in;\n}\n\nQDataStream &operator<<(QDataStream &out, Settings::ExternalLinkPolicy policy)\n{\n    out << static_cast<std::underlying_type_t<Settings::ExternalLinkPolicy>>(policy);\n    return out;\n}\n\nQDataStream &operator>>(QDataStream &in, Settings::ExternalLinkPolicy &policy)\n{\n    std::underlying_type_t<Settings::ExternalLinkPolicy> value;\n    in >> value;\n    policy = static_cast<Settings::ExternalLinkPolicy>(value);\n    return in;\n}\n"
  },
  {
    "path": "src/libs/core/settings.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_CORE_SETTINGS_H\n#define ZEAL_CORE_SETTINGS_H\n\n#include <QDataStream>\n#include <QKeySequence>\n#include <QObject>\n\nclass QSettings;\n\nnamespace Zeal {\nnamespace Core {\n\nclass Settings final : public QObject\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(Settings)\npublic:\n    /* This public members are here just for simplification and should go away\n     * once a more advanced settings management come in place.\n     */\n\n    // Startup\n    bool startMinimized;\n    bool checkForUpdate;\n    bool hideMenuBar;\n    // TODO: bool restoreLastState;\n\n    // System Tray\n    bool showSystrayIcon;\n    bool minimizeToSystray;\n    bool hideOnClose;\n\n    // Global Shortcuts\n    QKeySequence showShortcut;\n    // TODO: QKeySequence searchSelectedTextShortcut;\n\n    // Tabs Behavior\n    bool openNewTabAfterActive;\n\n    // Search\n    bool isFuzzySearchEnabled;\n\n    // Content\n    QString defaultFontFamily;\n    QString serifFontFamily;\n    QString sansSerifFontFamily;\n    QString fixedFontFamily;\n\n    int defaultFontSize;\n    int defaultFixedFontSize;\n    int minimumFontSize;\n\n    enum class ExternalLinkPolicy : unsigned int {\n        Ask = 0,\n        Open,\n        OpenInSystemBrowser\n    };\n    Q_ENUM(ExternalLinkPolicy)\n    ExternalLinkPolicy externalLinkPolicy = ExternalLinkPolicy::Ask;\n\n    enum class ContentAppearance : unsigned int {\n        Automatic = 0,\n        Light,\n        Dark\n    };\n    Q_ENUM(ContentAppearance)\n    ContentAppearance contentAppearance = ContentAppearance::Automatic;\n\n    bool isHighlightOnNavigateEnabled;\n    QString customCssFile;\n    bool isSmoothScrollingEnabled;\n\n    // Network\n    enum ProxyType : unsigned int {\n        None = 0,\n        System = 1,\n        Http = 3,\n        Socks5 = 4\n    };\n    Q_ENUM(ProxyType)\n\n    // Internal\n    // --------\n    // InstallId is a UUID used to identify a Zeal installation. Created on first start or after\n    // a settings wipe. It is not attached to user hardware or software, and is sent exclusively\n    // to *.zealdocs.org hosts.\n    QString installId;\n\n    ProxyType proxyType = ProxyType::System;\n    QString proxyHost;\n    quint16 proxyPort;\n    bool proxyAuthenticate;\n    QString proxyUserName;\n    QString proxyPassword;\n    bool isIgnoreSslErrorsEnabled;\n\n    // Other\n    QString docsetPath;\n\n    // State\n    QByteArray windowGeometry;\n    QByteArray verticalSplitterGeometry;\n    QByteArray tocSplitterState;\n\n    explicit Settings(QObject *parent = nullptr);\n    ~Settings() override;\n\n    // Helper functions.\n    bool isDarkModeEnabled() const;\n\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n    typedef Qt::ColorScheme ColorScheme;\n#else\n    enum class ColorScheme {\n        Unknown,\n        Light,\n        Dark,\n    };\n#endif\n\n    static ColorScheme colorScheme();\n\npublic slots:\n    void load();\n    void save();\n\nsignals:\n    void updated();\n\nprivate:\n    void migrate(QSettings *settings) const;\n#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n    void applyColorScheme();\n#endif\n\n    static QSettings *qsettings(QObject *parent = nullptr);\n};\n\n} // namespace Core\n} // namespace Zeal\n\nQDataStream &operator<<(QDataStream &out, Zeal::Core::Settings::ContentAppearance policy);\nQDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::ContentAppearance &policy);\n\nQDataStream &operator<<(QDataStream &out, Zeal::Core::Settings::ExternalLinkPolicy policy);\nQDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::ExternalLinkPolicy &policy);\n\nQ_DECLARE_METATYPE(Zeal::Core::Settings::ContentAppearance)\nQ_DECLARE_METATYPE(Zeal::Core::Settings::ExternalLinkPolicy)\n\n#endif // ZEAL_CORE_SETTINGS_H\n"
  },
  {
    "path": "src/libs/registry/CMakeLists.txt",
    "content": "add_library(Registry STATIC\n    docset.cpp\n    docsetmetadata.cpp\n    docsetregistry.cpp\n    listmodel.cpp\n    searchmodel.cpp\n    searchquery.cpp\n\n    # Show headers without .cpp in Qt Creator.\n    cancellationtoken.h\n    itemdatarole.h\n    searchresult.h\n)\n\nfind_package(Qt${QT_VERSION_MAJOR} COMPONENTS Concurrent Gui Network REQUIRED)\ntarget_link_libraries(Registry PRIVATE Util Qt${QT_VERSION_MAJOR}::Concurrent Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Network)\n"
  },
  {
    "path": "src/libs/registry/cancellationtoken.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2015 Artur Spychaj\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_REGISTRY_CANCELLATIONTOKEN_H\n#define ZEAL_REGISTRY_CANCELLATIONTOKEN_H\n\n#include <atomic>\n\nnamespace Zeal {\nnamespace Registry {\n\n/// Token that stores whether cancel was called on it.\n/// In async code can be used to check if another thread called cancel.\nclass CancellationToken\n{\npublic:\n    inline bool isCanceled() const { return m_canceled; }\n\n    inline void cancel() { m_canceled = true; }\n    inline void reset() { m_canceled = false; }\n\nprivate:\n    std::atomic_bool m_canceled;\n};\n\n} // namespace Registry\n} // namespace Zeal\n\n#endif // ZEAL_REGISTRY_CANCELLATIONTOKEN_H\n"
  },
  {
    "path": "src/libs/registry/docset.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"docset.h\"\n\n#include \"cancellationtoken.h\"\n#include \"searchresult.h\"\n\n#include <util/fuzzy.h>\n#include <util/plist.h>\n#include <util/sqlitedatabase.h>\n\n#include <QDir>\n#include <QFile>\n#include <QJsonArray>\n#include <QJsonDocument>\n#include <QJsonObject>\n#include <QLoggingCategory>\n#include <QRegularExpression>\n#include <QVariant>\n#include <QVarLengthArray>\n\n#include <sqlite3.h>\n\n#include <algorithm>\n#include <cmath>\n#include <cstring>\n#include <utility>\n\nusing namespace Zeal::Registry;\n\nstatic Q_LOGGING_CATEGORY(log, \"zeal.registry.docset\")\n\nnamespace {\nconstexpr char IndexNamePrefix[] = \"__zi_name\"; // zi - Zeal index\nconstexpr char IndexNameVersion[] = \"0001\";     // Current index version\n\nconstexpr char NotFoundPageUrl[] = \"qrc:///browser/404.html\";\n\nnamespace InfoPlist {\nconstexpr char CFBundleName[] = \"CFBundleName\";\n//const char CFBundleIdentifier[] = \"CFBundleIdentifier\";\nconstexpr char DashDocSetFamily[] = \"DashDocSetFamily\";\nconstexpr char DashDocSetKeyword[] = \"DashDocSetKeyword\";\nconstexpr char DashDocSetPluginKeyword[] = \"DashDocSetPluginKeyword\";\nconstexpr char DashIndexFilePath[] = \"dashIndexFilePath\";\nconstexpr char DocSetPlatformFamily[] = \"DocSetPlatformFamily\";\n//const char IsDashDocset[] = \"isDashDocset\";\nconstexpr char IsJavaScriptEnabled[] = \"isJavaScriptEnabled\";\n} // namespace InfoPlist\n} // namespace\n\nstatic void sqliteScoreFunction(sqlite3_context *context, int argc, sqlite3_value **argv);\n\nDocset::Docset(QString path)\n    : m_path(std::move(path))\n{\n    QDir dir(m_path);\n    if (!dir.exists())\n        return;\n\n    loadMetadata();\n\n    // Attempt to find the icon in any supported format\n    const auto iconFiles = dir.entryList({QStringLiteral(\"icon.*\")}, QDir::Files);\n    for (const QString &iconFile : iconFiles) {\n        m_icon = QIcon(dir.filePath(iconFile));\n        if (!m_icon.availableSizes().isEmpty())\n            break;\n    }\n\n    if (!dir.cd(QStringLiteral(\"Contents\"))) {\n        qCWarning(log, \"[%s] Cannot change directory into 'Contents' at '%s'.\", qPrintable(m_name), qPrintable(m_path));\n        return;\n    }\n\n    // TODO: 'info.plist' is invalid according to Apple, and must always be 'Info.plist'\n    // https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPRuntimeConfig\n    // /Articles/ConfigFiles.html\n    Util::Plist plist;\n    if (dir.exists(QStringLiteral(\"Info.plist\")))\n        plist.read(dir.filePath(QStringLiteral(\"Info.plist\")));\n    else if (dir.exists(QStringLiteral(\"info.plist\")))\n        plist.read(dir.filePath(QStringLiteral(\"info.plist\")));\n    else {\n        qCWarning(log, \"Cannot find file 'Info.plist' or 'info.plist' for docset at '%s'.\", qPrintable(m_path));\n        return;\n    }\n\n    if (plist.hasError()) {\n        qCWarning(log, \"Failed to parse 'Info.plist' for docset at '%s'.\", qPrintable(m_path));\n        return;\n    }\n\n    if (m_name.isEmpty()) {\n        // Fallback if meta.json is absent\n        if (plist.contains(InfoPlist::CFBundleName)) {\n            m_name = m_title = plist[InfoPlist::CFBundleName].toString();\n            // TODO: Remove when MainWindow::docsetName() will not use directory name\n            m_name.replace(QLatin1Char(' '), QLatin1Char('_'));\n        } else {\n            m_name = QFileInfo(m_path).fileName().remove(QStringLiteral(\".docset\"));\n        }\n    }\n\n    if (m_title.isEmpty()) {\n        m_title = m_name;\n        m_title.replace(QLatin1Char('_'), QLatin1Char(' '));\n    }\n\n    // TODO: Verify if this is needed\n    if (plist.contains(InfoPlist::DashDocSetFamily)\n            && plist[InfoPlist::DashDocSetFamily].toString() == QLatin1String(\"cheatsheet\")) {\n        m_name = m_name + QLatin1String(\"cheats\");\n    }\n\n    if (!dir.cd(QStringLiteral(\"Resources\"))) {\n        qCWarning(log, \"[%s] Cannot change directory into 'Resources' at '%s'.\", qPrintable(m_name), qPrintable(m_path));\n        return;\n    }\n\n    if (!dir.exists(QStringLiteral(\"docSet.dsidx\"))) {\n        qCWarning(log, \"[%s] Cannot access 'docSet.dsidx' at '%s'.\", qPrintable(m_name), qPrintable(m_path));\n        return;\n    }\n\n    m_db = new Util::SQLiteDatabase(dir.filePath(QStringLiteral(\"docSet.dsidx\")));\n\n    if (!m_db->isOpen()) {\n        qCWarning(log, \"[%s] Cannot open database: %s.\", qPrintable(m_name), qPrintable(m_db->lastError()));\n        return;\n    }\n\n    sqlite3_create_function(m_db->handle(), \"zealScore\", 2, SQLITE_UTF8, nullptr,\n                            sqliteScoreFunction, nullptr, nullptr);\n\n    m_type = m_db->tables().contains(QStringLiteral(\"searchIndex\"), Qt::CaseInsensitive)\n            ? Type::Dash : Type::ZDash;\n\n    createIndex();\n\n    if (m_type == Docset::Type::ZDash) {\n        createView();\n    }\n\n    if (!dir.cd(QStringLiteral(\"Documents\"))) {\n        qCWarning(log, \"[%s] Cannot change directory into 'Documents' at '%s'.\", qPrintable(m_name), qPrintable(m_path));\n        m_type = Type::Invalid;\n        return;\n    }\n\n    // Setup keywords\n    if (plist.contains(InfoPlist::DocSetPlatformFamily))\n        m_keywords << plist[InfoPlist::DocSetPlatformFamily].toString();\n\n    if (plist.contains(InfoPlist::DashDocSetPluginKeyword))\n        m_keywords << plist[InfoPlist::DashDocSetPluginKeyword].toString();\n\n    if (plist.contains(InfoPlist::DashDocSetKeyword))\n        m_keywords << plist[InfoPlist::DashDocSetKeyword].toString();\n\n    if (plist.contains(InfoPlist::DashDocSetFamily)) {\n        const QString kw = plist[InfoPlist::DashDocSetFamily].toString();\n        if (!kw.contains(QLatin1String(\"dashtoc\"))) {\n            m_keywords << kw;\n        }\n    }\n\n    if (plist.contains(InfoPlist::IsJavaScriptEnabled)) {\n        m_isJavaScriptEnabled = plist[InfoPlist::IsJavaScriptEnabled].toBool();\n    }\n\n    m_keywords.removeDuplicates();\n\n    // Determine index page. This is ridiculous.\n    const QString mdIndexFilePath = m_indexFilePath; // Save path from the metadata.\n\n    // Prefer index path provided by the docset.\n    if (plist.contains(InfoPlist::DashIndexFilePath)) {\n        const QString indexFilePath = plist[InfoPlist::DashIndexFilePath].toString();\n        if (dir.exists(indexFilePath)) {\n            m_indexFilePath = indexFilePath;\n        }\n    }\n\n    // Check the metadata.\n    if (m_indexFilePath.isEmpty() && !mdIndexFilePath.isEmpty() && dir.exists(mdIndexFilePath)) {\n        m_indexFilePath = mdIndexFilePath;\n    }\n\n    // What if there is index.html.\n    if (m_indexFilePath.isEmpty() && dir.exists(QStringLiteral(\"index.html\"))) {\n        m_indexFilePath = QStringLiteral(\"index.html\");\n    }\n\n    // Log if unable to determine the index page. Otherwise the path will be set in setBaseUrl().\n    if (m_indexFilePath.isEmpty()) {\n        qCInfo(log, \"[%s] Cannot determine index file.\", qPrintable(m_name));\n        m_indexFileUrl.setUrl(NotFoundPageUrl);\n    } else {\n        m_indexFileUrl = createPageUrl(m_indexFilePath);\n    }\n\n    countSymbols();\n}\n\nDocset::~Docset()\n{\n    delete m_db;\n}\n\nbool Docset::isValid() const\n{\n    return m_type != Type::Invalid;\n}\n\nQString Docset::name() const\n{\n    return m_name;\n}\n\nQString Docset::title() const\n{\n    return m_title;\n}\n\nQStringList Docset::keywords() const\n{\n    return m_keywords;\n}\n\nQString Docset::version() const\n{\n    return m_version;\n}\n\nint Docset::revision() const\n{\n    return m_revision;\n}\n\nQString Docset::feedUrl() const\n{\n    return m_feedUrl;\n}\n\nQString Docset::path() const\n{\n    return m_path;\n}\n\nQString Docset::documentPath() const\n{\n    return QDir(m_path).filePath(QStringLiteral(\"Contents/Resources/Documents\"));\n}\n\nQIcon Docset::icon() const\n{\n    return m_icon;\n}\n\nQIcon Docset::symbolTypeIcon(const QString &symbolType) const\n{\n    static const QIcon unknownIcon(QStringLiteral(\"typeIcon:Unknown.png\"));\n\n    const QIcon icon(QStringLiteral(\"typeIcon:%1.png\").arg(symbolType));\n    return icon.availableSizes().isEmpty() ? unknownIcon : icon;\n}\n\nQUrl Docset::indexFileUrl() const\n{\n    return m_indexFileUrl;\n}\n\nQMap<QString, int> Docset::symbolCounts() const\n{\n    return m_symbolCounts;\n}\n\nint Docset::symbolCount(const QString &symbolType) const\n{\n    return m_symbolCounts.value(symbolType);\n}\n\nconst QMultiMap<QString, QUrl> &Docset::symbols(const QString &symbolType) const\n{\n    if (!m_symbols.contains(symbolType))\n        loadSymbols(symbolType);\n    return m_symbols[symbolType];\n}\n\nQList<SearchResult> Docset::search(const QString &query, const CancellationToken &token) const\n{\n    QString sql;\n    if (m_type == Docset::Type::Dash) {\n        if (m_isFuzzySearchEnabled) {\n            sql = QStringLiteral(\"SELECT name, type, path, '', zealScore('%1', name) as score\"\n                                 \"  FROM searchIndex\"\n                                 \"  WHERE score > 0\"\n                                 \"  ORDER BY score DESC\");\n        } else {\n            sql = QStringLiteral(\"SELECT name, type, path, '', -length(name) as score\"\n                                 \"  FROM searchIndex\"\n                                 \"  WHERE (name LIKE '%%1%' ESCAPE '\\\\')\"\n                                 \"  ORDER BY score DESC\");\n        }\n    } else {\n        if (m_isFuzzySearchEnabled) {\n            sql = QStringLiteral(\"SELECT name, type, path, fragment, zealScore('%1', name) as score\"\n                                 \"  FROM searchIndex\"\n                                 \"  WHERE score > 0\"\n                                 \"  ORDER BY score DESC\");\n        } else {\n            sql = QStringLiteral(\"SELECT name, type, path, fragment, -length(name) as score\"\n                                 \"  FROM searchIndex\"\n                                 \"  WHERE (name LIKE '%%1%' ESCAPE '\\\\')\"\n                                 \"  ORDER BY score DESC\");\n        }\n    }\n\n    // Limit for very short queries.\n    // TODO: Show a notification about the reduced result set.\n    if (query.size() < 3) {\n        sql += QLatin1String(\"  LIMIT 1000\");\n    }\n\n    // Make it safe to use in a SQL query.\n    QString sanitizedQuery = query;\n    sanitizedQuery.replace(QLatin1Char('\\''), QLatin1String(\"''\"));\n    m_db->prepare(sql.arg(sanitizedQuery));\n\n    QList<SearchResult> results;\n    while (m_db->next() && !token.isCanceled()) {\n        SearchResult result;\n        result.name = m_db->value(0).toString();\n        result.type = parseSymbolType(m_db->value(1).toString());\n        result.urlPath = m_db->value(2).toString();\n        result.urlFragment = m_db->value(3).toString();\n        result.docset = const_cast<Docset *>(this);\n        result.score = m_db->value(4).toDouble();\n\n        // Compute match positions for highlighting.\n        if (m_isFuzzySearchEnabled) {\n            // Fuzzy search: use fuzzy matching algorithm.\n            Util::Fuzzy::score(query, result.name, &result.matchPositions);\n        } else {\n            // Non-fuzzy search: highlight only first occurrence.\n            const int pos = result.name.indexOf(query, 0, Qt::CaseInsensitive);\n            if (pos != -1) {\n                for (int i = 0; i < query.length(); ++i) {\n                    result.matchPositions.append(pos + i);\n                }\n            }\n        }\n\n        results.append(std::move(result));\n    }\n\n    return results;\n}\n\nQList<SearchResult> Docset::relatedLinks(const QUrl &url) const\n{\n    if (!m_baseUrl.isParentOf(url)) {\n        return {};\n    }\n\n    // Get page path within the docset.\n    const QString path = url.path().mid(m_baseUrl.path().length() + 1);\n\n    // Prepare the query to look up all pages with the same url.\n    QString sql;\n    if (m_type == Docset::Type::Dash) {\n        sql = QStringLiteral(\"SELECT name, type, path\"\n                             \"  FROM searchIndex\"\n                             \"  WHERE path LIKE \\\"%1%%\\\" AND path <> \\\"%1\\\"\");\n    } else if (m_type == Docset::Type::ZDash) {\n        sql = QStringLiteral(\"SELECT name, type, path, fragment\"\n                             \"  FROM searchIndex\"\n                             \"  WHERE path = \\\"%1\\\" AND fragment IS NOT NULL\");\n    }\n\n    QList<SearchResult> results;\n\n    m_db->prepare(sql.arg(path));\n    while (m_db->next()) {\n        results.append({m_db->value(0).toString(),\n                        parseSymbolType(m_db->value(1).toString()),\n                        m_db->value(2).toString(), m_db->value(3).toString(),\n                        const_cast<Docset *>(this), 0});\n    }\n\n    if (results.size() == 1) {\n        return {};\n    }\n\n    return results;\n}\n\nQUrl Docset::searchResultUrl(const SearchResult &result) const\n{\n    return createPageUrl(result.urlPath, result.urlFragment);\n}\n\nvoid Docset::loadMetadata()\n{\n    const QDir dir(m_path);\n\n    // Fallback if meta.json is absent\n    if (!dir.exists(QStringLiteral(\"meta.json\")))\n        return;\n\n    QScopedPointer<QFile> file(new QFile(dir.filePath(QStringLiteral(\"meta.json\"))));\n    if (!file->open(QIODevice::ReadOnly))\n        return;\n\n    QJsonParseError jsonError;\n    const QJsonObject jsonObject = QJsonDocument::fromJson(file->readAll(), &jsonError).object();\n\n    if (jsonError.error != QJsonParseError::NoError)\n        return;\n\n    m_name = jsonObject[QStringLiteral(\"name\")].toString();\n    m_title = jsonObject[QStringLiteral(\"title\")].toString();\n    m_version = jsonObject[QStringLiteral(\"version\")].toString();\n    m_revision = jsonObject[QStringLiteral(\"revision\")].toString().toInt();\n\n    if (jsonObject.contains(QStringLiteral(\"feed_url\"))) {\n        m_feedUrl = jsonObject[QStringLiteral(\"feed_url\")].toString();\n    }\n\n    if (jsonObject.contains(QStringLiteral(\"extra\"))) {\n        const QJsonObject extra = jsonObject[QStringLiteral(\"extra\")].toObject();\n\n        if (extra.contains(QStringLiteral(\"indexFilePath\"))) {\n            m_indexFilePath = extra[QStringLiteral(\"indexFilePath\")].toString();\n        }\n\n        if (extra.contains(QStringLiteral(\"keywords\"))) {\n            for (const QJsonValueRef kw : extra[QStringLiteral(\"keywords\")].toArray()) {\n                m_keywords << kw.toString();\n            }\n        }\n\n        if (extra.contains(QStringLiteral(\"isJavaScriptEnabled\"))) {\n            m_isJavaScriptEnabled = extra[QStringLiteral(\"isJavaScriptEnabled\")].toBool();\n        }\n    }\n}\n\nvoid Docset::countSymbols()\n{\n    static const QString sql = QStringLiteral(\"SELECT type, COUNT(*)\"\n                                              \"  FROM searchIndex\"\n                                              \"  GROUP BY type\");\n    if (!m_db->prepare(sql)) {\n        qCWarning(log, \"[%s] Cannot prepare statement to count symbols: %s.\",\n                  qPrintable(m_name), qPrintable(m_db->lastError()));\n        return;\n    }\n\n    while (m_db->next()) {\n        const QString symbolTypeStr = m_db->value(0).toString();\n\n        // A workaround for https://github.com/zealdocs/zeal/issues/980.\n        if (symbolTypeStr.isEmpty()) {\n            qCDebug(log, \"[%s] Found empty symbol type, skipping...\", qPrintable(m_name));\n            continue;\n        }\n\n        const QString symbolType = parseSymbolType(symbolTypeStr);\n        m_symbolStrings.insert(symbolType, symbolTypeStr);\n        m_symbolCounts[symbolType] += m_db->value(1).toInt();\n    }\n}\n\n// TODO: Fetch and cache only portions of symbols\nvoid Docset::loadSymbols(const QString &symbolType) const\n{\n    // Iterator `it` is a QPair<QMap::const_iterator, QMap::const_iterator>,\n    // with it.first and it.second respectively pointing to the start and the end\n    // of the range of nodes having symbolType as key. It effectively represents a\n    // contiguous view over the nodes with a specified key.\n    for (auto it = std::as_const(m_symbolStrings).equal_range(symbolType); it.first != it.second; ++it.first) {\n        loadSymbols(symbolType, it.first.value());\n    }\n}\n\nvoid Docset::loadSymbols(const QString &symbolType, const QString &symbolString) const\n{\n    QString sql;\n    if (m_type == Docset::Type::Dash) {\n        sql = QStringLiteral(\"SELECT name, path\"\n                             \"  FROM searchIndex\"\n                             \"  WHERE type='%1'\"\n                             \"  ORDER BY name\");\n    } else {\n        sql = QStringLiteral(\"SELECT name, path, fragment\"\n                             \"  FROM searchIndex\"\n                             \"  WHERE type='%1'\"\n                             \"  ORDER BY name\");\n    }\n\n    if (!m_db->prepare(sql.arg(symbolString))) {\n        qCWarning(log, \"[%s] Cannot prepare statement to load symbols for type '%s': %s.\",\n                  qPrintable(m_name), qPrintable(symbolString), qPrintable(m_db->lastError()));\n        return;\n    }\n\n    QMultiMap<QString, QUrl> &symbols = m_symbols[symbolType];\n    while (m_db->next()) {\n        symbols.insert(m_db->value(0).toString(),\n                       createPageUrl(m_db->value(1).toString(),\n                                     m_db->value(2).toString()));\n    }\n}\n\nvoid Docset::createIndex()\n{\n    static const QString indexListQuery = QStringLiteral(\"PRAGMA INDEX_LIST('%1')\");\n    static const QString indexDropQuery = QStringLiteral(\"DROP INDEX '%1'\");\n    static const QString indexCreateQuery = QStringLiteral(\"CREATE INDEX IF NOT EXISTS %1%2\"\n                                                           \" ON %3 (%4 COLLATE NOCASE)\");\n\n    const QString tableName = m_type == Type::Dash ? QStringLiteral(\"searchIndex\")\n                                                   : QStringLiteral(\"ztoken\");\n    const QString columnName = m_type == Type::Dash ? QStringLiteral(\"name\")\n                                                    : QStringLiteral(\"ztokenname\");\n\n    m_db->prepare(indexListQuery.arg(tableName));\n\n    QStringList oldIndexes;\n\n    while (m_db->next()) {\n        const QString indexName = m_db->value(1).toString();\n        if (!indexName.startsWith(IndexNamePrefix))\n            continue;\n\n        if (indexName.endsWith(IndexNameVersion))\n            return;\n\n        oldIndexes << indexName;\n    }\n\n    // Drop old indexes\n    for (const QString &oldIndexName : std::as_const(oldIndexes)) {\n        m_db->execute(indexDropQuery.arg(oldIndexName));\n    }\n\n    m_db->execute(indexCreateQuery.arg(IndexNamePrefix).arg(IndexNameVersion).arg(tableName).arg(columnName));\n}\n\nvoid Docset::createView()\n{\n    static const QString viewCreateQuery\n            = QStringLiteral(\"CREATE VIEW IF NOT EXISTS searchIndex AS\"\n                             \"  SELECT\"\n                             \"    ztokenname AS name,\"\n                             \"    ztypename AS type,\"\n                             \"    zpath AS path,\"\n                             \"    zanchor AS fragment\"\n                             \"  FROM ztoken\"\n                             \"  INNER JOIN ztokenmetainformation\"\n                             \"    ON ztoken.zmetainformation = ztokenmetainformation.z_pk\"\n                             \"  INNER JOIN zfilepath\"\n                             \"    ON ztokenmetainformation.zfile = zfilepath.z_pk\"\n                             \"  INNER JOIN ztokentype\"\n                             \"    ON ztoken.ztokentype = ztokentype.z_pk\");\n\n    m_db->execute(viewCreateQuery);\n}\n\nQUrl Docset::createPageUrl(const QString &path, const QString &fragment) const\n{\n    QString realPath;\n    QString realFragment;\n\n    if (fragment.isEmpty()) {\n        const QStringList urlParts = path.split(QLatin1Char('#'));\n        realPath = urlParts[0];\n        if (urlParts.size() > 1)\n            realFragment = urlParts[1];\n    } else {\n        realPath = path;\n        realFragment = fragment;\n    }\n\n    static const QRegularExpression dashEntryRegExp(QStringLiteral(\"<dash_entry_.*>\"));\n    realPath.remove(dashEntryRegExp);\n    realFragment.remove(dashEntryRegExp);\n\n    QUrl url = m_baseUrl;\n    url.setPath(m_baseUrl.path() + \"/\" + realPath, QUrl::TolerantMode);\n\n    if (!realFragment.isEmpty()) {\n        if (realFragment.startsWith(QLatin1String(\"//apple_ref\"))\n                || realFragment.startsWith(QLatin1String(\"//dash_ref\"))) {\n            url.setFragment(realFragment, QUrl::DecodedMode);\n        } else {\n            url.setFragment(realFragment);\n        }\n    }\n\n    return url;\n}\n\nQString Docset::parseSymbolType(const QString &str)\n{\n    // Dash symbol aliases\n    const static QHash<QString, QString> aliases = {\n        // Attribute\n        {QStringLiteral(\"Package Attributes\"), QStringLiteral(\"Attribute\")},\n        {QStringLiteral(\"Private Attributes\"), QStringLiteral(\"Attribute\")},\n        {QStringLiteral(\"Protected Attributes\"), QStringLiteral(\"Attribute\")},\n        {QStringLiteral(\"Public Attributes\"), QStringLiteral(\"Attribute\")},\n        {QStringLiteral(\"Static Package Attributes\"), QStringLiteral(\"Attribute\")},\n        {QStringLiteral(\"Static Private Attributes\"), QStringLiteral(\"Attribute\")},\n        {QStringLiteral(\"Static Protected Attributes\"), QStringLiteral(\"Attribute\")},\n        {QStringLiteral(\"Static Public Attributes\"), QStringLiteral(\"Attribute\")},\n        {QStringLiteral(\"XML Attributes\"), QStringLiteral(\"Attribute\")},\n        // Binding\n        {QStringLiteral(\"binding\"), QStringLiteral(\"Binding\")},\n        // Category\n        {QStringLiteral(\"cat\"), QStringLiteral(\"Category\")},\n        {QStringLiteral(\"Groups\"), QStringLiteral(\"Category\")},\n        {QStringLiteral(\"Pages\"), QStringLiteral(\"Category\")},\n        // Class\n        {QStringLiteral(\"cl\"), QStringLiteral(\"Class\")},\n        {QStringLiteral(\"specialization\"), QStringLiteral(\"Class\")},\n        {QStringLiteral(\"tmplt\"), QStringLiteral(\"Class\")},\n        // Constant\n        {QStringLiteral(\"data\"), QStringLiteral(\"Constant\")},\n        {QStringLiteral(\"econst\"), QStringLiteral(\"Constant\")},\n        {QStringLiteral(\"enumdata\"), QStringLiteral(\"Constant\")},\n        {QStringLiteral(\"enumelt\"), QStringLiteral(\"Constant\")},\n        {QStringLiteral(\"clconst\"), QStringLiteral(\"Constant\")},\n        {QStringLiteral(\"structdata\"), QStringLiteral(\"Constant\")},\n        {QStringLiteral(\"writerid\"), QStringLiteral(\"Constant\")},\n        {QStringLiteral(\"Notifications\"), QStringLiteral(\"Constant\")},\n        // Constructor\n        {QStringLiteral(\"structctr\"), QStringLiteral(\"Constructor\")},\n        {QStringLiteral(\"Public Constructors\"), QStringLiteral(\"Constructor\")},\n        // Enumeration\n        {QStringLiteral(\"enum\"), QStringLiteral(\"Enumeration\")},\n        {QStringLiteral(\"Enum\"), QStringLiteral(\"Enumeration\")},\n        {QStringLiteral(\"Enumerations\"), QStringLiteral(\"Enumeration\")},\n        // Event\n        {QStringLiteral(\"event\"), QStringLiteral(\"Event\")},\n        {QStringLiteral(\"Public Events\"), QStringLiteral(\"Event\")},\n        {QStringLiteral(\"Inherited Events\"), QStringLiteral(\"Event\")},\n        {QStringLiteral(\"Private Events\"), QStringLiteral(\"Event\")},\n        // Field\n        {QStringLiteral(\"Data Fields\"), QStringLiteral(\"Field\")},\n        // Function\n        {QStringLiteral(\"dcop\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"func\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"ffunc\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"signal\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"slot\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"grammar\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Function Prototypes\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Functions/Subroutines\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Members\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Package Functions\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Private Member Functions\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Private Slots\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Protected Member Functions\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Protected Slots\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Public Member Functions\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Public Slots\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Signals\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Static Package Functions\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Static Private Member Functions\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Static Protected Member Functions\"), QStringLiteral(\"Function\")},\n        {QStringLiteral(\"Static Public Member Functions\"), QStringLiteral(\"Function\")},\n        // Guide\n        {QStringLiteral(\"doc\"), QStringLiteral(\"Guide\")},\n        // Namespace\n        {QStringLiteral(\"ns\"), QStringLiteral(\"Namespace\")},\n        // Macro\n        {QStringLiteral(\"macro\"), QStringLiteral(\"Macro\")},\n        // Method\n        {QStringLiteral(\"clm\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"enumcm\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"enumctr\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"enumm\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"intfctr\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"intfcm\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"intfm\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"intfsub\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"instsub\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"instctr\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"instm\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"structcm\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"structm\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"structsub\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"Class Methods\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"Inherited Methods\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"Instance Methods\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"Private Methods\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"Protected Methods\"), QStringLiteral(\"Method\")},\n        {QStringLiteral(\"Public Methods\"), QStringLiteral(\"Method\")},\n        // Operator\n        {QStringLiteral(\"intfopfunc\"), QStringLiteral(\"Operator\")},\n        {QStringLiteral(\"opfunc\"), QStringLiteral(\"Operator\")},\n        // Property\n        {QStringLiteral(\"enump\"), QStringLiteral(\"Property\")},\n        {QStringLiteral(\"intfdata\"), QStringLiteral(\"Property\")},\n        {QStringLiteral(\"intfp\"), QStringLiteral(\"Property\")},\n        {QStringLiteral(\"instp\"), QStringLiteral(\"Property\")},\n        {QStringLiteral(\"structp\"), QStringLiteral(\"Property\")},\n        {QStringLiteral(\"Inherited Properties\"), QStringLiteral(\"Property\")},\n        {QStringLiteral(\"Private Properties\"), QStringLiteral(\"Property\")},\n        {QStringLiteral(\"Protected Properties\"), QStringLiteral(\"Property\")},\n        {QStringLiteral(\"Public Properties\"), QStringLiteral(\"Property\")},\n        // Protocol\n        {QStringLiteral(\"intf\"), QStringLiteral(\"Protocol\")},\n        // Structure\n        {QStringLiteral(\"_Struct\"), QStringLiteral(\"Structure\")},\n        {QStringLiteral(\"_Structs\"), QStringLiteral(\"Structure\")},\n        {QStringLiteral(\"struct\"), QStringLiteral(\"Structure\")},\n        {QStringLiteral(\"Control Structure\"), QStringLiteral(\"Structure\")},\n        {QStringLiteral(\"Data Structures\"), QStringLiteral(\"Structure\")},\n        {QStringLiteral(\"Struct\"), QStringLiteral(\"Structure\")},\n        // Type\n        {QStringLiteral(\"tag\"), QStringLiteral(\"Type\")},\n        {QStringLiteral(\"tdef\"), QStringLiteral(\"Type\")},\n        {QStringLiteral(\"Data Types\"), QStringLiteral(\"Type\")},\n        {QStringLiteral(\"Package Types\"), QStringLiteral(\"Type\")},\n        {QStringLiteral(\"Private Types\"), QStringLiteral(\"Type\")},\n        {QStringLiteral(\"Protected Types\"), QStringLiteral(\"Type\")},\n        {QStringLiteral(\"Public Types\"), QStringLiteral(\"Type\")},\n        {QStringLiteral(\"Typedefs\"), QStringLiteral(\"Type\")},\n        // Variable\n        {QStringLiteral(\"var\"), QStringLiteral(\"Variable\")}\n    };\n\n    return aliases.value(str, str);\n}\n\nQUrl Docset::baseUrl() const\n{\n    return m_baseUrl;\n}\n\nvoid Docset::setBaseUrl(const QUrl &baseUrl)\n{\n    m_baseUrl = baseUrl;\n\n    if (!m_indexFilePath.isEmpty()) {\n        m_indexFileUrl = createPageUrl(m_indexFilePath);\n    }\n}\n\nbool Docset::isFuzzySearchEnabled() const\n{\n    return m_isFuzzySearchEnabled;\n}\n\nvoid Docset::setFuzzySearchEnabled(bool enabled)\n{\n    m_isFuzzySearchEnabled = enabled;\n}\n\nbool Docset::isJavaScriptEnabled() const\n{\n    return m_isJavaScriptEnabled;\n}\n\nstatic void sqliteScoreFunction(sqlite3_context *context, int argc, sqlite3_value **argv)\n{\n    Q_UNUSED(argc)\n\n    auto needle = reinterpret_cast<const char *>(sqlite3_value_text(argv[0]));\n    auto haystack = reinterpret_cast<const char *>(sqlite3_value_text(argv[1]));\n\n    sqlite3_result_double(context, Zeal::Util::Fuzzy::scoreFunction(needle, haystack));\n}\n"
  },
  {
    "path": "src/libs/registry/docset.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_REGISTRY_DOCSET_H\n#define ZEAL_REGISTRY_DOCSET_H\n\n#include <QIcon>\n#include <QMap>\n#include <QMetaObject>\n#include <QMultiMap>\n#include <QUrl>\n\nnamespace Zeal {\n\nnamespace Util {\nclass SQLiteDatabase;\n}\n\nnamespace Registry {\n\nclass CancellationToken;\nstruct SearchResult;\n\nclass Docset final\n{\n    Q_DISABLE_COPY_MOVE(Docset)\npublic:\n    explicit Docset(QString path);\n    virtual ~Docset();\n\n    bool isValid() const;\n\n    QString name() const;\n    QString title() const;\n    QStringList keywords() const;\n\n    QString version() const;\n    int revision() const;\n    QString feedUrl() const;\n\n    QString path() const;\n    QString documentPath() const;\n    QIcon icon() const;\n    QIcon symbolTypeIcon(const QString &symbolType) const;\n    QUrl indexFileUrl() const;\n\n    QMap<QString, int> symbolCounts() const;\n    int symbolCount(const QString &symbolType) const;\n\n    const QMultiMap<QString, QUrl> &symbols(const QString &symbolType) const;\n\n    QList<SearchResult> search(const QString &query, const CancellationToken &token) const;\n    QList<SearchResult> relatedLinks(const QUrl &url) const;\n\n    // FIXME: This a temporary solution to create URL on demand.\n    QUrl searchResultUrl(const SearchResult &result) const;\n\n    // FIXME: This is an ugly workaround before we have a proper docset sources implementation\n    bool hasUpdate = false;\n\n    QUrl baseUrl() const;\n    void setBaseUrl(const QUrl &baseUrl);\n\n    bool isFuzzySearchEnabled() const;\n    void setFuzzySearchEnabled(bool enabled);\n\n    bool isJavaScriptEnabled() const;\n\nprivate:\n    enum class Type {\n        Invalid,\n        Dash,\n        ZDash\n    };\n\n    void loadMetadata();\n    void countSymbols();\n    void loadSymbols(const QString &symbolType) const;\n    void loadSymbols(const QString &symbolType, const QString &symbolString) const;\n    void createIndex();\n    void createView();\n    QUrl createPageUrl(const QString &path, const QString &fragment = QString()) const;\n\n    static QString parseSymbolType(const QString &str);\n\n    QString m_name;\n    QString m_title;\n    QStringList m_keywords;\n    QString m_version;\n    int m_revision = 0;\n    QString m_feedUrl;\n    Docset::Type m_type = Type::Invalid;\n    QString m_path;\n    QIcon m_icon;\n\n    QUrl m_indexFileUrl;\n    QString m_indexFilePath;\n\n    QMultiMap<QString, QString> m_symbolStrings;\n    QMap<QString, int> m_symbolCounts;\n    mutable QMap<QString, QMultiMap<QString, QUrl>> m_symbols;\n    Util::SQLiteDatabase *m_db = nullptr;\n\n    bool m_isFuzzySearchEnabled = false;\n    bool m_isJavaScriptEnabled = false;\n\n    QUrl m_baseUrl;\n};\n\n} // namespace Registry\n} // namespace Zeal\n\n#endif // ZEAL_REGISTRY_DOCSET_H\n"
  },
  {
    "path": "src/libs/registry/docsetmetadata.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"docsetmetadata.h\"\n\n#include <QFile>\n#include <QGuiApplication>\n#include <QJsonArray>\n#include <QJsonDocument>\n#include <QRandomGenerator>\n#include <QVariant>\n#include <QXmlStreamReader>\n\nusing namespace Zeal::Registry;\n\nDocsetMetadata::DocsetMetadata(const QJsonObject &jsonObject)\n{\n    m_name = jsonObject[QStringLiteral(\"name\")].toString();\n    m_title = jsonObject[QStringLiteral(\"title\")].toString();\n\n    m_rawIcon = QByteArray::fromBase64(jsonObject[QStringLiteral(\"icon\")].toString().toLocal8Bit());\n    m_icon.addPixmap(QPixmap::fromImage(QImage::fromData(m_rawIcon)));\n\n    m_rawIcon2x = QByteArray::fromBase64(jsonObject[QStringLiteral(\"icon2x\")].toString()\n            .toLocal8Bit());\n    if (qApp->devicePixelRatio() > 1.0) {\n        QPixmap pixmap = QPixmap::fromImage(QImage::fromData(m_rawIcon2x));\n        pixmap.setDevicePixelRatio(2.0);\n        m_icon.addPixmap(pixmap);\n    }\n\n    for (const QJsonValueRef vv : jsonObject[QStringLiteral(\"aliases\")].toArray()) {\n        m_aliases << vv.toString();\n    }\n\n    for (const QJsonValueRef vv : jsonObject[QStringLiteral(\"versions\")].toArray()) {\n        m_versions << vv.toString();\n    }\n\n    // Unfortunately, API returns revision as a string, so it needs to be converted to integer\n    // for comparison to work properly.\n    m_revision = jsonObject[QStringLiteral(\"revision\")].toString().toInt();\n\n    m_feedUrl = QUrl(jsonObject[QStringLiteral(\"feed_url\")].toString());\n\n    for (const QJsonValueRef vv : jsonObject[QStringLiteral(\"urls\")].toArray()) {\n        m_urls.append(QUrl(vv.toString()));\n    }\n\n    m_extra = jsonObject[QStringLiteral(\"extra\")].toObject();\n}\n\n/*!\n  Creates meta.json for specified docset \\a version in the \\a path.\n*/\nvoid DocsetMetadata::save(const QString &path, const QString &version)\n{\n    QScopedPointer<QFile> file(new QFile(path + QLatin1String(\"/meta.json\")));\n    if (!file->open(QIODevice::WriteOnly))\n        return;\n\n    QJsonObject jsonObject;\n\n    jsonObject[QStringLiteral(\"name\")] = m_name;\n    jsonObject[QStringLiteral(\"title\")] = m_title;\n\n    if (!version.isEmpty())\n        jsonObject[QStringLiteral(\"version\")] = version;\n\n    if (version == latestVersion() && m_revision > 0)\n        jsonObject[QStringLiteral(\"revision\")] = QString::number(m_revision);\n\n    if (!m_feedUrl.isEmpty())\n        jsonObject[QStringLiteral(\"feed_url\")] = m_feedUrl.toString();\n\n    if (!m_urls.isEmpty()) {\n        QJsonArray urls;\n        for (const QUrl &url : std::as_const(m_urls)) {\n            urls.append(url.toString());\n        }\n\n        jsonObject[QStringLiteral(\"urls\")] = urls;\n    }\n\n    if (!m_extra.isEmpty())\n        jsonObject[QStringLiteral(\"extra\")] = m_extra;\n\n    file->write(QJsonDocument(jsonObject).toJson());\n    file->close();\n\n    if (m_rawIcon.isEmpty())\n        return;\n\n    file->setFileName(path + QLatin1String(\"/icon.png\"));\n    if (file->open(QIODevice::WriteOnly))\n        file->write(m_rawIcon);\n    file->close();\n\n    if (m_rawIcon2x.isEmpty())\n        return;\n\n    file->setFileName(path + QLatin1String(\"/icon@2x.png\"));\n    if (file->open(QIODevice::WriteOnly))\n        file->write(m_rawIcon2x);\n    file->close();\n}\n\nQString DocsetMetadata::name() const\n{\n    return m_name;\n}\n\nQIcon DocsetMetadata::icon() const\n{\n    return m_icon;\n}\n\nQString DocsetMetadata::title() const\n{\n    return m_title;\n}\n\nQStringList DocsetMetadata::aliases() const\n{\n    return m_aliases;\n}\n\nQStringList DocsetMetadata::versions() const\n{\n    return m_versions;\n}\n\nQString DocsetMetadata::latestVersion() const\n{\n    return m_versions.isEmpty() ? QString() : m_versions.first();\n}\n\nint DocsetMetadata::revision() const\n{\n    return m_revision;\n}\n\nQUrl DocsetMetadata::feedUrl() const\n{\n    return m_feedUrl;\n}\n\nQUrl DocsetMetadata::url() const\n{\n    return m_urls.at(QRandomGenerator::global()->bounded(m_urls.size()));\n}\n\nQList<QUrl> DocsetMetadata::urls() const\n{\n    return m_urls;\n}\n\nDocsetMetadata DocsetMetadata::fromDashFeed(const QUrl &feedUrl, const QByteArray &data)\n{\n    DocsetMetadata metadata;\n\n    metadata.m_name = feedUrl.fileName();\n\n    // Strip \".xml\" extension if any.\n    if (metadata.m_name.endsWith(QLatin1String(\".xml\"))) {\n        metadata.m_name.chop(4);\n    }\n\n    metadata.m_title = metadata.m_name;\n    metadata.m_title.replace(QLatin1Char('_'), QLatin1Char(' '));\n\n    metadata.m_feedUrl = feedUrl;\n\n    QXmlStreamReader xml(data);\n\n    while (!xml.atEnd()) {\n        const QXmlStreamReader::TokenType token = xml.readNext();\n        if (token != QXmlStreamReader::StartElement)\n            continue;\n\n        // Try to pull out the relevant data\n        if (xml.name() == QLatin1String(\"version\")) {\n            if (xml.readNext() != QXmlStreamReader::Characters)\n                continue;\n            metadata.m_versions << xml.text().toString();\n        } else if (xml.name() == QLatin1String(\"url\")) {\n            if (xml.readNext() != QXmlStreamReader::Characters)\n                continue;\n            metadata.m_urls.append(QUrl(xml.text().toString()));\n        }\n    }\n\n    return metadata;\n}\n"
  },
  {
    "path": "src/libs/registry/docsetmetadata.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_REGISTRY_DOCSETMETADATA_H\n#define ZEAL_REGISTRY_DOCSETMETADATA_H\n\n#include <QIcon>\n#include <QJsonObject>\n#include <QStringList>\n#include <QUrl>\n\nnamespace Zeal {\nnamespace Registry {\n\nclass DocsetMetadata\n{\npublic:\n    explicit DocsetMetadata() = default;\n    explicit DocsetMetadata(const QJsonObject &jsonObject);\n\n    void save(const QString &path, const QString &version);\n\n    QString name() const;\n    QString title() const;\n    QStringList aliases() const;\n    QStringList versions() const;\n    QString latestVersion() const;\n    int revision() const;\n    QIcon icon() const;\n\n    QUrl feedUrl() const;\n    QUrl url() const;\n    QList<QUrl> urls() const;\n\n    static DocsetMetadata fromDashFeed(const QUrl &feedUrl, const QByteArray &data);\n\nprivate:\n    QString m_name;\n    QString m_title;\n    QStringList m_aliases;\n    QStringList m_versions;\n    int m_revision = 0;\n\n    QByteArray m_rawIcon;\n    QByteArray m_rawIcon2x;\n    QIcon m_icon;\n\n    QJsonObject m_extra;\n\n    QUrl m_feedUrl;\n    QList<QUrl> m_urls;\n};\n\n} // namespace Registry\n} // namespace Zeal\n\n#endif // ZEAL_REGISTRY_DOCSETMETADATA_H\n"
  },
  {
    "path": "src/libs/registry/docsetregistry.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"docsetregistry.h\"\n\n#include \"docset.h\"\n#include \"listmodel.h\"\n#include \"searchquery.h\"\n#include \"searchresult.h\"\n\n#include <core/application.h>\n#include <core/httpserver.h>\n\n#include <QDir>\n#include <QLoggingCategory>\n#include <QThread>\n\n#include <QtConcurrent>\n\n#include <functional>\n#include <future>\n\nusing namespace Zeal::Registry;\n\nstatic Q_LOGGING_CATEGORY(log, \"zeal.registry.docsetregistry\")\n\nvoid MergeQueryResults(QList<SearchResult> &finalResult, const QList<SearchResult> &partial)\n{\n    finalResult << partial;\n}\n\nDocsetRegistry::DocsetRegistry(QObject *parent)\n    : QObject(parent)\n    , m_model(new ListModel(this))\n    , m_thread(new QThread(this))\n{\n    // Register for use in signal connections.\n    qRegisterMetaType<QList<SearchResult>>(\"QList<SearchResult>\");\n\n    // FIXME: Only search should be performed in a separate thread\n    moveToThread(m_thread);\n    m_thread->start();\n}\n\nDocsetRegistry::~DocsetRegistry()\n{\n    m_thread->exit();\n    m_thread->wait();\n    qDeleteAll(m_docsets);\n}\n\nQAbstractItemModel *DocsetRegistry::model() const\n{\n    return m_model;\n}\n\nQString DocsetRegistry::storagePath() const\n{\n    return m_storagePath;\n}\n\nvoid DocsetRegistry::setStoragePath(const QString &path)\n{\n    if (path == m_storagePath) {\n        return;\n    }\n\n    QMetaObject::invokeMethod(this, [this, path](){\n        unloadAllDocsets();\n        addDocsetsFromFolder(path);\n        m_storagePath = path;\n    });\n}\n\nbool DocsetRegistry::isFuzzySearchEnabled() const\n{\n    return m_isFuzzySearchEnabled;\n}\n\nvoid DocsetRegistry::setFuzzySearchEnabled(bool enabled)\n{\n    if (enabled == m_isFuzzySearchEnabled) {\n        return;\n    }\n\n    m_isFuzzySearchEnabled = enabled;\n\n    for (Docset *docset : std::as_const(m_docsets)) {\n        docset->setFuzzySearchEnabled(enabled);\n    }\n}\n\nint DocsetRegistry::count() const\n{\n    return m_docsets.count();\n}\n\nbool DocsetRegistry::contains(const QString &name) const\n{\n    return m_docsets.contains(name);\n}\n\nQStringList DocsetRegistry::names() const\n{\n    return m_docsets.keys();\n}\n\nvoid DocsetRegistry::loadDocset(const QString &path)\n{\n    std::future<Docset *> f = std::async(std::launch::async, [path](){\n        return new Docset(path);\n    });\n\n    f.wait();\n    Docset *docset = f.get();\n    // TODO: Emit error\n    if (!docset->isValid()) {\n        qCWarning(log, \"Could not load docset '%s' from '%s'. Reinstall the docset.\",\n                  qPrintable(docset->name()), qPrintable(docset->path()));\n        delete docset;\n        return;\n    }\n\n    docset->setFuzzySearchEnabled(m_isFuzzySearchEnabled);\n\n    const QString name = docset->name();\n    if (m_docsets.contains(name)) {\n        unloadDocset(name);\n    }\n\n    // Setup HTTP mount.\n    QUrl url = Core::Application::instance()->httpServer()->mount(name, docset->documentPath());\n    if (url.isEmpty()) {\n        qCWarning(log, \"Could not enable docset '%s' from '%s'. Reinstall the docset.\",\n                  qPrintable(docset->name()), qPrintable(docset->path()));\n        delete docset;\n        return;\n    }\n\n    docset->setBaseUrl(url);\n\n    m_docsets[name] = docset;\n\n    emit docsetLoaded(name);\n}\n\nvoid DocsetRegistry::unloadDocset(const QString &name)\n{\n    emit docsetAboutToBeUnloaded(name);\n    Core::Application::instance()->httpServer()->unmount(name);\n    delete m_docsets.take(name);\n    emit docsetUnloaded(name);\n}\n\nvoid DocsetRegistry::unloadAllDocsets()\n{\n    const auto keys = m_docsets.keys();\n    for (const QString &name : keys) {\n        unloadDocset(name);\n    }\n}\n\nDocset *DocsetRegistry::docset(const QString &name) const\n{\n    return m_docsets[name];\n}\n\nDocset *DocsetRegistry::docset(int index) const\n{\n    if (index < 0 || index >= m_docsets.size())\n        return nullptr;\n\n    auto it = m_docsets.cbegin();\n    std::advance(it, index);\n    return *it;\n}\n\nDocset *DocsetRegistry::docsetForUrl(const QUrl &url)\n{\n    for (Docset *docset : std::as_const(m_docsets)) {\n        if (docset->baseUrl().isParentOf(url))\n            return docset;\n    }\n\n    return nullptr;\n}\n\nQList<Docset *> DocsetRegistry::docsets() const\n{\n    return m_docsets.values();\n}\n\nvoid DocsetRegistry::search(const QString &query)\n{\n    m_cancellationToken.cancel();\n\n    if (query.isEmpty()) {\n        emit searchCompleted({});\n        return;\n    }\n\n    QMetaObject::invokeMethod(this, \"_runQuery\", Qt::QueuedConnection, Q_ARG(QString, query));\n}\n\nvoid DocsetRegistry::_runQuery(const QString &query)\n{\n    m_cancellationToken.reset();\n\n    QList<Docset *> enabledDocsets;\n\n    const SearchQuery searchQuery = SearchQuery::fromString(query);\n    if (searchQuery.hasKeywords()) {\n        for (Docset *docset : std::as_const(m_docsets)) {\n            if (searchQuery.hasKeywords(docset->keywords()))\n                enabledDocsets << docset;\n        }\n    } else {\n        enabledDocsets = docsets();\n    }\n\n    QFuture<QList<SearchResult>> queryResultsFuture\n            = QtConcurrent::mappedReduced(enabledDocsets,\n                                          std::bind(&Docset::search,\n                                                    std::placeholders::_1,\n                                                    searchQuery.query(),\n                                                    std::ref(m_cancellationToken)),\n                                          &MergeQueryResults);\n    QList<SearchResult> results = queryResultsFuture.result();\n\n    if (m_cancellationToken.isCanceled())\n        return;\n\n    std::sort(results.begin(), results.end());\n\n    if (m_cancellationToken.isCanceled())\n        return;\n\n    emit searchCompleted(results);\n}\n\n// Recursively finds and adds all docsets in a given directory.\nvoid DocsetRegistry::addDocsetsFromFolder(const QString &path)\n{\n    const QDir dir(path);\n    const auto subDirectories = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllDirs);\n    for (const QFileInfo &subdir : subDirectories) {\n        if (subdir.suffix() == QLatin1String(\"docset\"))\n            loadDocset(subdir.filePath());\n        else\n            addDocsetsFromFolder(subdir.filePath());\n    }\n}\n"
  },
  {
    "path": "src/libs/registry/docsetregistry.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_REGISTRY_DOCSETREGISTRY_H\n#define ZEAL_REGISTRY_DOCSETREGISTRY_H\n\n#include \"cancellationtoken.h\"\n#include \"searchresult.h\"\n\n#include <QMap>\n#include <QObject>\n\nclass QAbstractItemModel;\nclass QThread;\n\nnamespace Zeal {\nnamespace Registry {\n\nclass Docset;\n\nclass DocsetRegistry final : public QObject\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(DocsetRegistry)\npublic:\n    explicit DocsetRegistry(QObject *parent = nullptr);\n    ~DocsetRegistry() override;\n\n    QAbstractItemModel *model() const;\n\n    QString storagePath() const;\n    void setStoragePath(const QString &path);\n\n    bool isFuzzySearchEnabled() const;\n    void setFuzzySearchEnabled(bool enabled);\n\n    int count() const;\n    bool contains(const QString &name) const;\n    QStringList names() const;\n\n    void loadDocset(const QString &path);\n    void unloadDocset(const QString &name);\n    void unloadAllDocsets();\n\n    Docset *docset(const QString &name) const;\n    Docset *docset(int index) const;\n    Docset *docsetForUrl(const QUrl &url);\n    QList<Docset *> docsets() const;\n\n    void search(const QString &query);\n    const QList<SearchResult> &queryResults();\n\nsignals:\n    void docsetLoaded(const QString &name);\n    void docsetAboutToBeUnloaded(const QString &name);\n    void docsetUnloaded(const QString &name);\n    void searchCompleted(const QList<SearchResult> &results);\n\nprivate slots:\n    void _runQuery(const QString &query);\n\nprivate:\n    void addDocsetsFromFolder(const QString &path);\n\n    QAbstractItemModel *m_model = nullptr;\n\n    QString m_storagePath;\n    bool m_isFuzzySearchEnabled = false;\n\n    QThread *m_thread = nullptr;\n    QMap<QString, Docset *> m_docsets;\n\n    CancellationToken m_cancellationToken;\n};\n\n} // namespace Registry\n} // namespace Zeal\n\n#endif // ZEAL_REGISTRY_DOCSETREGISTRY_H\n"
  },
  {
    "path": "src/libs/registry/itemdatarole.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_REGISTRY_ITEMDATAROLE_H\n#define ZEAL_REGISTRY_ITEMDATAROLE_H\n\n#include <Qt>\n\nnamespace Zeal {\nnamespace Registry {\n\nenum ItemDataRole {\n    DocsetIconRole = Qt::UserRole,\n    DocsetNameRole,\n    MatchPositionsRole,\n    UpdateAvailableRole,\n    UrlRole\n};\n\nenum SectionIndex {\n    Name,\n    SearchPrefix,\n    Actions\n};\n\n} // namespace Registry\n} // namespace Zeal\n\n#endif // ZEAL_REGISTRY_ITEMDATAROLE_H\n"
  },
  {
    "path": "src/libs/registry/listmodel.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"listmodel.h\"\n\n#include \"docset.h\"\n#include \"docsetregistry.h\"\n#include \"itemdatarole.h\"\n\n#include <iterator>\n\nusing namespace Zeal::Registry;\n\nListModel::ListModel(DocsetRegistry *docsetRegistry)\n    : QAbstractItemModel(docsetRegistry)\n    , m_docsetRegistry(docsetRegistry)\n{\n    connect(m_docsetRegistry, &DocsetRegistry::docsetLoaded, this, &ListModel::addDocset);\n    connect(m_docsetRegistry, &DocsetRegistry::docsetAboutToBeUnloaded, this, &ListModel::removeDocset);\n}\n\nListModel::~ListModel()\n{\n    for (auto &kv : m_docsetItems) {\n        qDeleteAll(kv.second->groups);\n        delete kv.second;\n    }\n}\n\nQVariant ListModel::headerData(int section, Qt::Orientation orientation, int role) const\n{\n    if (orientation != Qt::Horizontal || role != Qt::DisplayRole) {\n        return QAbstractItemModel::headerData(section, orientation, role);\n    }\n\n    switch (section) {\n    case SectionIndex::Name:\n        return tr(\"Name\");\n    case SectionIndex::SearchPrefix:\n        return tr(\"Search prefix\");\n    default:\n        return QLatin1String();\n    }\n}\n\nQVariant ListModel::data(const QModelIndex &index, int role) const\n{\n    if (!index.isValid()) {\n        return QVariant();\n    }\n\n    switch (role) {\n    case Qt::DecorationRole:\n        if (index.column() != SectionIndex::Name) {\n            return QVariant();\n        }\n\n        switch (indexLevel(index)) {\n        case IndexLevel::Docset:\n            return itemInRow(index.row())->docset->icon();\n        case IndexLevel::Group: {\n            auto docsetItem = static_cast<DocsetItem *>(index.internalPointer());\n            const QString symbolType = docsetItem->groups.at(index.row())->symbolType;\n            return docsetItem->docset->symbolTypeIcon(symbolType);\n        }\n        case IndexLevel::Symbol: {\n            auto groupItem = static_cast<GroupItem *>(index.internalPointer());\n            return groupItem->docsetItem->docset->symbolTypeIcon(groupItem->symbolType);\n        }\n        default:\n            return QVariant();\n        }\n    case Qt::DisplayRole:\n        switch (indexLevel(index)) {\n        case IndexLevel::Docset:\n            switch (index.column()) {\n            case SectionIndex::Name:\n                return itemInRow(index.row())->docset->title();\n            case SectionIndex::SearchPrefix:\n                return itemInRow(index.row())->docset->keywords().join(QLatin1String(\", \"));\n            default:\n                return QVariant();\n            }\n        case IndexLevel::Group: {\n            auto docsetItem = static_cast<DocsetItem *>(index.internalPointer());\n            const QString symbolType = docsetItem->groups.at(index.row())->symbolType;\n            return QStringLiteral(\"%1 (%2)\").arg(pluralize(symbolType),\n                                                 QString::number(docsetItem->docset->symbolCount(symbolType)));\n        }\n        case IndexLevel::Symbol: {\n            auto groupItem = static_cast<GroupItem *>(index.internalPointer());\n            auto it = groupItem->docsetItem->docset->symbols(groupItem->symbolType).cbegin();\n            std::advance(it, index.row());\n            return it.key();\n        }\n        default:\n            return QVariant();\n        }\n    case Qt::ToolTipRole:\n        if (index.column() != SectionIndex::Name) {\n            return QVariant();\n        }\n\n        switch (indexLevel(index)) {\n        case IndexLevel::Docset: {\n            const auto docset = itemInRow(index.row())->docset;\n            return tr(\"Version: %1r%2\").arg(docset->version()).arg(docset->revision());\n        }\n        default:\n            return QVariant();\n        }\n    case ItemDataRole::UrlRole:\n        switch (indexLevel(index)) {\n        case IndexLevel::Docset:\n            return itemInRow(index.row())->docset->indexFileUrl();\n        case IndexLevel::Symbol: {\n            auto groupItem = static_cast<GroupItem *>(index.internalPointer());\n            auto it = groupItem->docsetItem->docset->symbols(groupItem->symbolType).cbegin();\n            std::advance(it, index.row());\n            return it.value();\n        }\n        default:\n            return QVariant();\n        }\n    case ItemDataRole::DocsetNameRole:\n        if (index.parent().isValid())\n            return QVariant();\n        return itemInRow(index.row())->docset->name();\n    case ItemDataRole::UpdateAvailableRole:\n        if (index.parent().isValid())\n            return QVariant();\n        return itemInRow(index.row())->docset->hasUpdate;\n    default:\n        return QVariant();\n    }\n}\n\nQModelIndex ListModel::index(int row, int column, const QModelIndex &parent) const\n{\n    if (!hasIndex(row, column, parent)) {\n        return {};\n    }\n\n    switch (indexLevel(parent)) {\n    case IndexLevel::Root:\n        return createIndex(row, column);\n    case IndexLevel::Docset:\n        return createIndex(row, column, static_cast<void *>(itemInRow(parent.row())));\n    case IndexLevel::Group: {\n        auto docsetItem = static_cast<DocsetItem *>(parent.internalPointer());\n        return createIndex(row, column, docsetItem->groups.at(parent.row()));\n    }\n    default:\n        return {};\n    }\n}\n\nQModelIndex ListModel::parent(const QModelIndex &child) const\n{\n    switch (indexLevel(child)) {\n    case IndexLevel::Group: {\n        auto item = static_cast<DocsetItem *>(child.internalPointer());\n\n        auto it = std::find_if(m_docsetItems.cbegin(), m_docsetItems.cend(), [item](const auto &pair) {\n            return pair.second == item;\n        });\n\n        if (it == m_docsetItems.cend()) {\n            // TODO: Report error, this should never happen.\n            return {};\n        }\n\n        const int row = static_cast<int>(std::distance(m_docsetItems.begin(), it));\n        return createIndex(row, 0);\n    }\n    case IndexLevel::Symbol: {\n        auto item = static_cast<GroupItem *>(child.internalPointer());\n        return createIndex(item->docsetItem->groups.indexOf(item), 0, item->docsetItem);\n    }\n    default:\n        return {};\n    }\n}\n\nint ListModel::columnCount(const QModelIndex &parent) const\n{\n    if (indexLevel(parent) == IndexLevel::Root) {\n        return 3;\n    }\n\n    return 1;\n}\n\nint ListModel::rowCount(const QModelIndex &parent) const\n{\n    if (parent.column() > 0) {\n        return 0;\n    }\n\n    switch (indexLevel(parent)) {\n    case IndexLevel::Root:\n        return static_cast<int>(m_docsetItems.size());\n    case IndexLevel::Docset:\n        return itemInRow(parent.row())->docset->symbolCounts().count();\n    case IndexLevel::Group: {\n        auto docsetItem = static_cast<DocsetItem *>(parent.internalPointer());\n        return docsetItem->docset->symbolCount(docsetItem->groups.at(parent.row())->symbolType);\n    }\n    default:\n        return 0;\n    }\n}\n\nvoid ListModel::addDocset(const QString &name)\n{\n    const int row = static_cast<int>(std::distance(m_docsetItems.begin(), m_docsetItems.upper_bound(name)));\n    beginInsertRows(QModelIndex(), row, row);\n\n    auto docsetItem = new DocsetItem();\n    docsetItem->docset = m_docsetRegistry->docset(name);\n\n    const auto keys = docsetItem->docset->symbolCounts().keys();\n    for (const QString &symbolType : keys) {\n        auto groupItem = new GroupItem();\n        groupItem->docsetItem = docsetItem;\n        groupItem->symbolType = symbolType;\n        docsetItem->groups.append(groupItem);\n    }\n\n    m_docsetItems.insert({name, docsetItem});\n\n    endInsertRows();\n}\n\nvoid ListModel::removeDocset(const QString &name)\n{\n    auto it = m_docsetItems.find(name);\n    if (it == m_docsetItems.cend()) {\n        // TODO: Investigate why this can happen (see #420)\n        return;\n    }\n\n    const int row = static_cast<int>(std::distance(m_docsetItems.begin(), it));\n    beginRemoveRows(QModelIndex(), row, row);\n\n    qDeleteAll(it->second->groups);\n    delete it->second;\n    m_docsetItems.erase(it);\n\n    endRemoveRows();\n}\n\nQString ListModel::pluralize(const QString &s)\n{\n    if (s.endsWith(QLatin1String(\"y\"))) {\n        return s.left(s.length() - 1) + QLatin1String(\"ies\");\n    }\n\n    return s + (s.endsWith('s') ? QLatin1String(\"es\") : QLatin1String(\"s\"));\n}\n\nListModel::IndexLevel ListModel::indexLevel(const QModelIndex &index)\n{\n    if (!index.isValid()) {\n        return IndexLevel::Root;\n    }\n\n    if (!index.internalPointer()) {\n        return IndexLevel::Docset;\n    }\n\n    if (*static_cast<IndexLevel *>(index.internalPointer()) == IndexLevel::Docset) {\n        return IndexLevel::Group;\n    }\n\n    return IndexLevel::Symbol;\n}\n\nListModel::DocsetItem *ListModel::itemInRow(int row) const\n{\n    auto it = m_docsetItems.cbegin();\n    std::advance(it, row);\n    return it->second;\n}\n"
  },
  {
    "path": "src/libs/registry/listmodel.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_REGISTRY_LISTMODEL_H\n#define ZEAL_REGISTRY_LISTMODEL_H\n\n#include <util/caseinsensitivemap.h>\n\n#include <QAbstractItemModel>\n\nnamespace Zeal {\nnamespace Registry {\n\nclass Docset;\nclass DocsetRegistry;\n\nclass ListModel final : public QAbstractItemModel\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(ListModel)\npublic:\n    ~ListModel() override;\n\n    QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;\n\n    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;\n    QModelIndex index(int row, int column, const QModelIndex &parent) const override;\n    QModelIndex parent(const QModelIndex &child) const override;\n    int columnCount(const QModelIndex &parent) const override;\n    int rowCount(const QModelIndex &parent) const override;\n\nprivate slots:\n    void addDocset(const QString &name);\n    void removeDocset(const QString &name);\n\nprivate:\n    friend class DocsetRegistry;\n\n    enum class IndexLevel {\n        Root,\n        Docset,\n        Group,\n        Symbol\n    };\n\n    explicit ListModel(DocsetRegistry *docsetRegistry);\n\n    inline static QString pluralize(const QString &s);\n    inline static IndexLevel indexLevel(const QModelIndex &index);\n\n    DocsetRegistry *m_docsetRegistry = nullptr;\n\n    struct DocsetItem;\n    struct GroupItem {\n        const IndexLevel level = IndexLevel::Group;\n        DocsetItem *docsetItem = nullptr;\n        QString symbolType;\n    };\n\n    struct DocsetItem {\n        const IndexLevel level = IndexLevel::Docset;\n        Docset *docset = nullptr;\n        QList<GroupItem *> groups;\n    };\n\n    inline DocsetItem *itemInRow(int row) const;\n\n    Util::CaseInsensitiveMap<DocsetItem *> m_docsetItems;\n};\n\n} // namespace Registry\n} // namespace Zeal\n\n#endif // ZEAL_REGISTRY_LISTMODEL_H\n"
  },
  {
    "path": "src/libs/registry/searchmodel.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"searchmodel.h\"\n\n#include \"docset.h\"\n#include \"itemdatarole.h\"\n\nusing namespace Zeal::Registry;\n\nSearchModel::SearchModel(QObject *parent)\n    : QAbstractListModel(parent)\n{\n}\n\nSearchModel *SearchModel::clone(QObject *parent)\n{\n    auto model = new SearchModel(parent);\n    model->m_dataList = m_dataList;\n    return model;\n}\n\nbool SearchModel::isEmpty() const\n{\n    return m_dataList.isEmpty();\n}\n\nQVariant SearchModel::data(const QModelIndex &index, int role) const\n{\n    if (!index.isValid())\n        return QVariant();\n\n    auto item = static_cast<SearchResult *>(index.internalPointer());\n\n    switch (role) {\n    case Qt::DisplayRole:\n        return item->name;\n\n    case Qt::DecorationRole:\n        return item->docset->symbolTypeIcon(item->type);\n\n    case ItemDataRole::DocsetIconRole:\n        return item->docset->icon();\n\n    case ItemDataRole::MatchPositionsRole:\n        return QVariant::fromValue(item->matchPositions);\n\n    case ItemDataRole::UrlRole:\n        return item->docset->searchResultUrl(*item);\n\n    default:\n        return QVariant();\n    }\n}\n\nQModelIndex SearchModel::index(int row, int column, const QModelIndex &parent) const\n{\n    if (parent.isValid() || m_dataList.count() <= row || column > 1)\n        return {};\n\n    // FIXME: const_cast\n    auto item = const_cast<SearchResult *>(&m_dataList.at(row));\n    return createIndex(row, column, item);\n}\n\nint SearchModel::rowCount(const QModelIndex &parent) const\n{\n    if (!parent.isValid())\n        return m_dataList.count();\n    return 0;\n}\n\nbool SearchModel::removeRows(int row, int count, const QModelIndex &parent)\n{\n    if (row + count <= m_dataList.size() && !parent.isValid()) {\n        beginRemoveRows(parent, row, row + count - 1);\n        while (count) {\n            m_dataList.removeAt(row);\n            --count;\n        }\n        endRemoveRows();\n        return true;\n    }\n    return false;\n}\n\nvoid SearchModel::removeSearchResultWithName(const QString &name)\n{\n    QMutableListIterator<SearchResult> iterator(m_dataList);\n\n    int rowNum = 0;\n    while (iterator.hasNext()) {\n        if (iterator.next().docset->name() == name) {\n            beginRemoveRows(QModelIndex(), rowNum, rowNum);\n            iterator.remove();\n            rowNum -= 1;\n            endRemoveRows();\n        }\n        rowNum += 1;\n    }\n}\n\nvoid SearchModel::setResults(const QList<SearchResult> &results)\n{\n    beginResetModel();\n    m_dataList = results;\n    endResetModel();\n    emit updated();\n}\n"
  },
  {
    "path": "src/libs/registry/searchmodel.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_REGISTRY_SEARCHMODEL_H\n#define ZEAL_REGISTRY_SEARCHMODEL_H\n\n#include \"searchresult.h\"\n\n#include <QAbstractListModel>\n\nnamespace Zeal {\nnamespace Registry {\n\nclass SearchModel final : public QAbstractListModel\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(SearchModel)\npublic:\n    explicit SearchModel(QObject *parent = nullptr);\n    SearchModel *clone(QObject *parent = nullptr);\n\n    bool isEmpty() const;\n\n    QVariant data(const QModelIndex &index, int role) const override;\n    QModelIndex index(int row, int column, const QModelIndex &parent) const override;\n    int rowCount(const QModelIndex &parent = QModelIndex()) const override;\n    bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;\n    void removeSearchResultWithName(const QString &name);\n\npublic slots:\n    void setResults(const QList<SearchResult> &results = QList<SearchResult>());\n\nsignals:\n    void updated();\n\nprivate:\n    QList<SearchResult> m_dataList;\n};\n\n} // namespace Registry\n} // namespace Zeal\n\n#endif // ZEAL_REGISTRY_SEARCHMODEL_H\n"
  },
  {
    "path": "src/libs/registry/searchquery.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"searchquery.h\"\n\n#include <utility>\n\nusing namespace Zeal::Registry;\n\nnamespace {\nconst char prefixSeparator = ':';\nconst char keywordSeparator = ',';\n} // namespace\n\nSearchQuery::SearchQuery(QString query, const QStringList &keywords)\n    : m_query(std::move(query))\n{\n    setKeywords(keywords);\n}\n\nSearchQuery SearchQuery::fromString(const QString &str)\n{\n    const int sepAt = str.indexOf(prefixSeparator);\n    const int next = sepAt + 1;\n\n    QString query;\n    QStringList keywords;\n    if (sepAt > 0 && (next >= str.size() || str.at(next) != prefixSeparator)) {\n        query = str.mid(next).trimmed();\n\n        const QString keywordStr = str.left(sepAt).trimmed();\n        keywords = keywordStr.split(keywordSeparator);\n    } else {\n        query = str.trimmed();\n    }\n\n    return SearchQuery(query, keywords);\n}\n\nQString SearchQuery::toString() const\n{\n    if (m_keywords.isEmpty()) {\n        return m_query;\n    }\n\n    return m_keywordPrefix + m_query;\n}\n\nbool SearchQuery::isEmpty() const\n{\n    return m_query.isEmpty() && m_keywords.isEmpty();\n}\n\nQStringList SearchQuery::keywords() const\n{\n    return m_keywords;\n}\n\nvoid SearchQuery::setKeywords(const QStringList &list)\n{\n    if (list.isEmpty())\n        return;\n\n    m_keywords = list;\n    m_keywordPrefix = list.join(keywordSeparator) + prefixSeparator;\n}\n\nbool SearchQuery::hasKeywords() const\n{\n    return !m_keywords.isEmpty();\n}\n\nbool SearchQuery::hasKeywords(const QStringList &keywords) const\n{\n    for (const QString &keyword : keywords) {\n        if (m_keywords.contains(keyword, Qt::CaseInsensitive)) {\n            return true;\n        }\n    }\n\n    return false;\n}\n\nint SearchQuery::keywordPrefixSize() const\n{\n    return m_keywordPrefix.size();\n}\n\nQString SearchQuery::query() const\n{\n    return m_query;\n}\n\nvoid SearchQuery::setQuery(const QString &str)\n{\n    m_query = str;\n}\n\nQDataStream &operator<<(QDataStream &out, const Zeal::Registry::SearchQuery &query)\n{\n    out << query.toString();\n    return out;\n}\n\nQDataStream &operator>>(QDataStream &in, Zeal::Registry::SearchQuery &query)\n{\n    QString str;\n    in >> str;\n    query = SearchQuery::fromString(str);\n    return in;\n}\n"
  },
  {
    "path": "src/libs/registry/searchquery.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_REGISTRY_SEARCHQUERY_H\n#define ZEAL_REGISTRY_SEARCHQUERY_H\n\n#include <QDataStream>\n#include <QStringList>\n\nnamespace Zeal {\nnamespace Registry {\n\n/**\n * @short The search query model.\n */\nclass SearchQuery\n{\npublic:\n    explicit SearchQuery() = default;\n    explicit SearchQuery(QString query, const QStringList &keywords = QStringList());\n\n    /// Creates a search query from a string. Single separator will be\n    /// used to contstruct docset filter, but separator repeated twice\n    /// will be left inside coreQuery part since double semicolon is\n    /// used inside qualified symbol names in popular programming\n    /// languages (c++, ruby, perl, etc.).\n    ///\n    /// Examples:\n    ///   \"android:setTypeFa\" #=> docsetFilters = [\"android\"], coreQuery = \"setTypeFa\"\n    ///   \"noprefix\"          #=> docsetFilters = [], coreQuery = \"noprefix\"\n    ///   \":find\"             #=> docsetFilters = [], coreQuery = \":find\"\n    ///   \"std::string\"       #=> docsetFilters = [], coreQuery = \"std::string\"\n    ///   \"c++:std::string\"   #=> docsetFilters = [\"c++\"], coreQuery = \"std::string\"\n    ///\n    /// Multiple docsets are supported using the ',' character:\n    ///   \"java,android:setTypeFa #=> docsetFilters = [\"java\", \"android\"], coreQuery = \"setTypeFa\"\n\n    static SearchQuery fromString(const QString &str);\n\n    QString toString() const;\n\n    bool isEmpty() const;\n\n    QStringList keywords() const;\n    void setKeywords(const QStringList &list);\n\n    /// Returns true if there's a docset filter for the given query\n    bool hasKeywords() const;\n\n    /// Returns true if one the query contains one of the @c keywords.\n    bool hasKeywords(const QStringList &keywords) const;\n\n    /// Returns the docset filter raw size for the given query\n    int keywordPrefixSize() const;\n\n    QString query() const;\n    void setQuery(const QString &str);\n\nprivate:\n    QString m_query;\n    QStringList m_keywords;\n    QString m_keywordPrefix;\n};\n\n} // namespace Registry\n} // namespace Zeal\n\nQDataStream &operator<<(QDataStream &out, const Zeal::Registry::SearchQuery &query);\nQDataStream &operator>>(QDataStream &in, Zeal::Registry::SearchQuery &query);\n\n#endif // ZEAL_REGISTRY_SEARCHQUERY_H\n"
  },
  {
    "path": "src/libs/registry/searchresult.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_REGISTRY_SEARCHRESULT_H\n#define ZEAL_REGISTRY_SEARCHRESULT_H\n\n#include <QString>\n#include <QUrl>\n#include <QVector>\n\nnamespace Zeal {\nnamespace Registry {\n\nclass Docset;\n\nstruct SearchResult\n{\n    QString name;\n    QString type;\n\n    QString urlPath;\n    QString urlFragment;\n\n    Docset *docset;\n\n    double score;\n    QVector<int> matchPositions;\n\n    inline bool operator<(const SearchResult &other) const\n    {\n        if (score == other.score)\n            return QString::compare(name, other.name, Qt::CaseInsensitive) < 0;\n        return score > other.score;\n    }\n};\n\n} // namespace Registry\n} // namespace Zeal\n\n#endif // ZEAL_REGISTRY_SEARCHRESULT_H\n"
  },
  {
    "path": "src/libs/sidebar/CMakeLists.txt",
    "content": "add_library(Sidebar STATIC\n    container.cpp\n    proxyview.cpp\n    view.cpp\n    viewprovider.cpp\n)\n\nfind_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED)\ntarget_link_libraries(Sidebar PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)\n"
  },
  {
    "path": "src/libs/sidebar/container.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"container.h\"\n\n#include \"view.h\"\n\n#include <core/application.h>\n#include <core/settings.h>\n#include <ui/widgets/layouthelper.h>\n\n#include <QSplitter>\n#include <QVBoxLayout>\n\nusing namespace Zeal;\nusing namespace Zeal::Sidebar;\n\nContainer::Container(QWidget *parent)\n    : QWidget(parent)\n{\n    setMinimumWidth(150);\n\n    // Setup splitter.\n    m_splitter = new QSplitter();\n    m_splitter->setOrientation(Qt::Vertical);\n    connect(m_splitter, &QSplitter::splitterMoved, this, [this]() {\n        Core::Application::instance()->settings()->tocSplitterState = m_splitter->saveState();\n    });\n\n    // Setup main layout.\n    auto layout = WidgetUi::LayoutHelper::createBorderlessLayout<QVBoxLayout>();\n    layout->addWidget(m_splitter);\n    setLayout(layout);\n}\n\nContainer::~Container() = default;\n\nvoid Container::addView(View *view)\n{\n    if (m_views.contains(view))\n        return;\n\n    m_views.append(view);\n    m_splitter->addWidget(view);\n}\n"
  },
  {
    "path": "src/libs/sidebar/container.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_SIDEBAR_CONTAINER_H\n#define ZEAL_SIDEBAR_CONTAINER_H\n\n#include <QWidget>\n\nclass QSplitter;\n\nnamespace Zeal {\nnamespace Sidebar {\n\nclass View;\n\n// TODO: Implement view groups (alt. naming: tabs, pages) (move splitter into a group?).\nclass Container : public QWidget\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(Container)\npublic:\n    explicit Container(QWidget *parent = nullptr);\n    ~Container() override;\n\n    void addView(View *view);\n\nprivate:\n    QSplitter *m_splitter = nullptr;\n\n    QList<View *> m_views;\n};\n\n} // namespace Sidebar\n} // namespace Zeal\n\n#endif // ZEAL_SIDEBAR_CONTAINER_H\n"
  },
  {
    "path": "src/libs/sidebar/proxyview.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"proxyview.h\"\n\n#include \"viewprovider.h\"\n\n#include <ui/widgets/layouthelper.h>\n\n#include <QLoggingCategory>\n#include <QVBoxLayout>\n\n#include <utility>\n\nusing namespace Zeal;\nusing namespace Zeal::Sidebar;\n\nstatic Q_LOGGING_CATEGORY(log, \"zeal.sidebar.proxyview\")\n\nProxyView::ProxyView(ViewProvider *provider, QString id, QWidget *parent)\n    : View(parent)\n    , m_viewProvider(provider)\n    , m_viewId(std::move(id))\n{\n    setLayout(WidgetUi::LayoutHelper::createBorderlessLayout<QVBoxLayout>());\n\n    connect(m_viewProvider, &ViewProvider::viewChanged, this, [this]() {\n        auto view = m_viewProvider->view(m_viewId);\n        if (view == nullptr) {\n            qCWarning(log, \"ViewProvider returned invalid view for id '%s'.\", qPrintable(m_viewId));\n            return;\n        }\n\n        if (m_view == view)\n            return;\n\n        clearCurrentView();\n        layout()->addWidget(view);\n        view->show();\n        m_view = view;\n    });\n}\n\nProxyView::~ProxyView()\n{\n    clearCurrentView();\n}\n\nvoid ProxyView::clearCurrentView()\n{\n    // Unparent the view, because we don't own it.\n    QLayout *l = layout();\n    if (l->isEmpty() || m_view == nullptr) {\n        return;\n    }\n\n    m_view->hide();\n    l->removeWidget(m_view);\n    m_view->setParent(nullptr);\n    m_view = nullptr;\n}\n"
  },
  {
    "path": "src/libs/sidebar/proxyview.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_SIDEBAR_PROXYVIEW_H\n#define ZEAL_SIDEBAR_PROXYVIEW_H\n\n#include \"view.h\"\n\nnamespace Zeal {\nnamespace Sidebar {\n\nclass ViewProvider;\n\nclass ProxyView final : public View\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(ProxyView)\npublic:\n    explicit ProxyView(ViewProvider *provider, QString id = QString(), QWidget *parent = nullptr);\n    ~ProxyView() override;\n\nprivate:\n    void clearCurrentView();\n\n    ViewProvider *m_viewProvider = nullptr;\n    QString m_viewId;\n\n    View *m_view = nullptr;\n};\n\n} // namespace Sidebar\n} // namespace Zeal\n\n#endif // ZEAL_SIDEBAR_PROXYVIEW_H\n"
  },
  {
    "path": "src/libs/sidebar/view.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"view.h\"\n\nusing namespace Zeal::Sidebar;\n\nView::View(QWidget *parent)\n    : QWidget(parent)\n{\n}\n"
  },
  {
    "path": "src/libs/sidebar/view.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_SIDEBAR_VIEW_H\n#define ZEAL_SIDEBAR_VIEW_H\n\n#include <QWidget>\n\nnamespace Zeal {\nnamespace Sidebar {\n\nclass View : public QWidget\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(View)\npublic:\n    explicit View(QWidget *parent = nullptr);\n};\n\n} // namespace Sidebar\n} // namespace Zeal\n\n#endif // ZEAL_SIDEBAR_VIEW_H\n"
  },
  {
    "path": "src/libs/sidebar/viewprovider.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"viewprovider.h\"\n\nusing namespace Zeal::Sidebar;\n\nViewProvider::ViewProvider(QObject *parent)\n    : QObject(parent)\n{\n}\n"
  },
  {
    "path": "src/libs/sidebar/viewprovider.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_SIDEBAR_VIEWPROVIDER_H\n#define ZEAL_SIDEBAR_VIEWPROVIDER_H\n\n#include <QObject>\n\nnamespace Zeal {\nnamespace Sidebar {\n\nclass View;\n\nclass ViewProvider : public QObject\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(ViewProvider)\npublic:\n    explicit ViewProvider(QObject *parent = nullptr);\n\n    virtual View *view(const QString &id = QString()) const = 0;\n\nsignals:\n    void viewChanged();\n};\n\n} // namespace Sidebar\n} // namespace Zeal\n\n#endif // ZEAL_SIDEBAR_VIEWPROVIDER_H\n"
  },
  {
    "path": "src/libs/ui/CMakeLists.txt",
    "content": "add_subdirectory(qxtglobalshortcut)\nadd_subdirectory(widgets)\n\nset(Ui_FORMS\n    aboutdialog.ui\n    docsetsdialog.ui\n    settingsdialog.ui\n)\n\nadd_library(Ui STATIC\n    aboutdialog.cpp\n    browsertab.cpp\n    docsetlistitemdelegate.cpp\n    docsetsdialog.cpp\n    mainwindow.cpp\n    searchitemdelegate.cpp\n    searchsidebar.cpp\n    settingsdialog.cpp\n    sidebarviewprovider.cpp\n    ${Ui_FORMS} # For Qt Creator.\n)\n\ntarget_link_libraries(Ui PRIVATE Browser Sidebar QxtGlobalShortcut Widgets Registry Util)\n\nfind_package(Qt${QT_VERSION_MAJOR} COMPONENTS WebEngineWidgets REQUIRED)\ntarget_link_libraries(Ui PRIVATE Qt${QT_VERSION_MAJOR}::WebEngineWidgets)\n"
  },
  {
    "path": "src/libs/ui/aboutdialog.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"aboutdialog.h\"\n#include \"ui_aboutdialog.h\"\n\n#include <core/application.h>\n\nusing namespace Zeal::WidgetUi;\n\nAboutDialog::AboutDialog(QWidget *parent)\n    : QDialog(parent)\n    , ui(new Ui::AboutDialog)\n{\n    ui->setupUi(this);\n    setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);\n\n    ui->versionLabel->setText(Core::Application::versionString());\n    ui->buttonBox->setFocus(Qt::OtherFocusReason);\n}\n\nAboutDialog::~AboutDialog()\n{\n    delete ui;\n}\n"
  },
  {
    "path": "src/libs/ui/aboutdialog.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_WIDGETUI_ABOUTDIALOG_H\n#define ZEAL_WIDGETUI_ABOUTDIALOG_H\n\n#include <QDialog>\n\nnamespace Zeal {\nnamespace WidgetUi {\n\nnamespace Ui {\nclass AboutDialog;\n} // namespace Ui\n\nclass AboutDialog : public QDialog\n{\n    Q_OBJECT\npublic:\n    explicit AboutDialog(QWidget *parent = nullptr);\n    ~AboutDialog() override;\n\nprivate:\n    Ui::AboutDialog *ui;\n};\n\n} // namespace WidgetUi\n} // namespace Zeal\n\n#endif // ZEAL_WIDGETUI_ABOUTDIALOG_H\n"
  },
  {
    "path": "src/libs/ui/aboutdialog.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>Zeal::WidgetUi::AboutDialog</class>\n <widget class=\"QDialog\" name=\"Zeal::WidgetUi::AboutDialog\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>420</width>\n    <height>420</height>\n   </rect>\n  </property>\n  <property name=\"windowTitle\">\n   <string>About Zeal</string>\n  </property>\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n   <item>\n    <widget class=\"QFrame\" name=\"frame\">\n     <property name=\"frameShape\">\n      <enum>QFrame::StyledPanel</enum>\n     </property>\n     <property name=\"frameShadow\">\n      <enum>QFrame::Sunken</enum>\n     </property>\n     <layout class=\"QHBoxLayout\" name=\"horizontalLayout\" stretch=\"0,1\">\n      <property name=\"spacing\">\n       <number>10</number>\n      </property>\n      <item>\n       <widget class=\"QLabel\" name=\"label_2\">\n        <property name=\"text\">\n         <string/>\n        </property>\n        <property name=\"pixmap\">\n         <pixmap resource=\"../../app/resources/zeal.qrc\">:/icons/logo/64x64.png</pixmap>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\n        <property name=\"spacing\">\n         <number>0</number>\n        </property>\n        <item>\n         <widget class=\"QLabel\" name=\"nameLabel\">\n          <property name=\"text\">\n           <string>&lt;h1&gt;Zeal&lt;/h1&gt;</string>\n          </property>\n         </widget>\n        </item>\n        <item>\n         <widget class=\"QLabel\" name=\"versionLabel\">\n          <property name=\"text\">\n           <string/>\n          </property>\n          <property name=\"textInteractionFlags\">\n           <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>\n          </property>\n         </widget>\n        </item>\n       </layout>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <widget class=\"QTabWidget\" name=\"tabWidget\">\n     <property name=\"currentIndex\">\n      <number>0</number>\n     </property>\n     <widget class=\"QWidget\" name=\"tab\">\n      <attribute name=\"title\">\n       <string>About</string>\n      </attribute>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_3\">\n       <item>\n        <widget class=\"QLabel\" name=\"label\">\n         <property name=\"text\">\n          <string>&lt;strong&gt;A simple offline documentation browser&lt;/strong&gt;\n&lt;br&gt;&lt;br&gt;\nCopyright &amp;copy; Oleg Shparber and other contributors, 2013-2026.\n&lt;br&gt;\n&lt;a href=&quot;https://zealdocs.org&quot;&gt;zealdocs.org&lt;/a&gt;\n&lt;br&gt;\n&lt;a href=&quot;ircs://irc.libera.chat:6697/zealdocs&quot;&gt;#zealdocs&lt;/a&gt; on &lt;a href=&quot;https://libera.chat&quot;&gt;Libera Chat&lt;/a&gt;\n&lt;br&gt;&lt;br&gt;\nZeal is an open source software available under the terms of the General Public License version 3 (&lt;a href=&quot;https://www.gnu.org/copyleft/gpl.html&quot;&gt;GPLv3&lt;/a&gt;) or later.\n&lt;br&gt;&lt;br&gt;\nDocsets are courtesy of &lt;a href=&quot;https://kapeli.com/dash&quot;&gt;Dash&lt;/a&gt;.</string>\n         </property>\n         <property name=\"alignment\">\n          <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>\n         </property>\n         <property name=\"wordWrap\">\n          <bool>true</bool>\n         </property>\n         <property name=\"openExternalLinks\">\n          <bool>true</bool>\n         </property>\n        </widget>\n       </item>\n      </layout>\n     </widget>\n     <widget class=\"QWidget\" name=\"tab_2\">\n      <attribute name=\"title\">\n       <string>Licenses</string>\n      </attribute>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_4\">\n       <item>\n        <widget class=\"QTextBrowser\" name=\"textBrowser\">\n         <property name=\"html\">\n          <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;\n&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;\np, li { white-space: pre-wrap; }\n&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Roboto'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt;Zeal heavily relies on other open source software listed below.&lt;/span&gt;&lt;/p&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;&quot;&gt;cpp-httplib&lt;/span&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt; - &lt;/span&gt;&lt;a href=&quot;https://github.com/yhirose/cpp-httplib&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;&quot;&gt;github.com/yhirose/cpp-httplib&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt;&lt;br /&gt;License: MIT License&lt;/span&gt;&lt;/p&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;&quot;&gt;libarchive&lt;/span&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt; - &lt;/span&gt;&lt;a href=&quot;https://www.libarchive.org/&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;&quot;&gt;www.libarchive.org&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt;&lt;br /&gt;License: Simplified BSD License&lt;/span&gt;&lt;/p&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;&quot;&gt;LibQxt&lt;/span&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt; - &lt;/span&gt;&lt;a href=&quot;https://bitbucket.org/libqxt/libqxt&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;&quot;&gt;bitbucket.org/libqxt/libqxt&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt;&lt;br /&gt;License: New BSD License&lt;/span&gt;&lt;/p&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;&quot;&gt;Lucide&lt;/span&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt; - &lt;/span&gt;&lt;a href=&quot;https://lucide.dev/&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;&quot;&gt;lucide.dev&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt;&lt;br /&gt;License: ISC License&lt;/span&gt;&lt;/p&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;&quot;&gt;Oat&lt;/span&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt; - &lt;/span&gt;&lt;a href=&quot;https://oat.ink/&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;&quot;&gt;oat.ink&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt;&lt;br /&gt;License: MIT License&lt;/span&gt;&lt;/p&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;&quot;&gt;Qt&lt;/span&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt; - &lt;/span&gt;&lt;a href=&quot;https://www.qt.io/&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;&quot;&gt;www.qt.io&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt;&lt;br /&gt;License: GNU LGPL version 3&lt;/span&gt;&lt;/p&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;&quot;&gt;Simple Icons&lt;/span&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt; - &lt;/span&gt;&lt;a href=&quot;https://simpleicons.org/&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;&quot;&gt;simpleicons.org&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt;&lt;br /&gt;License: CC0 1.0 Universal&lt;/span&gt;&lt;/p&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;&quot;&gt;SQLite&lt;/span&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt; - &lt;/span&gt;&lt;a href=&quot;https://www.sqlite.org/&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;&quot;&gt;www.sqlite.org&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt;&quot;&gt;&lt;br /&gt;License: Public Domain&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n         </property>\n         <property name=\"openExternalLinks\">\n          <bool>true</bool>\n         </property>\n        </widget>\n       </item>\n      </layout>\n     </widget>\n    </widget>\n   </item>\n   <item>\n    <widget class=\"QDialogButtonBox\" name=\"buttonBox\">\n     <property name=\"standardButtons\">\n      <set>QDialogButtonBox::Close</set>\n     </property>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <resources>\n  <include location=\"../../app/resources/zeal.qrc\"/>\n </resources>\n <connections>\n  <connection>\n   <sender>buttonBox</sender>\n   <signal>clicked(QAbstractButton*)</signal>\n   <receiver>Zeal::WidgetUi::AboutDialog</receiver>\n   <slot>close()</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>248</x>\n     <y>254</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>157</x>\n     <y>274</y>\n    </hint>\n   </hints>\n  </connection>\n </connections>\n</ui>\n"
  },
  {
    "path": "src/libs/ui/browsertab.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"browsertab.h\"\n\n#include \"searchsidebar.h\"\n#include \"widgets/layouthelper.h\"\n#include \"widgets/toolbarframe.h\"\n\n#include <browser/webcontrol.h>\n#include <core/application.h>\n#include <core/settings.h>\n#include <registry/docset.h>\n#include <registry/docsetregistry.h>\n#include <registry/searchquery.h>\n\n#include <QApplication>\n#include <QLabel>\n#include <QMenu>\n#include <QStyle>\n#include <QToolButton>\n#include <QVBoxLayout>\n#include <QWebEngineHistory>\n\nusing namespace Zeal;\nusing namespace Zeal::WidgetUi;\n\nnamespace {\nconstexpr char WelcomePageUrl[] = \"qrc:///browser/welcome.html\";\n} // namespace\n\nBrowserTab::BrowserTab(QWidget *parent)\n    : QWidget(parent)\n{\n    // Setup WebControl.\n    m_webControl = new Browser::WebControl(this);\n    connect(m_webControl, &Browser::WebControl::titleChanged, this, &BrowserTab::titleChanged);\n    connect(m_webControl, &Browser::WebControl::urlChanged, this, [this](const QUrl &url) {\n        emit iconChanged(docsetIcon(url));\n\n        // Only update TOC if the base URL changed.\n        const QUrl baseUrl = url.adjusted(QUrl::RemoveFragment);\n        if (baseUrl != m_baseUrl) {\n            m_baseUrl = baseUrl;\n\n            Registry::Docset *docset = Core::Application::instance()->docsetRegistry()->docsetForUrl(url);\n            if (docset) {\n                searchSidebar()->pageTocModel()->setResults(docset->relatedLinks(url));\n                m_webControl->setJavaScriptEnabled(docset->isJavaScriptEnabled());\n            } else {\n                searchSidebar()->pageTocModel()->setResults();\n                // Always enable JS outside of docsets.\n                m_webControl->setJavaScriptEnabled(true);\n            }\n        }\n\n        m_backButton->setEnabled(m_webControl->canGoBack());\n        m_forwardButton->setEnabled(m_webControl->canGoForward());\n    });\n\n    // Setup navigation toolbar.\n    m_backButton = new QToolButton();\n    m_backButton->setAutoRaise(true);\n    m_backButton->setIcon(qApp->style()->standardIcon(QStyle::SP_ArrowBack));\n    m_backButton->setStyleSheet(QStringLiteral(\"QToolButton::menu-indicator { image: none; }\"));\n    m_backButton->setText(QStringLiteral(\"←\"));\n    m_backButton->setToolTip(tr(\"Go back one page\"));\n\n    auto backMenu = new QMenu(m_backButton);\n    connect(backMenu, &QMenu::aboutToShow, this, [this, backMenu]() {\n        backMenu->clear();\n        QWebEngineHistory *history = m_webControl->history();\n        QList<QWebEngineHistoryItem> items = history->backItems(10);\n        for (auto it = items.crbegin(); it != items.crend(); ++it) {\n            const QIcon icon = docsetIcon(it->url());\n            const QWebEngineHistoryItem item = *it;\n            backMenu->addAction(icon, it->title(), this, [=](bool) { history->goToItem(item); });\n        }\n    });\n    m_backButton->setMenu(backMenu);\n\n    connect(m_backButton, &QToolButton::clicked, m_webControl, &Browser::WebControl::back);\n\n    m_forwardButton = new QToolButton();\n    m_forwardButton->setAutoRaise(true);\n    m_forwardButton->setIcon(qApp->style()->standardIcon(QStyle::SP_ArrowForward));\n    m_forwardButton->setStyleSheet(QStringLiteral(\"QToolButton::menu-indicator { image: none; }\"));\n    m_forwardButton->setText(QStringLiteral(\"→\"));\n    m_forwardButton->setToolTip(tr(\"Go forward one page\"));\n\n    auto forwardMenu = new QMenu(m_forwardButton);\n    connect(forwardMenu, &QMenu::aboutToShow, this, [this, forwardMenu]() {\n        forwardMenu->clear();\n        QWebEngineHistory *history = m_webControl->history();\n        const auto forwardItems = history->forwardItems(10);\n        for (const QWebEngineHistoryItem &item : forwardItems) {\n            const QIcon icon = docsetIcon(item.url());\n            forwardMenu->addAction(icon, item.title(), this, [=](bool) { history->goToItem(item); });\n        }\n    });\n    m_forwardButton->setMenu(forwardMenu);\n\n    connect(m_forwardButton, &QToolButton::clicked, m_webControl, &Browser::WebControl::forward);\n\n    auto label = new QLabel();\n    label->setAlignment(Qt::AlignCenter);\n    connect(m_webControl, &Browser::WebControl::titleChanged, this, [label](const QString &title) {\n        if (title.isEmpty())\n            return;\n\n        label->setText(title);\n    });\n\n    auto toolBarLayout = new QHBoxLayout();\n    toolBarLayout->setContentsMargins(4, 0, 4, 0);\n    toolBarLayout->setSpacing(4);\n\n    toolBarLayout->addWidget(m_backButton);\n    toolBarLayout->addWidget(m_forwardButton);\n    toolBarLayout->addWidget(label, 1);\n\n    auto toolBarFrame = new ToolBarFrame();\n    toolBarFrame->setLayout(toolBarLayout);\n\n    // Setup main layout.\n    auto layout = LayoutHelper::createBorderlessLayout<QVBoxLayout>();\n    layout->addWidget(toolBarFrame);\n    layout->addWidget(m_webControl);\n    setLayout(layout);\n\n    auto registry = Core::Application::instance()->docsetRegistry();\n    using Registry::DocsetRegistry;\n    connect(registry, &DocsetRegistry::docsetAboutToBeUnloaded,\n            this, [this, registry](const QString &name) {\n        Registry::Docset *docset = registry->docsetForUrl(m_webControl->url());\n        if (docset == nullptr ||  docset->name() != name) {\n            return;\n        }\n\n\n        // TODO: Add custom 'Page has been removed' page.\n        navigateToStartPage();\n\n        // TODO: Cleanup history.\n    });\n}\n\nBrowserTab *BrowserTab::clone(QWidget *parent) const\n{\n    auto tab = new BrowserTab(parent);\n\n    if (m_searchSidebar) {\n        tab->m_searchSidebar = m_searchSidebar->clone();\n        connect(tab->m_searchSidebar, &SearchSidebar::activated,\n                tab->m_webControl, &Browser::WebControl::focus);\n        connect(tab->m_searchSidebar, &SearchSidebar::navigationRequested,\n                tab->m_webControl, &Browser::WebControl::load);\n    }\n\n    tab->m_webControl->restoreHistory(m_webControl->saveHistory());\n    tab->m_webControl->setZoomLevel(m_webControl->zoomLevel());\n\n    return tab;\n}\n\nBrowserTab::~BrowserTab()\n{\n    if (m_searchSidebar) {\n        // The sidebar is not in this widget's hierarchy, so direct delete is not safe.\n        m_searchSidebar->deleteLater();\n    }\n}\n\nBrowser::WebControl *BrowserTab::webControl() const\n{\n    return m_webControl;\n}\n\nSearchSidebar *BrowserTab::searchSidebar()\n{\n    if (m_searchSidebar == nullptr) {\n        // Create SearchSidebar managed by this tab.\n        m_searchSidebar = new SearchSidebar();\n        connect(m_searchSidebar, &SearchSidebar::activated,\n                m_webControl, &Browser::WebControl::focus);\n        connect(m_searchSidebar, &SearchSidebar::navigationRequested,\n                m_webControl, &Browser::WebControl::load);\n    }\n\n    return m_searchSidebar;\n}\n\nvoid BrowserTab::navigateToStartPage()\n{\n    m_webControl->load(QUrl(WelcomePageUrl));\n}\n\nvoid BrowserTab::search(const Registry::SearchQuery &query)\n{\n    if (query.isEmpty())\n        return;\n\n    m_searchSidebar->search(query);\n}\n\nQIcon BrowserTab::docsetIcon(const QUrl &url) const\n{\n    Registry::Docset *docset = Core::Application::instance()->docsetRegistry()->docsetForUrl(url);\n    return docset ? docset->icon() : QIcon(QStringLiteral(\":/icons/logo/icon.png\"));\n}\n"
  },
  {
    "path": "src/libs/ui/browsertab.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_WIDGETUI_BROWSERTAB_H\n#define ZEAL_WIDGETUI_BROWSERTAB_H\n\n#include <registry/searchmodel.h>\n\n#include <QModelIndexList>\n#include <QWidget>\n\nclass QToolButton;\n\nnamespace Zeal {\n\nnamespace Browser {\nclass WebControl;\n} // namespace Browser\n\nnamespace Registry {\nclass SearchQuery;\n} //namespace Registry\n\nnamespace WidgetUi {\n\nclass SearchSidebar;\n\nclass BrowserTab : public QWidget\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(BrowserTab)\npublic:\n    explicit BrowserTab(QWidget *parent = nullptr);\n    BrowserTab *clone(QWidget *parent = nullptr) const;\n    ~BrowserTab() override;\n\n    Browser::WebControl *webControl() const;\n    SearchSidebar *searchSidebar(); // TODO: const\n\npublic slots:\n    void navigateToStartPage();\n    void search(const Registry::SearchQuery &query);\n\nsignals:\n    void iconChanged(const QIcon &icon);\n    void titleChanged(const QString &title);\n\nprivate:\n    QIcon docsetIcon(const QUrl &url) const;\n\n    // Widgets.\n    SearchSidebar *m_searchSidebar = nullptr;\n    Browser::WebControl *m_webControl = nullptr;\n    QToolButton *m_backButton = nullptr;\n    QToolButton *m_forwardButton = nullptr;\n\n    // State.\n    QUrl m_baseUrl;\n};\n\n} // namespace WidgetUi\n} // namespace Zeal\n\n#endif // ZEAL_WIDGETUI_BROWSERTAB_H\n"
  },
  {
    "path": "src/libs/ui/docsetlistitemdelegate.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"docsetlistitemdelegate.h\"\n\n#include <registry/itemdatarole.h>\n\n#include <QPainter>\n#include <QProgressBar>\n\nusing namespace Zeal::WidgetUi;\n\nnamespace {\nconstexpr int ProgressBarWidth = 150;\n} // namespace\n\nDocsetListItemDelegate::DocsetListItemDelegate(QObject *parent)\n    : QStyledItemDelegate(parent)\n{\n}\n\nvoid DocsetListItemDelegate::paint(QPainter *painter,\n                                 const QStyleOptionViewItem &option,\n                                 const QModelIndex &index) const\n{\n    if (index.model()->data(index, ShowProgressRole).toBool()) {\n        paintProgressBar(painter, option, index);\n        return;\n    }\n\n    if (index.column() != Registry::SectionIndex::Actions) {\n        QStyledItemDelegate::paint(painter, option, index);\n        return;\n    }\n\n    QStyledItemDelegate::paint(painter, option, index);\n\n    if (!index.model()->data(index, Registry::ItemDataRole::UpdateAvailableRole).toBool()) {\n        return;\n    }\n\n    const QString text = tr(\"Update available\");\n\n    QFont font(painter->font());\n    font.setItalic(true);\n\n    const QFontMetrics fontMetrics(font);\n    const int margin = 4; // Random small number\n\n    QRect textRect = option.rect.adjusted(-margin, 0, -margin, 0);\n    textRect.setLeft(textRect.right() - fontMetrics.horizontalAdvance(text) - 2);\n    textRect = QStyle::visualRect(option.direction, option.rect, textRect);\n    // Constant LeftToRight because we don't need to flip it any further.\n    // Vertically align the text in the middle to match QCommonStyle behavior.\n    const auto alignedRect = QStyle::alignedRect(Qt::LeftToRight, option.displayAlignment,\n                                                 QSize(textRect.size().width(), fontMetrics.height()), textRect);\n\n    painter->save();\n\n    QPalette palette = option.palette;\n\n#ifdef Q_OS_WINDOWS\n    // QWindowsVistaStyle overrides highlight color.\n    if (option.widget->style()->objectName() == QLatin1String(\"windowsvista\")) {\n        palette.setColor(QPalette::All, QPalette::HighlightedText,\n                         palette.color(QPalette::Active, QPalette::Text));\n    }\n#endif\n\n    const QPalette::ColorGroup cg = (option.state & QStyle::State_Active)\n            ? QPalette::Normal : QPalette::Inactive;\n\n    if (option.state & QStyle::State_Selected) {\n        painter->setPen(palette.color(cg, QPalette::HighlightedText));\n    } else {\n        painter->setPen(palette.color(cg, QPalette::Text));\n    }\n\n    painter->setFont(font);\n    painter->drawText(alignedRect, text);\n\n    painter->restore();\n}\n\nvoid DocsetListItemDelegate::paintProgressBar(QPainter *painter,\n                                            const QStyleOptionViewItem &option,\n                                            const QModelIndex &index) const\n{\n    bool ok;\n    const int value = index.model()->data(index, ValueRole).toInt(&ok);\n\n    if (!ok) {\n        QStyledItemDelegate::paint(painter, option, index);\n        return;\n    }\n\n    // Adjust maximum text width\n    QStyleOptionViewItem styleOption = option;\n    styleOption.rect.setRight(styleOption.rect.right() - ProgressBarWidth);\n\n    // Size progress bar\n    QScopedPointer<QProgressBar> renderer(new QProgressBar());\n    renderer->resize(ProgressBarWidth, styleOption.rect.height());\n    renderer->setRange(0, 100);\n    renderer->setValue(value);\n\n    const QString format = index.model()->data(index, FormatRole).toString();\n    if (!format.isEmpty()) {\n        renderer->setFormat(format);\n    }\n\n    painter->save();\n\n    // Paint progress bar\n    painter->translate(styleOption.rect.topRight());\n    renderer->render(painter);\n\n    painter->restore();\n\n    QStyledItemDelegate::paint(painter, styleOption, index);\n}\n"
  },
  {
    "path": "src/libs/ui/docsetlistitemdelegate.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_WIDGETUI_DOCSETLISTITEMDELEGATE_H\n#define ZEAL_WIDGETUI_DOCSETLISTITEMDELEGATE_H\n\n#include <QStyledItemDelegate>\n\nnamespace Zeal {\nnamespace WidgetUi {\n\nclass DocsetListItemDelegate : public QStyledItemDelegate\n{\n    Q_OBJECT\npublic:\n    enum ProgressRoles {\n        ValueRole = Qt::UserRole + 10,\n        FormatRole,\n        ShowProgressRole\n    };\n\n    explicit DocsetListItemDelegate(QObject *parent = nullptr);\n\n    void paint(QPainter *painter,\n               const QStyleOptionViewItem &option,\n               const QModelIndex &index) const override;\n\nprivate:\n    void paintProgressBar(QPainter *painter,\n                          const QStyleOptionViewItem &option,\n                          const QModelIndex &index) const;\n};\n\n} // namespace WidgetUi\n} // namespace Zeal\n\n#endif // ZEAL_WIDGETUI_DOCSETLISTITEMDELEGATE_H\n"
  },
  {
    "path": "src/libs/ui/docsetsdialog.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"docsetsdialog.h\"\n#include \"ui_docsetsdialog.h\"\n\n#include \"docsetlistitemdelegate.h\"\n\n#include <core/application.h>\n#include <core/filemanager.h>\n#include <core/settings.h>\n#include <registry/docset.h>\n#include <registry/docsetregistry.h>\n#include <registry/itemdatarole.h>\n#include <util/humanizer.h>\n\n#include <QClipboard>\n#include <QDateTime>\n#include <QDir>\n#include <QInputDialog>\n#include <QJsonArray>\n#include <QJsonDocument>\n#include <QLoggingCategory>\n#include <QLocale>\n#include <QMessageBox>\n#include <QNetworkReply>\n#include <QNetworkRequest>\n#include <QTemporaryFile>\n#include <QUrl>\n\n#include <memory>\n\nusing namespace Zeal;\nusing namespace Zeal::WidgetUi;\n\nstatic Q_LOGGING_CATEGORY(log, \"zeal.widgetui.docsetsdialog\")\n\n#ifdef Q_OS_WINDOWS\nextern Q_CORE_EXPORT int qt_ntfs_permission_lookup;\n#endif\n\nnamespace {\nconstexpr char ApiServerUrl[] = \"https://api.zealdocs.org/v1\";\nconstexpr char RedirectServerUrl[] = \"https://go.zealdocs.org/d/%1/%2/latest\";\n// TODO: Each source plugin should have its own cache\nconstexpr char DocsetListCacheFileName[] = \"com.kapeli.json\";\n\n// TODO: Make the timeout period configurable\nconstexpr int CacheTimeout = 24 * 60 * 60; // 24 hours in seconds\n\n// QNetworkReply properties\nconstexpr char DocsetNameProperty[] = \"docsetName\";\nconstexpr char DownloadTypeProperty[] = \"downloadType\";\nconstexpr char ListItemIndexProperty[] = \"listItem\";\n} // namespace\n\nDocsetsDialog::DocsetsDialog(Core::Application *app, QWidget *parent)\n    : QDialog(parent)\n    , ui(new Ui::DocsetsDialog())\n    , m_application(app)\n    , m_docsetRegistry(app->docsetRegistry())\n{\n    ui->setupUi(this);\n    setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);\n\n    loadDocsetList();\n\n    m_isStorageReadOnly = !isDirWritable(m_application->settings()->docsetPath);\n\n#ifdef Q_OS_MACOS\n    ui->availableDocsetList->setAttribute(Qt::WA_MacShowFocusRect, false);\n    ui->installedDocsetList->setAttribute(Qt::WA_MacShowFocusRect, false);\n#endif\n\n    ui->statusLabel->clear(); // Clear text shown in the designer mode.\n    ui->storageStatusLabel->setVisible(m_isStorageReadOnly);\n\n    const QFileInfo fi(m_application->settings()->docsetPath);\n    ui->storageStatusLabel->setText(fi.exists() ? tr(\"<b>Docset storage is read only.</b>\")\n                                                : tr(\"<b>Docset storage does not exist.</b>\"));\n\n    connect(m_application, &Core::Application::extractionCompleted,\n            this, &DocsetsDialog::extractionCompleted);\n    connect(m_application, &Core::Application::extractionError,\n            this, &DocsetsDialog::extractionError);\n    connect(m_application, &Core::Application::extractionProgress,\n            this, &DocsetsDialog::extractionProgress);\n\n    // Setup signals & slots\n    connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton *button) {\n        if (button == ui->buttonBox->button(QDialogButtonBox::Cancel)) {\n            cancelDownloads();\n            return;\n        }\n\n        if (button == ui->buttonBox->button(QDialogButtonBox::Close)) {\n            close();\n            return;\n        }\n    });\n\n    setupInstalledDocsetsTab();\n    setupAvailableDocsetsTab();\n\n    if (m_isStorageReadOnly) {\n        disableControls();\n    }\n}\n\nDocsetsDialog::~DocsetsDialog()\n{\n    delete ui;\n}\n\nvoid DocsetsDialog::addDashFeed()\n{\n    QString clipboardText = QApplication::clipboard()->text();\n    if (!clipboardText.startsWith(QLatin1String(\"dash-feed://\")))\n        clipboardText.clear();\n\n    QString feedUrl = QInputDialog::getText(this, QStringLiteral(\"Zeal\"), tr(\"Feed URL:\"),\n                                            QLineEdit::Normal, clipboardText);\n    feedUrl = feedUrl.trimmed();\n    if (feedUrl.isEmpty())\n        return;\n\n    if (feedUrl.startsWith(QLatin1String(\"dash-feed://\"))) {\n        feedUrl = feedUrl.remove(0, 12);\n        feedUrl = QUrl::fromPercentEncoding(feedUrl.toUtf8());\n    }\n\n    QNetworkReply *reply = download(QUrl(feedUrl));\n    reply->setProperty(DownloadTypeProperty, DownloadDashFeed);\n}\n\nvoid DocsetsDialog::updateSelectedDocsets()\n{\n    const auto selectedRows = ui->installedDocsetList->selectionModel()->selectedRows();\n    for (const QModelIndex &index : selectedRows) {\n        if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool())\n            continue;\n\n        downloadDashDocset(index);\n    }\n}\n\nvoid DocsetsDialog::updateAllDocsets()\n{\n    QAbstractItemModel *model = ui->installedDocsetList->model();\n    for (int i = 0; i < model->rowCount(); ++i) {\n        const QModelIndex index = model->index(i, 0);\n        if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool())\n            continue;\n\n        downloadDashDocset(index);\n    }\n}\n\nvoid DocsetsDialog::removeSelectedDocsets()\n{\n    QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel();\n    if (!selectionModel->hasSelection())\n        return;\n\n    int ret;\n\n    const QModelIndexList selectedIndexes = selectionModel->selectedRows();\n    if (selectedIndexes.size() == 1) {\n        const QString docsetTitle = selectedIndexes.first().data().toString();\n        ret = QMessageBox::question(this, QStringLiteral(\"Zeal\"),\n                                    tr(\"Remove <b>%1</b> docset?\").arg(docsetTitle));\n    } else {\n        ret = QMessageBox::question(this, QStringLiteral(\"Zeal\"),\n                                    tr(\"Remove <b>%n</b> docset(s)?\", nullptr,\n                                       selectedIndexes.size()));\n    }\n\n    if (ret == QMessageBox::No) {\n        return;\n    }\n\n    // Gather names first, because model indicies become invalid when docsets are removed.\n    QStringList names;\n    for (const QModelIndex &index : selectedIndexes) {\n        names.append(index.data(Registry::ItemDataRole::DocsetNameRole).toString());\n    }\n\n    for (const QString &name : names) {\n        removeDocset(name);\n    }\n}\n\nvoid DocsetsDialog::updateDocsetFilter(const QString &filterString)\n{\n    const bool doSearch = !filterString.simplified().isEmpty();\n\n    for (int i = 0; i < ui->availableDocsetList->count(); ++i) {\n        QListWidgetItem *item = ui->availableDocsetList->item(i);\n\n        // Skip installed docsets\n        if (m_docsetRegistry->contains(item->data(Registry::ItemDataRole::DocsetNameRole).toString()))\n            continue;\n\n        item->setHidden(doSearch && !item->text().contains(filterString, Qt::CaseInsensitive));\n    }\n}\n\nvoid DocsetsDialog::downloadSelectedDocsets()\n{\n    QItemSelectionModel *selectionModel = ui->availableDocsetList->selectionModel();\n    const auto selectedRows = selectionModel->selectedRows();\n    for (const QModelIndex &index : selectedRows) {\n        selectionModel->select(index, QItemSelectionModel::Deselect);\n\n        // Do nothing if a download is already in progress.\n        if (index.data(DocsetListItemDelegate::ShowProgressRole).toBool())\n            continue;\n\n        QAbstractItemModel *model = ui->availableDocsetList->model();\n        model->setData(index, tr(\"Downloading: %p%\"), DocsetListItemDelegate::FormatRole);\n        model->setData(index, 0, DocsetListItemDelegate::ValueRole);\n        model->setData(index, true, DocsetListItemDelegate::ShowProgressRole);\n\n        downloadDashDocset(index);\n    }\n}\n\n/*!\n  \\internal\n  Should be connected to all \\l QNetworkReply::finished signals in order to process possible\n  HTTP-redirects correctly.\n*/\nvoid DocsetsDialog::downloadCompleted()\n{\n    QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(\n                qobject_cast<QNetworkReply *>(sender()));\n\n    m_replies.removeOne(reply.data());\n\n    if (reply->error() != QNetworkReply::NoError) {\n        if (reply->error() != QNetworkReply::OperationCanceledError) {\n            const QString msg = tr(\"Download failed!<br><br><b>Error:</b> %1<br><b>URL:</b> %2\")\n                    .arg(reply->errorString(), reply->request().url().toString());\n            const int ret = QMessageBox::warning(this, QStringLiteral(\"Zeal\"), msg,\n                                                 QMessageBox::Retry | QMessageBox::Cancel);\n\n            if (ret == QMessageBox::Retry) {\n                QNetworkReply *newReply = download(reply->request().url());\n\n                // Copy properties\n                newReply->setProperty(DocsetNameProperty, reply->property(DocsetNameProperty));\n                newReply->setProperty(DownloadTypeProperty, reply->property(DownloadTypeProperty));\n                newReply->setProperty(ListItemIndexProperty,\n                                      reply->property(ListItemIndexProperty));\n                return;\n            }\n\n            bool ok;\n            QListWidgetItem *listItem = ui->availableDocsetList->item(\n                        reply->property(ListItemIndexProperty).toInt(&ok));\n            if (ok && listItem)\n                listItem->setData(DocsetListItemDelegate::ShowProgressRole, false);\n        }\n\n        updateStatus();\n        return;\n    }\n\n    switch (reply->property(DownloadTypeProperty).toUInt()) {\n    case DownloadDocsetList: {\n        const QByteArray replyData = reply->readAll();\n\n        QScopedPointer<QFile> file(new QFile(cacheLocation(DocsetListCacheFileName)));\n        if (file->open(QIODevice::WriteOnly)) {\n            file->write(replyData);\n            file->close(); // Flush to ensure timestamp update on all systems.\n        }\n\n        updateDocsetListDownloadTimeLabel(QFileInfo(file->fileName()).lastModified());\n\n        QJsonParseError jsonError;\n        const QJsonDocument jsonDoc = QJsonDocument::fromJson(replyData, &jsonError);\n\n        if (jsonError.error != QJsonParseError::NoError) {\n            qCWarning(log, \"Failed to parse docset list JSON at offset %lld: %s.\",\n                      static_cast<long long>(jsonError.offset), qPrintable(jsonError.errorString()));\n            const QMessageBox::StandardButton rc\n                    = QMessageBox::warning(this, QStringLiteral(\"Zeal\"),\n                                           tr(\"Server returned a corrupted docset list.\"),\n                                           QMessageBox::Retry | QMessageBox::Cancel);\n\n            if (rc == QMessageBox::Retry) {\n                downloadDocsetList();\n            }\n\n            break;\n        }\n\n        processDocsetList(jsonDoc.array());\n        break;\n    }\n\n    case DownloadDashFeed: {\n        Registry::DocsetMetadata metadata\n                = Registry::DocsetMetadata::fromDashFeed(reply->request().url(), reply->readAll());\n\n        if (metadata.urls().isEmpty()) {\n            QMessageBox::warning(this, QStringLiteral(\"Zeal\"), tr(\"Invalid docset feed!\"));\n            break;\n        }\n\n        m_userFeeds[metadata.name()] = metadata;\n        Registry::Docset *docset = m_docsetRegistry->docset(metadata.name());\n        if (docset == nullptr) {\n            // Fetch docset only on first feed download,\n            // since further downloads are only update checks\n            QNetworkReply *mdReply = download(metadata.url());\n            mdReply->setProperty(DocsetNameProperty, metadata.name());\n            mdReply->setProperty(DownloadTypeProperty, DownloadDocset);\n        } else {\n            // Check for feed update\n            if (metadata.latestVersion() != docset->version()\n                    || metadata.revision() > docset->revision()) {\n                docset->hasUpdate = true;\n\n                if (!m_isStorageReadOnly) {\n                    ui->updateAllDocsetsButton->setEnabled(true);\n                }\n\n                ui->installedDocsetList->reset();\n            }\n        }\n\n        break;\n    }\n\n    case DownloadDocset: {\n        const QString docsetName = reply->property(DocsetNameProperty).toString();\n        const QString docsetDirectoryName = docsetName + QLatin1String(\".docset\");\n\n        if (QDir(m_application->settings()->docsetPath).exists(docsetDirectoryName)) {\n            removeDocset(docsetName);\n        }\n\n        QTemporaryFile *tmpFile = m_tmpFiles[docsetName];\n        if (!tmpFile) {\n            tmpFile = new QTemporaryFile(QStringLiteral(\"%1/%2.XXXXXX.tmp\").arg(Core::Application::cacheLocation(), docsetName), this);\n            if (!tmpFile->open())\n                return;\n            m_tmpFiles.insert(docsetName, tmpFile);\n        }\n\n        while (reply->bytesAvailable()) {\n            tmpFile->write(reply->read(1024 * 1024)); // Use small chunks.\n        }\n\n        tmpFile->close();\n\n        QListWidgetItem *item = findDocsetListItem(docsetName);\n        if (item) {\n            item->setData(DocsetListItemDelegate::ValueRole, 0);\n            item->setData(DocsetListItemDelegate::FormatRole, tr(\"Installing: %p%\"));\n        }\n\n        m_application->extract(tmpFile->fileName(), m_application->settings()->docsetPath,\n                               docsetDirectoryName);\n        break;\n    }\n    }\n\n    // If all enqueued downloads have finished executing.\n    updateStatus();\n}\n\n// creates a total download progress for multiple QNetworkReplies\nvoid DocsetsDialog::downloadProgress(qint64 received, qint64 total)\n{\n    // Don't show progress for non-docset pages\n    if (total == -1 || received < 10240)\n        return;\n\n    auto reply = qobject_cast<QNetworkReply *>(sender());\n    if (!reply || !reply->isOpen())\n        return;\n\n    if (reply->property(DownloadTypeProperty).toInt() == DownloadDocset) {\n        const QString docsetName = reply->property(DocsetNameProperty).toString();\n\n        QTemporaryFile *tmpFile = m_tmpFiles[docsetName];\n        if (!tmpFile) {\n            tmpFile = new QTemporaryFile(QStringLiteral(\"%1/%2.XXXXXX.tmp\").arg(Core::Application::cacheLocation(), docsetName), this);\n            if (!tmpFile->open())\n                return;\n            m_tmpFiles.insert(docsetName, tmpFile);\n        }\n\n        tmpFile->write(reply->read(received));\n    }\n\n    // Try to get the item associated to the request\n    QListWidgetItem *item\n            = ui->availableDocsetList->item(reply->property(ListItemIndexProperty).toInt());\n    if (item) {\n        item->setData(DocsetListItemDelegate::ValueRole, percent(received, total));\n    }\n}\n\nvoid DocsetsDialog::extractionCompleted(const QString &filePath)\n{\n    const QString docsetName = docsetNameForTmpFilePath(filePath);\n\n    const QDir dataDir(m_application->settings()->docsetPath);\n    const QString docsetPath = dataDir.filePath(docsetName + QLatin1String(\".docset\"));\n\n    // Write metadata about docset\n    Registry::DocsetMetadata metadata = m_availableDocsets.count(docsetName)\n            ? m_availableDocsets[docsetName] : m_userFeeds[docsetName];\n    metadata.save(docsetPath, metadata.latestVersion());\n\n    m_docsetRegistry->loadDocset(docsetPath);\n\n    QListWidgetItem *listItem = findDocsetListItem(docsetName);\n    if (listItem) {\n        listItem->setHidden(true);\n        listItem->setData(DocsetListItemDelegate::ShowProgressRole, false);\n    }\n\n    delete m_tmpFiles.take(docsetName);\n\n    updateStatus();\n}\n\nvoid DocsetsDialog::extractionError(const QString &filePath, const QString &errorString)\n{\n    const QString docsetName = docsetNameForTmpFilePath(filePath);\n\n    QMessageBox::warning(this, QStringLiteral(\"Zeal\"),\n                         tr(\"Cannot extract docset <b>%1</b>: %2\").arg(docsetName, errorString));\n\n    QListWidgetItem *listItem = findDocsetListItem(docsetName);\n    if (listItem)\n        listItem->setData(DocsetListItemDelegate::ShowProgressRole, false);\n\n    delete m_tmpFiles.take(docsetName);\n}\n\nvoid DocsetsDialog::extractionProgress(const QString &filePath, qint64 extracted, qint64 total)\n{\n    const QString docsetName = docsetNameForTmpFilePath(filePath);\n    QListWidgetItem *listItem = findDocsetListItem(docsetName);\n    if (listItem)\n        listItem->setData(DocsetListItemDelegate::ValueRole, percent(extracted, total));\n}\n\nvoid DocsetsDialog::loadDocsetList()\n{\n    loadUserFeedList();\n\n    const QFileInfo fi(cacheLocation(DocsetListCacheFileName));\n    if (!fi.exists()) {\n        downloadDocsetList();\n        return;\n    }\n\n    const auto age = fi.lastModified().secsTo(QDateTime::currentDateTime());\n    if (age < 0 || age >= CacheTimeout) {\n        downloadDocsetList();\n        return;\n    }\n\n    QScopedPointer<QFile> file(new QFile(fi.filePath()));\n    if (!file->open(QIODevice::ReadOnly)) {\n        downloadDocsetList();\n        return;\n    }\n\n    QJsonParseError jsonError;\n    const QJsonDocument jsonDoc = QJsonDocument::fromJson(file->readAll(), &jsonError);\n\n    if (jsonError.error != QJsonParseError::NoError) {\n        downloadDocsetList();\n        return;\n    }\n\n    updateDocsetListDownloadTimeLabel(fi.lastModified());\n    processDocsetList(jsonDoc.array());\n}\n\nvoid DocsetsDialog::setupInstalledDocsetsTab()\n{\n    ui->installedDocsetList->setItemDelegate(new DocsetListItemDelegate(this));\n    ui->installedDocsetList->setModel(m_docsetRegistry->model());\n\n    ui->installedDocsetList->setItemsExpandable(false);\n    ui->installedDocsetList->setRootIsDecorated(false);\n\n    ui->installedDocsetList->header()->setStretchLastSection(true);\n    ui->installedDocsetList->header()->setSectionsMovable(false);\n    ui->installedDocsetList->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);\n    ui->installedDocsetList->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);\n\n    if (m_isStorageReadOnly) {\n        return;\n    }\n\n    connect(ui->installedDocsetList, &QTreeView::activated, this, [this](const QModelIndex &index) {\n        if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) {\n            return;\n        }\n\n        downloadDashDocset(index);\n    });\n\n    QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel();\n    connect(selectionModel, &QItemSelectionModel::selectionChanged,\n            this, [this, selectionModel]() {\n        ui->removeDocsetsButton->setEnabled(selectionModel->hasSelection());\n\n        const auto selectedRows = selectionModel->selectedRows();\n        for (const QModelIndex &index : selectedRows) {\n            if (index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) {\n                ui->updateSelectedDocsetsButton->setEnabled(true);\n                return;\n            }\n        }\n\n        ui->updateSelectedDocsetsButton->setEnabled(false);\n    });\n\n    connect(ui->addFeedButton, &QPushButton::clicked, this, &DocsetsDialog::addDashFeed);\n\n    connect(ui->updateSelectedDocsetsButton, &QPushButton::clicked,\n            this, &DocsetsDialog::updateSelectedDocsets);\n    connect(ui->updateAllDocsetsButton, &QPushButton::clicked,\n            this, &DocsetsDialog::updateAllDocsets);\n    connect(ui->removeDocsetsButton, &QPushButton::clicked,\n            this, &DocsetsDialog::removeSelectedDocsets);\n}\n\nvoid DocsetsDialog::setupAvailableDocsetsTab()\n{\n    using Registry::DocsetRegistry;\n\n    ui->availableDocsetList->setItemDelegate(new DocsetListItemDelegate(this));\n\n    connect(m_docsetRegistry, &DocsetRegistry::docsetUnloaded, this, [this](const QString &name) {\n        QListWidgetItem *item = findDocsetListItem(name);\n        if (!item)\n            return;\n\n        item->setHidden(false);\n    });\n    connect(m_docsetRegistry, &DocsetRegistry::docsetLoaded, this, [this](const QString &name) {\n        QListWidgetItem *item = findDocsetListItem(name);\n        if (!item)\n            return;\n\n        item->setHidden(true);\n    });\n\n    connect(ui->refreshButton, &QPushButton::clicked, this, &DocsetsDialog::downloadDocsetList);\n\n    connect(ui->docsetFilterInput, &QLineEdit::textEdited,\n            this, &DocsetsDialog::updateDocsetFilter);\n\n    if (m_isStorageReadOnly) {\n        return;\n    }\n\n    connect(ui->availableDocsetList, &QListView::activated, this, [this](const QModelIndex &index) {\n        // TODO: Cancel download if it's already in progress.\n        if (index.data(DocsetListItemDelegate::ShowProgressRole).toBool())\n            return;\n\n        ui->availableDocsetList->selectionModel()->select(index, QItemSelectionModel::Deselect);\n\n        QAbstractItemModel *model = ui->availableDocsetList->model();\n        model->setData(index, tr(\"Downloading: %p%\"), DocsetListItemDelegate::FormatRole);\n        model->setData(index, 0, DocsetListItemDelegate::ValueRole);\n        model->setData(index, true, DocsetListItemDelegate::ShowProgressRole);\n\n        downloadDashDocset(index);\n    });\n\n    QItemSelectionModel *selectionModel = ui->availableDocsetList->selectionModel();\n    connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this, selectionModel]() {\n        const auto selectedRows = selectionModel->selectedRows();\n        for (const QModelIndex &index : selectedRows) {\n            if (!index.data(DocsetListItemDelegate::ShowProgressRole).toBool()) {\n                ui->downloadDocsetsButton->setEnabled(true);\n                return;\n            }\n        }\n\n        ui->downloadDocsetsButton->setEnabled(false);\n    });\n\n    connect(ui->downloadDocsetsButton, &QPushButton::clicked,\n            this, &DocsetsDialog::downloadSelectedDocsets);\n}\n\nvoid DocsetsDialog::enableControls()\n{\n    if (m_isStorageReadOnly || !m_replies.isEmpty() || !m_tmpFiles.isEmpty()) {\n        return;\n    }\n\n    // Dialog buttons.\n    ui->buttonBox->setStandardButtons(QDialogButtonBox::Close);\n\n    // Available docsets\n    ui->refreshButton->setEnabled(true);\n\n    // Installed docsets\n    ui->addFeedButton->setEnabled(true);\n    QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel();\n    bool hasSelectedUpdates = false;\n    const auto selectedRows = selectionModel->selectedRows();\n    for (const QModelIndex &index : selectedRows) {\n        if (index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) {\n            hasSelectedUpdates = true;\n            break;\n        }\n    }\n\n    ui->updateSelectedDocsetsButton->setEnabled(hasSelectedUpdates);\n    ui->updateAllDocsetsButton->setEnabled(updatesAvailable());\n    ui->removeDocsetsButton->setEnabled(selectionModel->hasSelection());\n}\n\nvoid DocsetsDialog::disableControls()\n{\n    // Dialog buttons.\n    if (!m_isStorageReadOnly) {\n        // Always show the close button if storage is read only.\n        ui->buttonBox->setStandardButtons(QDialogButtonBox::Cancel);\n    }\n\n    // Installed docsets\n    ui->addFeedButton->setEnabled(false);\n    ui->updateSelectedDocsetsButton->setEnabled(false);\n    ui->updateAllDocsetsButton->setEnabled(false);\n    ui->downloadDocsetsButton->setEnabled(false);\n    ui->removeDocsetsButton->setEnabled(false);\n\n    // Available docsets\n    ui->refreshButton->setEnabled(false);\n}\n\nQListWidgetItem *DocsetsDialog::findDocsetListItem(const QString &name) const\n{\n    for (int i = 0; i < ui->availableDocsetList->count(); ++i) {\n        QListWidgetItem *item = ui->availableDocsetList->item(i);\n\n        if (item->data(Registry::ItemDataRole::DocsetNameRole).toString() == name)\n            return item;\n    }\n\n    return nullptr;\n}\n\nbool DocsetsDialog::updatesAvailable() const\n{\n    const auto docsets = m_docsetRegistry->docsets();\n    for (Registry::Docset *docset : docsets) {\n        if (docset->hasUpdate)\n            return true;\n    }\n\n    return false;\n}\n\nQNetworkReply *DocsetsDialog::download(const QUrl &url)\n{\n    QNetworkReply *reply = m_application->download(url);\n    connect(reply, &QNetworkReply::downloadProgress, this, &DocsetsDialog::downloadProgress);\n    connect(reply, &QNetworkReply::finished, this, &DocsetsDialog::downloadCompleted);\n    m_replies.append(reply);\n\n    disableControls();\n    updateStatus();\n\n    return reply;\n}\n\nvoid DocsetsDialog::cancelDownloads()\n{\n    for (QNetworkReply *reply : std::as_const(m_replies)) {\n        // Hide progress bar\n        QListWidgetItem *listItem\n                = ui->availableDocsetList->item(reply->property(ListItemIndexProperty).toInt());\n        if (listItem)\n            listItem->setData(DocsetListItemDelegate::ShowProgressRole, false);\n\n        if (reply->property(DownloadTypeProperty).toInt() == DownloadDocset)\n            delete m_tmpFiles.take(reply->property(DocsetNameProperty).toString());\n\n        reply->abort();\n    }\n\n    updateStatus();\n}\n\nvoid DocsetsDialog::loadUserFeedList()\n{\n    const auto docsets = m_docsetRegistry->docsets();\n    for (Registry::Docset *docset : docsets) {\n        if (!docset->feedUrl().isEmpty()) {\n            QNetworkReply *reply = download(QUrl(docset->feedUrl()));\n            reply->setProperty(DownloadTypeProperty, DownloadDashFeed);\n        }\n    }\n}\n\nvoid DocsetsDialog::downloadDocsetList()\n{\n    ui->availableDocsetList->clear();\n    m_availableDocsets.clear();\n\n    QNetworkReply *reply = download(QUrl(ApiServerUrl + QLatin1String(\"/docsets\")));\n    reply->setProperty(DownloadTypeProperty, DownloadDocsetList);\n}\n\nvoid DocsetsDialog::processDocsetList(const QJsonArray &list)\n{\n    for (const QJsonValue &v : list) {\n        QJsonObject docsetJson = v.toObject();\n\n        Registry::DocsetMetadata metadata(docsetJson);\n        m_availableDocsets.insert({metadata.name(), metadata});\n    }\n\n    // TODO: Move into dedicated method\n    for (const auto &kv : m_availableDocsets) {\n        const auto &metadata = kv.second;\n\n        auto listItem = new QListWidgetItem(metadata.icon(), metadata.title(), ui->availableDocsetList);\n        listItem->setData(Registry::ItemDataRole::DocsetNameRole, metadata.name());\n\n        if (!m_docsetRegistry->contains(metadata.name())) {\n            continue;\n        }\n\n        listItem->setHidden(true);\n\n        Registry::Docset *docset = m_docsetRegistry->docset(metadata.name());\n\n        if (metadata.latestVersion() != docset->version()\n                || metadata.revision() > docset->revision()) {\n            docset->hasUpdate = true;\n\n            if (!m_isStorageReadOnly) {\n                ui->updateAllDocsetsButton->setEnabled(true);\n            }\n        }\n    }\n\n    ui->installedDocsetList->reset();\n\n    // Reapply the filter after repopulating the list.\n    updateDocsetFilter(ui->docsetFilterInput->text());\n}\n\nvoid DocsetsDialog::updateDocsetListDownloadTimeLabel(const QDateTime &modifiedTime)\n{\n    if (!modifiedTime.isValid()) {\n        ui->lastUpdatedLabel->clear();\n        ui->lastUpdatedLabel->setToolTip(QString());\n        return;\n    }\n\n    ui->lastUpdatedLabel->setText(tr(\"Last updated %1.\").arg(Util::Humanizer::fromNow(modifiedTime)));\n\n    const QString updateTime = modifiedTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));\n    ui->lastUpdatedLabel->setToolTip(updateTime);\n}\n\nvoid DocsetsDialog::downloadDashDocset(const QModelIndex &index)\n{\n    const QString name = index.data(Registry::ItemDataRole::DocsetNameRole).toString();\n\n    if (m_availableDocsets.count(name) == 0 && !m_userFeeds.contains(name))\n        return;\n\n    QUrl url;\n    if (!m_userFeeds.contains(name)) {\n        // No feed present means that this is a Kapeli docset\n        QString urlString = QString(RedirectServerUrl).arg(\"com.kapeli\").arg(name);\n        url = QUrl(urlString);\n    } else {\n        url = m_userFeeds[name].url();\n    }\n\n    QNetworkReply *reply = download(url);\n    reply->setProperty(DocsetNameProperty, name);\n    reply->setProperty(DownloadTypeProperty, DownloadDocset);\n    reply->setProperty(ListItemIndexProperty,\n                       ui->availableDocsetList->row(findDocsetListItem(name)));\n}\n\nvoid DocsetsDialog::removeDocset(const QString &name)\n{\n    if (m_docsetRegistry->contains(name)) {\n        m_docsetRegistry->unloadDocset(name);\n    }\n\n    const QString docsetPath\n            = QDir(m_application->settings()->docsetPath).filePath(name + QLatin1String(\".docset\"));\n    if (!m_application->fileManager()->removeRecursively(docsetPath)) {\n        const QString error = tr(\"Cannot remove directory <b>%1</b>! It might be in use\"\n                                 \" by another process.\").arg(docsetPath);\n        QMessageBox::warning(this, QStringLiteral(\"Zeal\"), error);\n        return;\n    }\n\n    QListWidgetItem *listItem = findDocsetListItem(name);\n    if (listItem) {\n        listItem->setHidden(false);\n    }\n}\n\nvoid DocsetsDialog::updateStatus()\n{\n    QString text;\n\n    if (!m_replies.isEmpty()) {\n        text = tr(\"Downloading: %n.\", nullptr, m_replies.size());\n    }\n\n    if (!m_tmpFiles.isEmpty()) {\n        text += QLatin1String(\" \") + tr(\"Installing: %n.\", nullptr, m_tmpFiles.size());\n    }\n\n    ui->statusLabel->setText(text);\n\n    enableControls();\n}\n\nQString DocsetsDialog::docsetNameForTmpFilePath(const QString &filePath) const\n{\n    for (auto it = m_tmpFiles.cbegin(), end = m_tmpFiles.cend(); it != end; ++it) {\n        if (it.value()->fileName() == filePath) {\n            return it.key();\n        }\n    }\n\n    return QString();\n}\n\nint DocsetsDialog::percent(qint64 fraction, qint64 total)\n{\n    if (!total)\n        return 0;\n\n    return static_cast<int>(fraction / static_cast<double>(total) * 100);\n}\n\nQString DocsetsDialog::cacheLocation(const QString &fileName)\n{\n    return QDir(Core::Application::cacheLocation()).filePath(fileName);\n}\n\nbool DocsetsDialog::isDirWritable(const QString &path)\n{\n    auto file = std::make_unique<QTemporaryFile>(path + QLatin1String(\"/.zeal_writable_check_XXXXXX.tmp\"));\n    return file->open();\n}\n"
  },
  {
    "path": "src/libs/ui/docsetsdialog.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_WIDGETUI_DOCSETSDIALOG_H\n#define ZEAL_WIDGETUI_DOCSETSDIALOG_H\n\n#include <registry/docsetmetadata.h>\n#include <util/caseinsensitivemap.h>\n\n#include <QDialog>\n#include <QHash>\n#include <QMap>\n\nclass QDateTime;\nclass QListWidgetItem;\nclass QNetworkReply;\nclass QTemporaryFile;\nclass QUrl;\n\nnamespace Zeal {\n\nnamespace Registry {\nclass DocsetRegistry;\n} // namespace Registry\n\nnamespace Core {\nclass Application;\n}\n\nnamespace WidgetUi {\n\nnamespace Ui {\nclass DocsetsDialog;\n} // namespace Ui\n\nclass DocsetsDialog : public QDialog\n{\n    Q_OBJECT\npublic:\n    explicit DocsetsDialog(Core::Application *app, QWidget *parent = nullptr);\n    ~DocsetsDialog() override;\n\nprivate slots:\n    void addDashFeed();\n    void updateSelectedDocsets();\n    void updateAllDocsets();\n    void removeSelectedDocsets();\n    void updateDocsetFilter(const QString &filterString);\n\n    void downloadSelectedDocsets();\n\n    void downloadCompleted();\n    void downloadProgress(qint64 received, qint64 total);\n\n    void extractionCompleted(const QString &filePath);\n    void extractionError(const QString &filePath, const QString &errorString);\n    void extractionProgress(const QString &filePath, qint64 extracted, qint64 total);\n\n    void loadDocsetList();\n\nprivate:\n    enum DownloadType {\n        DownloadDashFeed,\n        DownloadDocset,\n        DownloadDocsetList\n    };\n\n    Ui::DocsetsDialog *ui = nullptr;\n    Core::Application *m_application = nullptr;\n    Registry::DocsetRegistry *m_docsetRegistry = nullptr;\n\n    bool m_isStorageReadOnly = false;\n\n    QList<QNetworkReply *> m_replies;\n\n    // TODO: Create a special model\n    Util::CaseInsensitiveMap<Registry::DocsetMetadata> m_availableDocsets;\n    QMap<QString, Registry::DocsetMetadata> m_userFeeds;\n\n    QHash<QString, QTemporaryFile *> m_tmpFiles;\n\n    void setupInstalledDocsetsTab();\n    void setupAvailableDocsetsTab();\n\n    void enableControls();\n    void disableControls();\n\n    QListWidgetItem *findDocsetListItem(const QString &name) const;\n    bool updatesAvailable() const;\n\n    QNetworkReply *download(const QUrl &url);\n    void cancelDownloads();\n\n    void loadUserFeedList();\n    void downloadDocsetList();\n    void processDocsetList(const QJsonArray &list);\n    void updateDocsetListDownloadTimeLabel(const QDateTime &modifiedTime);\n\n    void downloadDashDocset(const QModelIndex &index);\n    void removeDocset(const QString &name);\n\n    void updateStatus();\n\n    // FIXME: Come up with a better approach\n    QString docsetNameForTmpFilePath(const QString &filePath) const;\n\n    static inline int percent(qint64 fraction, qint64 total);\n\n    static QString cacheLocation(const QString &fileName);\n    static bool isDirWritable(const QString &path);\n};\n\n} // namespace WidgetUi\n} // namespace Zeal\n\n#endif // ZEAL_WIDGETUI_DOCSETSDIALOG_H\n"
  },
  {
    "path": "src/libs/ui/docsetsdialog.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>Zeal::WidgetUi::DocsetsDialog</class>\n <widget class=\"QDialog\" name=\"Zeal::WidgetUi::DocsetsDialog\">\n  <property name=\"windowModality\">\n   <enum>Qt::ApplicationModal</enum>\n  </property>\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>600</width>\n    <height>500</height>\n   </rect>\n  </property>\n  <property name=\"windowTitle\">\n   <string>Docsets</string>\n  </property>\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n   <item>\n    <widget class=\"QTabWidget\" name=\"tabWidget\">\n     <property name=\"currentIndex\">\n      <number>0</number>\n     </property>\n     <widget class=\"QWidget\" name=\"installedDocsetsTab\">\n      <attribute name=\"title\">\n       <string>Installed</string>\n      </attribute>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_5\">\n       <item>\n        <widget class=\"QTreeView\" name=\"installedDocsetList\">\n         <property name=\"selectionMode\">\n          <enum>QAbstractItemView::ExtendedSelection</enum>\n         </property>\n         <property name=\"selectionBehavior\">\n          <enum>QAbstractItemView::SelectRows</enum>\n         </property>\n         <property name=\"iconSize\">\n          <size>\n           <width>16</width>\n           <height>16</height>\n          </size>\n         </property>\n        </widget>\n       </item>\n       <item>\n        <layout class=\"QHBoxLayout\" name=\"docsetsActions\">\n         <item>\n          <widget class=\"QPushButton\" name=\"addFeedButton\">\n           <property name=\"text\">\n            <string>Add feed</string>\n           </property>\n          </widget>\n         </item>\n         <item>\n          <spacer name=\"horizontalSpacer\">\n           <property name=\"orientation\">\n            <enum>Qt::Horizontal</enum>\n           </property>\n           <property name=\"sizeHint\" stdset=\"0\">\n            <size>\n             <width>40</width>\n             <height>20</height>\n            </size>\n           </property>\n          </spacer>\n         </item>\n         <item>\n          <widget class=\"QPushButton\" name=\"updateSelectedDocsetsButton\">\n           <property name=\"enabled\">\n            <bool>false</bool>\n           </property>\n           <property name=\"text\">\n            <string>Update</string>\n           </property>\n          </widget>\n         </item>\n         <item>\n          <widget class=\"QPushButton\" name=\"updateAllDocsetsButton\">\n           <property name=\"enabled\">\n            <bool>false</bool>\n           </property>\n           <property name=\"text\">\n            <string>Update all</string>\n           </property>\n          </widget>\n         </item>\n         <item>\n          <widget class=\"QPushButton\" name=\"removeDocsetsButton\">\n           <property name=\"enabled\">\n            <bool>false</bool>\n           </property>\n           <property name=\"text\">\n            <string>Remove</string>\n           </property>\n          </widget>\n         </item>\n        </layout>\n       </item>\n      </layout>\n     </widget>\n     <widget class=\"QWidget\" name=\"availableDocsetsTab\">\n      <attribute name=\"title\">\n       <string>Available</string>\n      </attribute>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\n       <item>\n        <widget class=\"QLineEdit\" name=\"docsetFilterInput\">\n         <property name=\"placeholderText\">\n          <string>Filter docsets</string>\n         </property>\n         <property name=\"clearButtonEnabled\">\n          <bool>true</bool>\n         </property>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QListWidget\" name=\"availableDocsetList\">\n         <property name=\"selectionMode\">\n          <enum>QAbstractItemView::ExtendedSelection</enum>\n         </property>\n         <property name=\"selectionBehavior\">\n          <enum>QAbstractItemView::SelectRows</enum>\n         </property>\n         <property name=\"iconSize\">\n          <size>\n           <width>16</width>\n           <height>16</height>\n          </size>\n         </property>\n        </widget>\n       </item>\n       <item>\n        <layout class=\"QHBoxLayout\" name=\"horizontalLayout\">\n         <item>\n          <widget class=\"QLabel\" name=\"lastUpdatedLabel\">\n           <property name=\"text\">\n            <string/>\n           </property>\n          </widget>\n         </item>\n         <item>\n          <widget class=\"QPushButton\" name=\"refreshButton\">\n           <property name=\"text\">\n            <string>Refresh</string>\n           </property>\n          </widget>\n         </item>\n         <item>\n          <spacer name=\"horizontalSpacer_2\">\n           <property name=\"orientation\">\n            <enum>Qt::Horizontal</enum>\n           </property>\n           <property name=\"sizeHint\" stdset=\"0\">\n            <size>\n             <width>40</width>\n             <height>20</height>\n            </size>\n           </property>\n          </spacer>\n         </item>\n         <item>\n          <widget class=\"QPushButton\" name=\"downloadDocsetsButton\">\n           <property name=\"enabled\">\n            <bool>false</bool>\n           </property>\n           <property name=\"text\">\n            <string>Download</string>\n           </property>\n          </widget>\n         </item>\n        </layout>\n       </item>\n       <item>\n        <widget class=\"QLabel\" name=\"label_4\">\n         <property name=\"text\">\n          <string>&lt;i&gt;Docsets are provided by &lt;a href=&quot;https://kapeli.com/dash&quot;&gt;Dash&lt;/a&gt;.&lt;/i&gt;</string>\n         </property>\n         <property name=\"textFormat\">\n          <enum>Qt::RichText</enum>\n         </property>\n         <property name=\"openExternalLinks\">\n          <bool>true</bool>\n         </property>\n        </widget>\n       </item>\n      </layout>\n     </widget>\n    </widget>\n   </item>\n   <item>\n    <layout class=\"QHBoxLayout\" name=\"horizontalLayout_2\">\n     <item>\n      <widget class=\"QLabel\" name=\"statusLabel\">\n       <property name=\"text\">\n        <string>Downloading: 1. Installing: 5.</string>\n       </property>\n      </widget>\n     </item>\n     <item>\n      <spacer name=\"horizontalSpacer_3\">\n       <property name=\"orientation\">\n        <enum>Qt::Horizontal</enum>\n       </property>\n       <property name=\"sizeHint\" stdset=\"0\">\n        <size>\n         <width>40</width>\n         <height>20</height>\n        </size>\n       </property>\n      </spacer>\n     </item>\n     <item>\n      <widget class=\"QLabel\" name=\"storageStatusLabel\"/>\n     </item>\n     <item>\n      <widget class=\"QDialogButtonBox\" name=\"buttonBox\">\n       <property name=\"sizePolicy\">\n        <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Fixed\">\n         <horstretch>0</horstretch>\n         <verstretch>0</verstretch>\n        </sizepolicy>\n       </property>\n       <property name=\"standardButtons\">\n        <set>QDialogButtonBox::Close</set>\n       </property>\n      </widget>\n     </item>\n    </layout>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "src/libs/ui/mainwindow.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"mainwindow.h\"\n\n#include \"aboutdialog.h\"\n#include \"browsertab.h\"\n#include \"docsetsdialog.h\"\n#include \"searchsidebar.h\"\n#include \"settingsdialog.h\"\n#include \"sidebarviewprovider.h\"\n#include <qxtglobalshortcut/qxtglobalshortcut.h>\n\n#include <browser/settings.h>\n#include <browser/webbridge.h>\n#include <browser/webcontrol.h>\n#include <core/application.h>\n#include <core/settings.h>\n#include <sidebar/container.h>\n#include <sidebar/proxyview.h>\n\n#include <QAction>\n#include <QApplication>\n#include <QCloseEvent>\n#include <QDesktopServices>\n#include <QFocusEvent>\n#include <QIcon>\n#include <QKeyEvent>\n#include <QMenu>\n#include <QMenuBar>\n#include <QMessageBox>\n#include <QMouseEvent>\n#include <QScopedPointer>\n#include <QShortcut>\n#include <QSplitter>\n#include <QStackedWidget>\n#include <QSystemTrayIcon>\n#include <QTabBar>\n#include <QVBoxLayout>\n\nusing namespace Zeal;\nusing namespace Zeal::WidgetUi;\n\nMainWindow::MainWindow(Core::Application *app, QWidget *parent)\n    : QMainWindow(parent)\n    , m_application(app)\n    , m_settings(app->settings())\n{\n#ifndef PORTABLE_BUILD\n    setWindowTitle(tr(\"Zeal\"));\n#else\n    setWindowTitle(tr(\"Zeal Portable\"));\n#endif\n    resize(900, 600); // Default size. May be overridden by restoreGeometry.\n\n    setupMainMenu();\n    setupShortcuts();\n    setupTabBar();\n\n    // Setup central widget.\n    auto centralWidget = new QWidget(this);\n    auto centralWidgetLayout = new QVBoxLayout(centralWidget);\n    centralWidgetLayout->setContentsMargins(0, 0, 0, 0);\n    centralWidgetLayout->setSpacing(0);\n    centralWidgetLayout->addWidget(m_tabBar);\n\n    m_splitter = new QSplitter(Qt::Horizontal, centralWidget);\n    m_splitter->setChildrenCollapsible(false);\n    centralWidgetLayout->addWidget(m_splitter);\n\n    m_webViewStack = new QStackedWidget(m_splitter);\n    m_webViewStack->setMinimumWidth(400);\n    m_splitter->addWidget(m_webViewStack);\n\n    setCentralWidget(centralWidget);\n\n    restoreGeometry(m_settings->windowGeometry);\n\n    // Update check\n    connect(m_application, &Core::Application::updateCheckError, this, [this](const QString &message) {\n        QMessageBox::warning(this, QStringLiteral(\"Zeal\"), message);\n    });\n\n    connect(m_application, &Core::Application::updateCheckDone, this, [this](const QString &version) {\n        if (version.isEmpty()) {\n            QMessageBox::information(this, QStringLiteral(\"Zeal\"),\n                                     tr(\"You are using the latest version.\"));\n            return;\n        }\n\n        // TODO: Remove this ugly workaround for #637.\n        qApp->setQuitOnLastWindowClosed(false);\n        const int ret\n                = QMessageBox::information(this, QStringLiteral(\"Zeal\"),\n                                           tr(\"Zeal <b>%1</b> is available. Open download page?\").arg(version),\n                                           QMessageBox::Yes | QMessageBox::No,\n                                           QMessageBox::Yes);\n        qApp->setQuitOnLastWindowClosed(true);\n\n        if (ret == QMessageBox::Yes) {\n            QDesktopServices::openUrl(QUrl(QStringLiteral(\"https://zealdocs.org/download.html\")));\n        }\n    });\n\n    // Setup sidebar.\n    auto sbViewProvider = new SidebarViewProvider(this);\n    auto sbView = new Sidebar::ProxyView(sbViewProvider, QStringLiteral(\"index\"));\n\n    auto sb = new Sidebar::Container();\n    sb->addView(sbView);\n\n    // Setup splitter.\n    m_splitter->insertWidget(0, sb);\n    m_splitter->restoreState(m_settings->verticalSplitterGeometry);\n\n    // Setup web settings.\n    auto webSettings = new Browser::Settings(m_settings, this);\n\n    // Setup web bridge.\n    m_webBridge = new Browser::WebBridge(this);\n    connect(m_webBridge, &Browser::WebBridge::actionTriggered, this, [this](const QString &action) {\n        // TODO: In the future connect directly to the ActionManager.\n        if (action == \"openDocsetManager\") {\n            m_showDocsetManagerAction->trigger();\n        } else if (action == \"openPreferences\") {\n            m_showPreferencesAction->trigger();\n        }\n    });\n\n    createTab();\n\n    connect(m_settings, &Core::Settings::updated, this, &MainWindow::applySettings);\n    applySettings();\n\n    if (m_settings->checkForUpdate) {\n        m_application->checkForUpdates(true);\n    }\n}\n\nMainWindow::~MainWindow()\n{\n    m_settings->verticalSplitterGeometry = m_splitter->saveState();\n    m_settings->windowGeometry = saveGeometry();\n}\n\nvoid MainWindow::search(const Registry::SearchQuery &query)\n{\n    if (auto tab = currentTab()) {\n        tab->search(query);\n    }\n}\n\nvoid MainWindow::closeTab(int index)\n{\n    if (index == -1) {\n        index = m_tabBar->currentIndex();\n    }\n\n    if (index == -1) {\n        return;\n    }\n\n    BrowserTab *tab = tabAt(index);\n    m_webViewStack->removeWidget(tab);\n    tab->deleteLater();\n\n    // Handle the tab bar last to avoid currentChanged signal coming too early.\n    m_tabBar->removeTab(index);\n\n    if (m_webViewStack->count() == 0) {\n        createTab();\n    }\n}\n\nvoid MainWindow::moveTab(int from, int to)\n{\n    const QSignalBlocker blocker(m_webViewStack);\n    QWidget *w = m_webViewStack->widget(from);\n    m_webViewStack->removeWidget(w);\n    m_webViewStack->insertWidget(to, w);\n}\n\nBrowserTab *MainWindow::createTab()\n{\n    auto tab = new BrowserTab();\n    tab->navigateToStartPage();\n    addTab(tab);\n    return tab;\n}\n\nvoid MainWindow::duplicateTab(int index)\n{\n    BrowserTab *tab = tabAt(index);\n    if (tab == nullptr)\n        return;\n\n    // Add a duplicate next to the `index`.\n    addTab(tab->clone(), index + 1);\n}\n\nvoid MainWindow::addTab(BrowserTab *tab, int index)\n{\n    connect(tab, &BrowserTab::iconChanged, this, [this, tab](const QIcon &icon) {\n        const int index = m_webViewStack->indexOf(tab);\n        Q_ASSERT(m_tabBar->tabData(index).value<BrowserTab *>() == tab);\n        m_tabBar->setTabIcon(index, icon);\n    });\n    connect(tab, &BrowserTab::titleChanged, this, [this, tab](const QString &title) {\n        if (title.isEmpty())\n            return;\n\n#ifndef PORTABLE_BUILD\n        setWindowTitle(QStringLiteral(\"%1 - Zeal\").arg(title));\n#else\n        setWindowTitle(QStringLiteral(\"%1 - Zeal Portable\").arg(title));\n#endif\n        const int index = m_webViewStack->indexOf(tab);\n        Q_ASSERT(m_tabBar->tabData(index).value<BrowserTab *>() == tab);\n        m_tabBar->setTabText(index, title);\n        m_tabBar->setTabToolTip(index, title);\n    });\n\n    tab->webControl()->setWebBridgeObject(\"zAppBridge\", m_webBridge);\n    tab->searchSidebar()->focusSearchEdit();\n\n    if (index == -1) {\n        index = m_settings->openNewTabAfterActive\n                ? m_tabBar->currentIndex() + 1\n                : m_webViewStack->count();\n    }\n\n    m_webViewStack->insertWidget(index, tab);\n    m_tabBar->insertTab(index, tr(\"Loading…\"));\n    m_tabBar->setCurrentIndex(index);\n    m_tabBar->setTabData(index, QVariant::fromValue(tab));\n}\n\nBrowserTab *MainWindow::currentTab() const\n{\n    return tabAt(m_tabBar->currentIndex());\n}\n\nBrowserTab *MainWindow::tabAt(int index) const\n{\n    return qobject_cast<BrowserTab *>(m_webViewStack->widget(index));\n}\n\nvoid MainWindow::setupMainMenu()\n{\n    m_menuBar = new QMenuBar(this);\n    m_menuBar->installEventFilter(this);\n\n    // TODO: [Qt 6.3] Refactor using addAction(text, shortcut, receiver, member).\n    // TODO: [Qt 6.7] Use QIcon::ThemeIcon.\n\n    // File Menu.\n    auto menu = m_menuBar->addMenu(tr(\"&File\"));\n\n    // -> New Tab Action.\n    // Not a standard icon, but it is often provided by GTK themes.\n    auto action = menu->addAction(\n        QIcon::fromTheme(QStringLiteral(\"tab-new\")),\n        tr(\"New &Tab\")\n    );\n    addAction(action);\n    action->setShortcut(QKeySequence::AddTab);\n    connect(action, &QAction::triggered, this, &MainWindow::createTab);\n\n    // -> Close Tab Action.\n    action = menu->addAction(tr(\"&Close Tab\"));\n    addAction(action);\n    action->setShortcut(QKeySequence(Qt::ControlModifier | Qt::Key_W));\n    connect(action, &QAction::triggered, this, [this]() { closeTab(); });\n\n    menu->addSeparator();\n\n    // -> Quit Action.\n    action = m_quitAction = menu->addAction(\n        QIcon::fromTheme(QStringLiteral(\"application-exit\")),\n        // Follow Windows HIG.\n#ifdef Q_OS_WINDOWS\n        tr(\"E&xit\"),\n#else\n        tr(\"&Quit\"),\n#endif\n        qApp, &QApplication::quit\n    );\n    addAction(action);\n    action->setMenuRole(QAction::QuitRole);\n\n    // Some platform plugins do not define QKeySequence::Quit.\n    if (QKeySequence(QKeySequence::Quit).isEmpty()) {\n        action->setShortcut(QStringLiteral(\"Ctrl+Q\"));\n    } else {\n        action->setShortcut(QKeySequence::Quit);\n    }\n\n    // Edit Menu.\n    menu = m_menuBar->addMenu(tr(\"&Edit\"));\n\n    // -> Find in Page Action.\n    action = menu->addAction(\n        QIcon::fromTheme(QStringLiteral(\"edit-find\")),\n        tr(\"&Find in Page\")\n    );\n    addAction(action);\n    action->setShortcut(QKeySequence::Find);\n    connect(action, &QAction::triggered, this, [this]() {\n        if (auto tab = currentTab()) {\n            tab->webControl()->activateSearchBar();\n        }\n    });\n\n    menu->addSeparator();\n\n    // -> Preferences Action.\n    // cspell:disable-next-line - cSpell does not like the ampersand.\n    action = m_showPreferencesAction = menu->addAction(tr(\"Prefere&nces\"));\n    addAction(action);\n    action->setMenuRole(QAction::PreferencesRole);\n\n    if (QKeySequence(QKeySequence::Preferences).isEmpty()) {\n        action->setShortcut(QStringLiteral(\"Ctrl+,\"));\n    } else {\n        action->setShortcut(QKeySequence::Preferences);\n    }\n\n    connect(action, &QAction::triggered, this, [this]() {\n        if (m_globalShortcut) {\n            m_globalShortcut->setEnabled(false);\n        }\n\n        QScopedPointer<SettingsDialog> dialog(new SettingsDialog(this));\n        dialog->exec();\n\n        if (m_globalShortcut) {\n            m_globalShortcut->setEnabled(true);\n        }\n    });\n\n    // Menu bar is global on MacOS, so it should always be visible.\n#ifndef Q_OS_MACOS\n    // View Menu.\n    menu = m_menuBar->addMenu(tr(\"&View\"));\n\n    // -> Toolbars Submenu.\n    auto subMenu = menu->addMenu(tr(\"&Toolbars\"));\n\n    // -> Toggle Toolbar Action.\n    action = m_showMenuBarAction = subMenu->addAction(tr(\"&Menu Bar\"));\n    addAction(action);\n    action->setCheckable(true);\n    action->setChecked(!m_settings->hideMenuBar);\n    action->setShortcut(QKeySequence(QStringLiteral(\"Ctrl+M\")));\n    connect(action, &QAction::toggled, this, [this](bool checked) {\n        m_menuBar->setVisible(checked);\n        m_settings->hideMenuBar = !checked;\n        m_settings->save();\n    });\n\n    // Set menu bar visibility.\n    m_menuBar->setVisible(m_showMenuBarAction->isChecked());\n\n    // Show and focus menu bar on F10.\n    auto focusMenu = new QShortcut(Qt::Key_F10, this);\n    connect(focusMenu, &QShortcut::activated, this, [this]() {\n        m_menuBar->setVisible(true);\n\n        m_menuBar->setFocus();\n        if (!m_menuBar->actions().isEmpty()) {\n            m_menuBar->setActiveAction(m_menuBar->actions().first());\n        }\n    });\n#endif\n\n    // Tools Menu.\n    menu = m_menuBar->addMenu(tr(\"&Tools\"));\n\n    // -> Docsets Action.\n    m_showDocsetManagerAction = menu->addAction(\n        tr(\"&Docsets…\"),\n        this, [this]() {\n            QScopedPointer<DocsetsDialog> dialog(new DocsetsDialog(m_application, this));\n            dialog->exec();\n        }\n    );\n\n    // Help Menu.\n    menu = m_menuBar->addMenu(tr(\"&Help\"));\n\n    // -> Submit Feedback Action.\n    menu->addAction(\n        tr(\"&Submit Feedback…\"),\n        this, [this]() {\n            QDesktopServices::openUrl(QUrl(QStringLiteral(\"https://go.zealdocs.org/l/report-bug\")));\n        }\n    );\n\n    // -> Check for Updates Action.\n    menu->addAction(tr(\"&Check for Updates…\"), this, [this]() {\n        m_application->checkForUpdates();\n    });\n\n    menu->addSeparator();\n\n    // -> About Action.\n    action = menu->addAction(\n        QIcon::fromTheme(QStringLiteral(\"help-about\")),\n        tr(\"&About Zeal\"),\n        this, [this]() {\n            QScopedPointer<AboutDialog> dialog(new AboutDialog(this));\n            dialog->exec();\n        }\n    );\n    addAction(action);\n    action->setMenuRole(QAction::AboutRole);\n\n    setMenuBar(m_menuBar);\n}\n\nvoid MainWindow::setupShortcuts()\n{\n    // Initialize the global shortcut handler if supported.\n    if (QxtGlobalShortcut::isSupported()) {\n        m_globalShortcut = new QxtGlobalShortcut(m_settings->showShortcut, this);\n        connect(m_globalShortcut, &QxtGlobalShortcut::activated, this, &MainWindow::toggleWindow);\n    }\n\n    // Focus search bar.\n    auto shortcut = new QShortcut(QStringLiteral(\"Ctrl+K\"), this);\n    connect(shortcut, &QShortcut::activated, this, [this]() {\n        if (auto tab = currentTab()) {\n            tab->searchSidebar()->focusSearchEdit();\n        }\n    });\n\n    shortcut = new QShortcut(QStringLiteral(\"Ctrl+L\"), this);\n    connect(shortcut, &QShortcut::activated, this, [this]() {\n        if (auto tab = currentTab()) {\n            tab->searchSidebar()->focusSearchEdit();\n        }\n    });\n\n    // Duplicate current tab.\n    shortcut = new QShortcut(QStringLiteral(\"Ctrl+Alt+T\"), this);\n    connect(shortcut, &QShortcut::activated, this, [this]() { duplicateTab(m_tabBar->currentIndex()); });\n\n    // Hide/show sidebar.\n    // TODO: Move to the View menu.\n    shortcut = new QShortcut(QStringLiteral(\"Ctrl+B\"), this);\n    connect(shortcut, &QShortcut::activated, this, [this]() {\n        auto sb = m_splitter->widget(0);\n        if (sb == nullptr) {\n            // This should not really happen.\n            return;\n        }\n\n        sb->setVisible(!sb->isVisible());\n    });\n\n    // Browser Shortcuts.\n    shortcut = new QShortcut(QKeySequence::Back, this);\n    connect(shortcut, &QShortcut::activated, this, [this]() {\n        if (auto tab = currentTab()) {\n            tab->webControl()->back();\n        }\n    });\n    shortcut = new QShortcut(QKeySequence::Forward, this);\n    connect(shortcut, &QShortcut::activated, this, [this]() {\n        if (auto tab = currentTab()) {\n            tab->webControl()->forward();\n        }\n    });\n    shortcut = new QShortcut(QKeySequence::ZoomIn, this);\n    connect(shortcut, &QShortcut::activated, this, [this]() {\n        if (auto tab = currentTab()) {\n            tab->webControl()->zoomIn();\n        }\n    });\n    shortcut = new QShortcut(QStringLiteral(\"Ctrl+=\"), this);\n    connect(shortcut, &QShortcut::activated, this, [this]() {\n        if (auto tab = currentTab()) {\n            tab->webControl()->zoomIn();\n        }\n    });\n    shortcut = new QShortcut(QKeySequence::ZoomOut, this);\n    connect(shortcut, &QShortcut::activated, this, [this]() {\n        if (auto tab = currentTab()) {\n            tab->webControl()->zoomOut();\n        }\n    });\n    shortcut = new QShortcut(QStringLiteral(\"Ctrl+0\"), this);\n    connect(shortcut, &QShortcut::activated, this, [this]() {\n        if (auto tab = currentTab()) {\n            tab->webControl()->resetZoom();\n        }\n    });\n\n    // TODO: Use QKeySequence::NextChild, when QTBUG-112193 is fixed.\n    QAction *action = new QAction(this);\n    addAction(action);\n    action->setShortcuts({QKeySequence(Qt::ControlModifier | Qt::Key_Tab),\n                          QKeySequence(Qt::ControlModifier | Qt::Key_PageDown)});\n    connect(action, &QAction::triggered, this, [this]() {\n        const int count = m_tabBar->count();\n        if (count > 0) {\n            m_tabBar->setCurrentIndex((m_tabBar->currentIndex() + 1) % count);\n        }\n    });\n\n    // TODO: Use QKeySequence::PreviousChild, when QTBUG-15746 and QTBUG-112193 are fixed.\n    action = new QAction(this);\n    addAction(action);\n    action->setShortcuts({QKeySequence(Qt::ControlModifier | Qt::ShiftModifier | Qt::Key_Tab),\n                          QKeySequence(Qt::ControlModifier | Qt::Key_PageUp)});\n    connect(action, &QAction::triggered, this, [this]() {\n        const int count = m_tabBar->count();\n        if (count > 0) {\n            m_tabBar->setCurrentIndex((m_tabBar->currentIndex() - 1 + count) % count);\n        }\n    });\n}\n\nvoid MainWindow::setupTabBar()\n{\n    m_tabBar = new QTabBar(this);\n    m_tabBar->installEventFilter(this);\n    m_tabBar->setDocumentMode(true);\n    m_tabBar->setElideMode(Qt::ElideRight);\n    m_tabBar->setExpanding(false);\n    m_tabBar->setMovable(true);\n    m_tabBar->setSelectionBehaviorOnRemove(QTabBar::SelectPreviousTab);\n    m_tabBar->setStyleSheet(QStringLiteral(\"QTabBar::tab { width: 150px; }\"));\n    m_tabBar->setTabsClosable(true);\n    m_tabBar->setUsesScrollButtons(true);\n\n    connect(m_tabBar, &QTabBar::currentChanged, this, [this](int index) {\n        if (index == -1) {\n            return;\n        }\n\n        BrowserTab *tab = tabAt(index);\n#ifndef PORTABLE_BUILD\n        setWindowTitle(QStringLiteral(\"%1 - Zeal\").arg(tab->webControl()->title()));\n#else\n        setWindowTitle(QStringLiteral(\"%1 - Zeal Portable\").arg(tab->webControl()->title()));\n#endif\n\n        m_webViewStack->setCurrentIndex(index);\n        emit currentTabChanged();\n    });\n    connect(m_tabBar, &QTabBar::tabCloseRequested, this, &MainWindow::closeTab);\n    connect(m_tabBar, &QTabBar::tabMoved, this, &MainWindow::moveTab);\n\n    for (int i = 1; i < 10; i++) {\n        auto action = new QAction(m_tabBar);\n#ifdef Q_OS_LINUX\n        action->setShortcut(QStringLiteral(\"Alt+%1\").arg(i));\n#else\n        action->setShortcut(QStringLiteral(\"Ctrl+%1\").arg(i));\n#endif\n        if (i == 9) {\n            connect(action, &QAction::triggered, this, [=]() {\n                m_tabBar->setCurrentIndex(m_tabBar->count() - 1);\n            });\n        } else {\n            connect(action, &QAction::triggered, this, [=]() {\n                m_tabBar->setCurrentIndex(i - 1);\n            });\n        }\n\n        addAction(action);\n    }\n\n    connect(m_tabBar, &QTabBar::tabBarDoubleClicked, this, [this](int index) {\n        if (index == -1) {\n            createTab();\n        }\n    });\n}\n\nvoid MainWindow::createTrayIcon()\n{\n    if (m_trayIcon)\n        return;\n\n    m_trayIcon = new QSystemTrayIcon(this);\n    m_trayIcon->setIcon(QIcon::fromTheme(QStringLiteral(\"zeal-tray\"), windowIcon()));\n    m_trayIcon->setToolTip(QStringLiteral(\"Zeal\"));\n\n    connect(m_trayIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) {\n        if (reason != QSystemTrayIcon::Trigger && reason != QSystemTrayIcon::DoubleClick)\n            return;\n\n        toggleWindow();\n    });\n\n    auto trayIconMenu = new QMenu(this);\n    QAction *toggleAction = trayIconMenu->addAction(tr(\"Show Zeal\"),\n                                                    this, &MainWindow::toggleWindow);\n\n    connect(trayIconMenu, &QMenu::aboutToShow, this, [this, toggleAction]() {\n        toggleAction->setText(isVisible() ? tr(\"Minimize to Tray\") : tr(\"Show Zeal\"));\n    });\n\n    trayIconMenu->addSeparator();\n    trayIconMenu->addAction(m_quitAction);\n    m_trayIcon->setContextMenu(trayIconMenu);\n\n    m_trayIcon->show();\n}\n\nvoid MainWindow::removeTrayIcon()\n{\n    if (!m_trayIcon)\n        return;\n\n    QMenu *trayIconMenu = m_trayIcon->contextMenu();\n    delete m_trayIcon;\n    m_trayIcon = nullptr;\n    delete trayIconMenu;\n}\n\nvoid MainWindow::bringToFront()\n{\n    show();\n    setWindowState((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive);\n    raise();\n    activateWindow();\n\n    if (auto tab = currentTab()) {\n        tab->searchSidebar()->focusSearchEdit();\n    }\n}\n\nvoid MainWindow::changeEvent(QEvent *event)\n{\n    if (m_settings->showSystrayIcon && m_settings->minimizeToSystray\n            && event->type() == QEvent::WindowStateChange && isMinimized()) {\n        hide();\n    }\n\n    QMainWindow::changeEvent(event);\n}\n\nvoid MainWindow::closeEvent(QCloseEvent *event)\n{\n    if (m_settings->showSystrayIcon && m_settings->hideOnClose) {\n        event->ignore();\n        toggleWindow();\n    }\n}\n\nbool MainWindow::eventFilter(QObject *object, QEvent *event)\n{\n    if (object == m_tabBar) {\n        switch (event->type()) {\n        case QEvent::MouseButtonRelease: {\n            auto e = static_cast<QMouseEvent *>(event);\n            if (e->button() == Qt::MiddleButton) {\n                const int index = m_tabBar->tabAt(e->pos());\n                if (index != -1) {\n                    closeTab(index);\n                    return true;\n                }\n            }\n            break;\n        }\n        case QEvent::Wheel:\n            // TODO: Remove in case QTBUG-8428 is fixed on all platforms\n            return true;\n        default:\n            break;\n        }\n    }\n\n#ifndef Q_OS_MACOS\n    if (object == m_menuBar\n        && m_menuBar->isVisible()\n        && m_showMenuBarAction != nullptr\n        && !m_showMenuBarAction->isChecked()) {\n        switch (event->type()) {\n        // Hide menu bar when it loses focus.\n        case QEvent::FocusOut: {\n            auto e = static_cast<QFocusEvent *>(event);\n            if (e->reason() != Qt::PopupFocusReason) {\n                m_menuBar->hide();\n            }\n\n            break;\n        }\n        // Hide menu bar on Escape key press.\n        case QEvent::KeyPress: {\n            auto e = static_cast<QKeyEvent *>(event);\n            if (e->key() == Qt::Key_Escape) {\n                m_menuBar->hide();\n            }\n\n            break;\n        }\n\n        default:\n            break;\n        }\n    }\n#endif\n\n    return QMainWindow::eventFilter(object, event);\n}\n\n// Captures global events in order to pass them to the search bar.\nvoid MainWindow::keyPressEvent(QKeyEvent *keyEvent)\n{\n    switch (keyEvent->key()) {\n    case Qt::Key_Escape:\n        if (auto tab = currentTab()) {\n            tab->searchSidebar()->focusSearchEdit(true);\n        }\n        break;\n    case Qt::Key_Question:\n        if (auto tab = currentTab()) {\n            tab->searchSidebar()->focusSearchEdit();\n        }\n        break;\n    default:\n        QMainWindow::keyPressEvent(keyEvent);\n        break;\n    }\n}\n\nvoid MainWindow::applySettings()\n{\n    if (m_globalShortcut) {\n        m_globalShortcut->setShortcut(m_settings->showShortcut);\n    }\n\n    if (m_settings->showSystrayIcon)\n        createTrayIcon();\n    else\n        removeTrayIcon();\n}\n\nvoid MainWindow::toggleWindow()\n{\n    const bool checkActive = m_globalShortcut && sender() == m_globalShortcut;\n\n    if (!isVisible() || (checkActive && !isActiveWindow())) {\n        bringToFront();\n    } else {\n        if (m_trayIcon) {\n            hide();\n        } else {\n            showMinimized();\n        }\n    }\n}\n"
  },
  {
    "path": "src/libs/ui/mainwindow.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_WIDGETUI_MAINWINDOW_H\n#define ZEAL_WIDGETUI_MAINWINDOW_H\n\n#include <QMainWindow>\n\nclass QxtGlobalShortcut;\n\nclass QAction;\nclass QMenuBar;\nclass QSplitter;\nclass QStackedWidget;\nclass QSystemTrayIcon;\nclass QTabBar;\n\nnamespace Zeal {\n\nnamespace Browser {\nclass WebBridge;\n} // namespace Browser\n\nnamespace Core {\nclass Application;\nclass Settings;\n} // namespace Core\n\nnamespace Registry {\nclass SearchQuery;\n} //namespace Registry\n\nnamespace WidgetUi {\n\nclass BrowserTab;\nclass SidebarViewProvider;\n\nclass MainWindow : public QMainWindow\n{\n    Q_OBJECT\npublic:\n    explicit MainWindow(Core::Application *app, QWidget *parent = nullptr);\n    ~MainWindow() override;\n\n    void search(const Registry::SearchQuery &query);\n    void bringToFront();\n    BrowserTab *createTab();\n\npublic slots:\n    void toggleWindow();\n\nsignals:\n    void currentTabChanged();\n\nprotected:\n    void changeEvent(QEvent *event) override;\n    void closeEvent(QCloseEvent *event) override;\n    bool eventFilter(QObject *object, QEvent *event) override;\n    void keyPressEvent(QKeyEvent *keyEvent) override;\n\nprivate slots:\n    void applySettings();\n    void closeTab(int index = -1);\n    void moveTab(int from, int to);\n    void duplicateTab(int index);\n\nprivate:\n    void setupMainMenu();\n    void setupShortcuts();\n    void setupTabBar();\n\n    void addTab(BrowserTab *tab, int index = -1);\n    BrowserTab *currentTab() const;\n    BrowserTab *tabAt(int index) const;\n\n    void createTrayIcon();\n    void removeTrayIcon();\n\n    void syncTabState(BrowserTab *tab);\n\n    Core::Application *m_application = nullptr;\n    Core::Settings *m_settings = nullptr;\n\n    Browser::WebBridge *m_webBridge = nullptr;\n\n    QxtGlobalShortcut *m_globalShortcut = nullptr;\n\n    QMenuBar *m_menuBar = nullptr;\n    QTabBar *m_tabBar = nullptr;\n\n    QSplitter *m_splitter = nullptr;\n    QStackedWidget *m_webViewStack = nullptr;\n\n    // TODO: Replace with proper action manager.\n    QAction *m_quitAction = nullptr;\n    QAction *m_showDocsetManagerAction = nullptr;\n    QAction *m_showPreferencesAction = nullptr;\n#ifndef Q_OS_MACOS\n    QAction *m_showMenuBarAction = nullptr;\n#endif\n\n    friend class SidebarViewProvider;\n\n    QSystemTrayIcon *m_trayIcon = nullptr;\n};\n\n} // namespace WidgetUi\n} // namespace Zeal\n\n#endif // ZEAL_WIDGETUI_MAINWINDOW_H\n"
  },
  {
    "path": "src/libs/ui/qxtglobalshortcut/CMakeLists.txt",
    "content": "list(APPEND QxtGlobalShortcut_SOURCES\n    qxtglobalshortcut.cpp\n)\n\nif(APPLE)\n    list(APPEND QxtGlobalShortcut_SOURCES\n        qxtglobalshortcut_mac.cpp\n    )\nelseif(UNIX)\n    find_package(X11)\n    if(X11_FOUND)\n        list(APPEND QxtGlobalShortcut_SOURCES\n            qxtglobalshortcut_x11.cpp\n        )\n    else()\n        list(APPEND QxtGlobalShortcut_SOURCES\n            qxtglobalshortcut_noop.cpp\n        )\n    endif()\nelseif(WIN32)\n    list(APPEND QxtGlobalShortcut_SOURCES\n        qxtglobalshortcut_win.cpp\n    )\nelse()\n    list(APPEND QxtGlobalShortcut_SOURCES\n        qxtglobalshortcut_noop.cpp\n    )\nendif()\n\nadd_library(QxtGlobalShortcut STATIC ${QxtGlobalShortcut_SOURCES})\n\nfind_package(Qt${QT_VERSION_MAJOR} COMPONENTS Gui REQUIRED)\ntarget_link_libraries(QxtGlobalShortcut PRIVATE Qt${QT_VERSION_MAJOR}::Gui)\n\nif(APPLE)\n    find_library(CARBON_LIBRARY Carbon)\n    target_link_libraries(QxtGlobalShortcut PRIVATE ${CARBON_LIBRARY})\nelseif(UNIX AND X11_FOUND)\n    target_link_libraries(QxtGlobalShortcut PRIVATE ${X11_LIBRARIES})\n\n    if(QT_VERSION_MAJOR EQUAL 5)\n        find_package(Qt5 COMPONENTS X11Extras REQUIRED)\n        target_link_libraries(QxtGlobalShortcut PRIVATE Qt5::X11Extras)\n    else()\n        if(Qt6Core_VERSION VERSION_GREATER_EQUAL 6.10)\n            find_package(Qt6 COMPONENTS GuiPrivate REQUIRED)\n        endif()\n        target_link_libraries(QxtGlobalShortcut PRIVATE Qt${QT_VERSION_MAJOR}::GuiPrivate)\n    endif()\n\n    find_package(ECM REQUIRED NO_MODULE)\n    set(CMAKE_MODULE_PATH ${ECM_FIND_MODULE_DIR})\n\n    find_package(XCB COMPONENTS XCB KEYSYMS REQUIRED)\n    target_link_libraries(QxtGlobalShortcut PRIVATE XCB::XCB XCB::KEYSYMS)\nendif()\n"
  },
  {
    "path": "src/libs/ui/qxtglobalshortcut/qxtglobalshortcut.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n/****************************************************************************\n// Copyright (C) 2006 - 2011, the LibQxt project.\n** See the Qxt AUTHORS file for a list of authors and copyright holders.\n** All rights reserved.\n**\n** Redistribution and use in source and binary forms, with or without\n** modification, are permitted provided that the following conditions are met:\n**     * Redistributions of source code must retain the above copyright\n**       notice, this list of conditions and the following disclaimer.\n**     * Redistributions in binary form must reproduce the above copyright\n**       notice, this list of conditions and the following disclaimer in the\n**       documentation and/or other materials provided with the distribution.\n**     * Neither the name of the LibQxt project nor the\n**       names of its contributors may be used to endorse or promote products\n**       derived from this software without specific prior written permission.\n**\n** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n** DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY\n** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\n** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n**\n** <http://libqxt.org>  <foundation@libqxt.org>\n*****************************************************************************/\n\n#include \"qxtglobalshortcut.h\"\n#include \"qxtglobalshortcut_p.h\"\n\n#include <QAbstractEventDispatcher>\n#include <QLoggingCategory>\n\nstatic Q_LOGGING_CATEGORY(log, \"zeal.ui.qxtglobalshortcut\")\n\n#ifndef Q_OS_MACOS\nint QxtGlobalShortcutPrivate::ref = 0;\n#endif // Q_OS_MACOS\n\nQHash<QPair<quint32, quint32>, QxtGlobalShortcut *> QxtGlobalShortcutPrivate::shortcuts;\n\nQxtGlobalShortcutPrivate::QxtGlobalShortcutPrivate(QxtGlobalShortcut *qq)\n    : q_ptr(qq)\n{\n#ifndef Q_OS_MACOS\n    if (ref == 0)\n        QAbstractEventDispatcher::instance()->installNativeEventFilter(this);\n    ++ref;\n#endif // Q_OS_MACOS\n}\n\nQxtGlobalShortcutPrivate::~QxtGlobalShortcutPrivate()\n{\n#ifndef Q_OS_MACOS\n    --ref;\n    if (ref == 0) {\n        QAbstractEventDispatcher *ed = QAbstractEventDispatcher::instance();\n        if (ed != nullptr) {\n            ed->removeNativeEventFilter(this);\n        }\n    }\n#endif // Q_OS_MACOS\n}\n\nbool QxtGlobalShortcutPrivate::setShortcut(const QKeySequence &shortcut)\n{\n    Q_Q(QxtGlobalShortcut);\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n    const int combination = shortcut[0];\n#else\n    const int combination = shortcut[0].toCombined();\n#endif\n\n    key = shortcut.isEmpty() ? Qt::Key(0) : Qt::Key(combination & ~Qt::KeyboardModifierMask);\n    mods = shortcut.isEmpty() ? Qt::NoModifier : Qt::KeyboardModifiers(combination & Qt::KeyboardModifierMask);\n    const quint32 nativeKey = nativeKeycode(key);\n    const quint32 nativeMods = nativeModifiers(mods);\n    const bool res = registerShortcut(nativeKey, nativeMods);\n    if (!res) {\n        qCWarning(log, \"Failed to register '%s' shortcut.\", qPrintable(QKeySequence(key | mods).toString()));\n        return false;\n    }\n\n    shortcuts.insert({nativeKey, nativeMods}, q);\n\n    return true;\n}\n\nbool QxtGlobalShortcutPrivate::unsetShortcut()\n{\n    Q_Q(QxtGlobalShortcut);\n\n    const quint32 nativeKey = nativeKeycode(key);\n    const quint32 nativeMods = nativeModifiers(mods);\n\n    if (shortcuts.value({nativeKey, nativeMods}) != q) {\n        qCWarning(log, \"Tried to unregister unowned '%s' shortcut.\", qPrintable(QKeySequence(key | mods).toString()));\n        return false;\n    }\n\n    const bool res = unregisterShortcut(nativeKey, nativeMods);\n    if (!res) {\n        qCWarning(log, \"Failed to unregister '%s' shortcut.\", qPrintable(QKeySequence(key | mods).toString()));\n        return false;\n    }\n\n    shortcuts.remove({nativeKey, nativeMods});\n\n    key = Qt::Key(0);\n    mods = Qt::KeyboardModifiers(Qt::NoModifier);\n\n    return true;\n}\n\nbool QxtGlobalShortcutPrivate::activateShortcut(quint32 nativeKey, quint32 nativeMods)\n{\n    QxtGlobalShortcut *shortcut = shortcuts.value({nativeKey, nativeMods});\n    if (!shortcut || !shortcut->isEnabled())\n        return false;\n\n    emit shortcut->activated();\n    return true;\n}\n\n/*!\n    \\class QxtGlobalShortcut\n    \\inmodule QxtWidgets\n    \\brief The QxtGlobalShortcut class provides a global shortcut aka \"hotkey\".\n\n    A global shortcut triggers even if the application is not active. This\n    makes it easy to implement applications that react to certain shortcuts\n    still if some other application is active or if the application is for\n    example minimized to the system tray.\n\n    Example usage:\n    \\code\n    QxtGlobalShortcut *shortcut = new QxtGlobalShortcut(window);\n    connect(shortcut, SIGNAL(activated()), window, SLOT(toggleVisibility()));\n    shortcut->setShortcut(QKeySequence(\"Ctrl+Shift+F12\"));\n    \\endcode\n\n    \\bold {Note:} Since Qxt 0.6 QxtGlobalShortcut no more requires QxtApplication.\n */\n\n/*!\n    \\fn QxtGlobalShortcut::activated()\n\n    This signal is emitted when the user types the shortcut's key sequence.\n\n    \\sa shortcut\n */\n\n/*!\n    Constructs a new QxtGlobalShortcut with \\a parent.\n */\nQxtGlobalShortcut::QxtGlobalShortcut(QObject *parent)\n    : QObject(parent)\n    , d_ptr(new QxtGlobalShortcutPrivate(this))\n{\n}\n\n/*!\n    Constructs a new QxtGlobalShortcut with \\a shortcut and \\a parent.\n */\nQxtGlobalShortcut::QxtGlobalShortcut(const QKeySequence &shortcut, QObject *parent)\n    : QObject(parent)\n    , d_ptr(new QxtGlobalShortcutPrivate(this))\n{\n    setShortcut(shortcut);\n}\n\n/*!\n    Destructs the QxtGlobalShortcut.\n */\nQxtGlobalShortcut::~QxtGlobalShortcut()\n{\n    Q_D(QxtGlobalShortcut);\n    if (d->key != 0)\n        d->unsetShortcut();\n    delete d;\n}\n\n/*!\n    \\property QxtGlobalShortcut::shortcut\n    \\brief the shortcut key sequence\n\n    \\bold {Note:} Notice that corresponding key press and release events are not\n    delivered for registered global shortcuts even if they are disabled.\n    Also, comma separated key sequences are not supported.\n    Only the first part is used:\n\n    \\code\n    qxtShortcut->setShortcut(QKeySequence(\"Ctrl+Alt+A,Ctrl+Alt+B\"));\n    Q_ASSERT(qxtShortcut->shortcut() == QKeySequence(\"Ctrl+Alt+A\"));\n    \\endcode\n */\nQKeySequence QxtGlobalShortcut::shortcut() const\n{\n    Q_D(const QxtGlobalShortcut);\n    return QKeySequence(d->key | d->mods);\n}\n\nbool QxtGlobalShortcut::setShortcut(const QKeySequence &shortcut)\n{\n    Q_D(QxtGlobalShortcut);\n    if (d->key != 0 && !d->unsetShortcut())\n        return false;\n    if (shortcut.isEmpty())\n        return true;\n    return d->setShortcut(shortcut);\n}\n\n/*!\n    \\property QxtGlobalShortcut::enabled\n    \\brief whether the shortcut is enabled\n\n    A disabled shortcut does not get activated.\n\n    The default value is \\c true.\n\n    \\sa setDisabled()\n */\nbool QxtGlobalShortcut::isEnabled() const\n{\n    Q_D(const QxtGlobalShortcut);\n    return d->enabled;\n}\n\n/*!\n * \\brief QxtGlobalShortcut::isSupported checks if the current platform is supported.\n */\nbool QxtGlobalShortcut::isSupported()\n{\n    return QxtGlobalShortcutPrivate::isSupported();\n}\n\nvoid QxtGlobalShortcut::setEnabled(bool enabled)\n{\n    Q_D(QxtGlobalShortcut);\n    d->enabled = enabled;\n}\n"
  },
  {
    "path": "src/libs/ui/qxtglobalshortcut/qxtglobalshortcut.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n/****************************************************************************\n// Copyright (C) 2006 - 2011, the LibQxt project.\n** See the Qxt AUTHORS file for a list of authors and copyright holders.\n** All rights reserved.\n**\n** Redistribution and use in source and binary forms, with or without\n** modification, are permitted provided that the following conditions are met:\n**     * Redistributions of source code must retain the above copyright\n**       notice, this list of conditions and the following disclaimer.\n**     * Redistributions in binary form must reproduce the above copyright\n**       notice, this list of conditions and the following disclaimer in the\n**       documentation and/or other materials provided with the distribution.\n**     * Neither the name of the LibQxt project nor the\n**       names of its contributors may be used to endorse or promote products\n**       derived from this software without specific prior written permission.\n**\n** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n** DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY\n** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\n** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n**\n** <http://libqxt.org>  <foundation@libqxt.org>\n*****************************************************************************/\n\n#ifndef QXTGLOBALSHORTCUT_H\n#define QXTGLOBALSHORTCUT_H\n\n#include <QKeySequence>\n#include <QObject>\n\nclass QxtGlobalShortcutPrivate;\n\nclass QxtGlobalShortcut : public QObject\n{\n    Q_OBJECT\n\n    QxtGlobalShortcutPrivate *d_ptr = nullptr;\n    Q_DECLARE_PRIVATE(QxtGlobalShortcut)\n\n    Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled)\n    Q_PROPERTY(QKeySequence shortcut READ shortcut WRITE setShortcut)\npublic:\n    explicit QxtGlobalShortcut(QObject *parent = nullptr);\n    explicit QxtGlobalShortcut(const QKeySequence &shortcut, QObject *parent = nullptr);\n    ~QxtGlobalShortcut() override;\n\n    QKeySequence shortcut() const;\n    bool setShortcut(const QKeySequence &shortcut);\n\n    bool isEnabled() const;\n\n    static bool isSupported();\n\npublic slots:\n    void setEnabled(bool enabled);\n\nsignals:\n    void activated();\n};\n\n#endif // QXTGLOBALSHORTCUT_H\n"
  },
  {
    "path": "src/libs/ui/qxtglobalshortcut/qxtglobalshortcut_mac.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n/****************************************************************************\n// Copyright (C) 2006 - 2011, the LibQxt project.\n** See the Qxt AUTHORS file for a list of authors and copyright holders.\n** All rights reserved.\n**\n** Redistribution and use in source and binary forms, with or without\n** modification, are permitted provided that the following conditions are met:\n**     * Redistributions of source code must retain the above copyright\n**       notice, this list of conditions and the following disclaimer.\n**     * Redistributions in binary form must reproduce the above copyright\n**       notice, this list of conditions and the following disclaimer in the\n**       documentation and/or other materials provided with the distribution.\n**     * Neither the name of the LibQxt project nor the\n**       names of its contributors may be used to endorse or promote products\n**       derived from this software without specific prior written permission.\n**\n** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n** DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY\n** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\n** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n**\n** <http://libqxt.org>  <foundation@libqxt.org>\n*****************************************************************************/\n\n#include \"qxtglobalshortcut_p.h\"\n\n#include <QGuiApplication>\n#include <QHash>\n#include <QMap>\n\n#include <Carbon/Carbon.h>\n\nnamespace {\ntypedef QPair<uint, uint> Identifier;\nstatic QMap<quint32, EventHotKeyRef> keyRefs;\nstatic QHash<Identifier, quint32> keyIDs;\nstatic quint32 hotKeySerial = 0;\nstatic bool qxt_mac_handler_installed = false;\n\nOSStatus qxt_mac_handle_hot_key(EventHandlerCallRef nextHandler, EventRef event, void *data)\n{\n    Q_UNUSED(nextHandler)\n    Q_UNUSED(data)\n    if (GetEventClass(event) == kEventClassKeyboard && GetEventKind(event) == kEventHotKeyPressed) {\n        EventHotKeyID keyID;\n        GetEventParameter(event, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(keyID), NULL, &keyID);\n        Identifier id = keyIDs.key(keyID.id);\n        QxtGlobalShortcutPrivate::activateShortcut(id.second, id.first);\n    }\n\n    return noErr;\n}\n} // namespace\n\nbool QxtGlobalShortcutPrivate::isSupported()\n{\n    return QGuiApplication::platformName() == QLatin1String(\"cocoa\");\n}\n\nbool QxtGlobalShortcutPrivate::nativeEventFilter(const QByteArray &eventType,\n                                                 void *message, NativeEventFilterResult *result)\n{\n    Q_UNUSED(eventType)\n    Q_UNUSED(message)\n    Q_UNUSED(result)\n    return false;\n}\n\nquint32 QxtGlobalShortcutPrivate::nativeModifiers(Qt::KeyboardModifiers modifiers)\n{\n    quint32 native = 0;\n    if (modifiers & Qt::ShiftModifier)\n        native |= shiftKey;\n    if (modifiers & Qt::ControlModifier)\n        native |= cmdKey;\n    if (modifiers & Qt::AltModifier)\n        native |= optionKey;\n    if (modifiers & Qt::MetaModifier)\n        native |= controlKey;\n    if (modifiers & Qt::KeypadModifier)\n        native |= kEventKeyModifierNumLockMask;\n    return native;\n}\n\nquint32 QxtGlobalShortcutPrivate::nativeKeycode(Qt::Key key)\n{\n    UTF16Char ch;\n    // Constants found in NSEvent.h from AppKit.framework\n    switch (key) {\n    case Qt::Key_Return:\n        return kVK_Return;\n    case Qt::Key_Enter:\n        return kVK_ANSI_KeypadEnter;\n    case Qt::Key_Tab:\n        return kVK_Tab;\n    case Qt::Key_Space:\n        return kVK_Space;\n    case Qt::Key_Backspace:\n        return kVK_Delete;\n    case Qt::Key_Control:\n        return kVK_Command;\n    case Qt::Key_Shift:\n        return kVK_Shift;\n    case Qt::Key_CapsLock:\n        return kVK_CapsLock;\n    case Qt::Key_Option:\n        return kVK_Option;\n    case Qt::Key_Meta:\n        return kVK_Control;\n    case Qt::Key_F17:\n        return kVK_F17;\n    case Qt::Key_VolumeUp:\n        return kVK_VolumeUp;\n    case Qt::Key_VolumeDown:\n        return kVK_VolumeDown;\n    case Qt::Key_F18:\n        return kVK_F18;\n    case Qt::Key_F19:\n        return kVK_F19;\n    case Qt::Key_F20:\n        return kVK_F20;\n    case Qt::Key_F5:\n        return kVK_F5;\n    case Qt::Key_F6:\n        return kVK_F6;\n    case Qt::Key_F7:\n        return kVK_F7;\n    case Qt::Key_F3:\n        return kVK_F3;\n    case Qt::Key_F8:\n        return kVK_F8;\n    case Qt::Key_F9:\n        return kVK_F9;\n    case Qt::Key_F11:\n        return kVK_F11;\n    case Qt::Key_F13:\n        return kVK_F13;\n    case Qt::Key_F16:\n        return kVK_F16;\n    case Qt::Key_F14:\n        return kVK_F14;\n    case Qt::Key_F10:\n        return kVK_F10;\n    case Qt::Key_F12:\n        return kVK_F12;\n    case Qt::Key_F15:\n        return kVK_F15;\n    case Qt::Key_Help:\n        return kVK_Help;\n    case Qt::Key_Home:\n        return kVK_Home;\n    case Qt::Key_PageUp:\n        return kVK_PageUp;\n    case Qt::Key_Delete:\n        return kVK_ForwardDelete;\n    case Qt::Key_F4:\n        return kVK_F4;\n    case Qt::Key_End:\n        return kVK_End;\n    case Qt::Key_F2:\n        return kVK_F2;\n    case Qt::Key_PageDown:\n        return kVK_PageDown;\n    case Qt::Key_F1:\n        return kVK_F1;\n    case Qt::Key_Left:\n        return kVK_LeftArrow;\n    case Qt::Key_Right:\n        return kVK_RightArrow;\n    case Qt::Key_Down:\n        return kVK_DownArrow;\n    case Qt::Key_Up:\n        return kVK_UpArrow;\n    default:\n        ;\n    }\n\n    if (key == Qt::Key_Escape)\n        ch = 27;\n    else if (key == Qt::Key_Return)\n        ch = 13;\n    else if (key == Qt::Key_Enter)\n        ch = 3;\n    else if (key == Qt::Key_Tab)\n        ch = 9;\n    else\n        ch = key;\n\n    CFDataRef currentLayoutData;\n    TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();\n\n    if (currentKeyboard == NULL)\n        return 0;\n\n    currentLayoutData = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData);\n    CFRelease(currentKeyboard);\n    if (currentLayoutData == NULL)\n        return 0;\n\n    UCKeyboardLayout *header = (UCKeyboardLayout *)CFDataGetBytePtr(currentLayoutData);\n    UCKeyboardTypeHeader *table = header->keyboardTypeList;\n\n    uint8_t *data = (uint8_t *)header;\n    // God, would a little documentation for this shit kill you...\n    for (quint32 i = 0; i < header->keyboardTypeCount; ++i) {\n        UCKeyStateRecordsIndex *stateRec = 0;\n        if (table[i].keyStateRecordsIndexOffset != 0) {\n            stateRec = reinterpret_cast<UCKeyStateRecordsIndex *>(data + table[i].keyStateRecordsIndexOffset);\n            if (stateRec->keyStateRecordsIndexFormat != kUCKeyStateRecordsIndexFormat) stateRec = 0;\n        }\n\n        UCKeyToCharTableIndex *charTable = reinterpret_cast<UCKeyToCharTableIndex *>(data + table[i].keyToCharTableIndexOffset);\n        if (charTable->keyToCharTableIndexFormat != kUCKeyToCharTableIndexFormat)\n            continue;\n\n        for (quint32 j = 0; j < charTable->keyToCharTableCount; ++j) {\n            UCKeyOutput *keyToChar = reinterpret_cast<UCKeyOutput *>(data + charTable->keyToCharTableOffsets[j]);\n            for (quint32 k = 0; k < charTable->keyToCharTableSize; ++k) {\n                if (keyToChar[k] & kUCKeyOutputTestForIndexMask) {\n                    long idx = keyToChar[k] & kUCKeyOutputGetIndexMask;\n                    if (stateRec && idx < stateRec->keyStateRecordCount) {\n                        UCKeyStateRecord *rec = reinterpret_cast<UCKeyStateRecord *>(data + stateRec->keyStateRecordOffsets[idx]);\n                        if (rec->stateZeroCharData == ch) return k;\n                    }\n                } else if (!(keyToChar[k] & kUCKeyOutputSequenceIndexMask) && keyToChar[k] < 0xFFFE) {\n                    if (keyToChar[k] == ch)\n                        return k;\n                }\n            } // for k\n        } // for j\n    } // for i\n    return 0;\n}\n\nbool QxtGlobalShortcutPrivate::registerShortcut(quint32 nativeKey, quint32 nativeMods)\n{\n    if (!qxt_mac_handler_installed) {\n        EventTypeSpec t;\n        t.eventClass = kEventClassKeyboard;\n        t.eventKind = kEventHotKeyPressed;\n        InstallApplicationEventHandler(&qxt_mac_handle_hot_key, 1, &t, NULL, NULL);\n    }\n\n    EventHotKeyID keyID;\n    keyID.signature = 'cute';\n    keyID.id = ++hotKeySerial;\n\n    EventHotKeyRef ref = 0;\n    bool rv = !RegisterEventHotKey(nativeKey, nativeMods, keyID, GetApplicationEventTarget(), 0, &ref);\n    if (rv) {\n        keyIDs.insert(Identifier(nativeMods, nativeKey), keyID.id);\n        keyRefs.insert(keyID.id, ref);\n    }\n    return rv;\n}\n\nbool QxtGlobalShortcutPrivate::unregisterShortcut(quint32 nativeKey, quint32 nativeMods)\n{\n    Identifier id(nativeMods, nativeKey);\n    if (!keyIDs.contains(id))\n        return false;\n\n    EventHotKeyRef ref = keyRefs.take(keyIDs[id]);\n    keyIDs.remove(id);\n    return !UnregisterEventHotKey(ref);\n}\n"
  },
  {
    "path": "src/libs/ui/qxtglobalshortcut/qxtglobalshortcut_noop.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"qxtglobalshortcut_p.h\"\n\nbool QxtGlobalShortcutPrivate::isSupported()\n{\n    return false;\n}\n\nbool QxtGlobalShortcutPrivate::nativeEventFilter(const QByteArray &eventType,\n                                                 void *message,\n                                                 NativeEventFilterResult *result)\n{\n    Q_UNUSED(eventType)\n    Q_UNUSED(message)\n    Q_UNUSED(result)\n\n    return false;\n}\n\nquint32 QxtGlobalShortcutPrivate::nativeModifiers(Qt::KeyboardModifiers modifiers)\n{\n    Q_UNUSED(modifiers)\n\n    return 0;\n}\n\nquint32 QxtGlobalShortcutPrivate::nativeKeycode(Qt::Key key)\n{\n    Q_UNUSED(key)\n\n    return 0;\n}\n\nbool QxtGlobalShortcutPrivate::registerShortcut(quint32 nativeKey, quint32 nativeMods)\n{\n    Q_UNUSED(nativeKey)\n    Q_UNUSED(nativeMods)\n\n    return false;\n}\n\nbool QxtGlobalShortcutPrivate::unregisterShortcut(quint32 nativeKey, quint32 nativeMods)\n{\n    Q_UNUSED(nativeKey)\n    Q_UNUSED(nativeMods)\n\n    return false;\n}\n"
  },
  {
    "path": "src/libs/ui/qxtglobalshortcut/qxtglobalshortcut_p.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n/****************************************************************************\n// Copyright (C) 2006 - 2011, the LibQxt project.\n** See the Qxt AUTHORS file for a list of authors and copyright holders.\n** All rights reserved.\n**\n** Redistribution and use in source and binary forms, with or without\n** modification, are permitted provided that the following conditions are met:\n**     * Redistributions of source code must retain the above copyright\n**       notice, this list of conditions and the following disclaimer.\n**     * Redistributions in binary form must reproduce the above copyright\n**       notice, this list of conditions and the following disclaimer in the\n**       documentation and/or other materials provided with the distribution.\n**     * Neither the name of the LibQxt project nor the\n**       names of its contributors may be used to endorse or promote products\n**       derived from this software without specific prior written permission.\n**\n** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n** DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY\n** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\n** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n**\n** <http://libqxt.org>  <foundation@libqxt.org>\n*****************************************************************************/\n\n#ifndef QXTGLOBALSHORTCUT_P_H\n#define QXTGLOBALSHORTCUT_P_H\n\n#include <QAbstractNativeEventFilter>\n#include <QHash>\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n#define NativeEventFilterResult long\n#else\n#define NativeEventFilterResult qintptr\n#endif\n\nclass QKeySequence;\n\nclass QxtGlobalShortcut;\n\nclass QxtGlobalShortcutPrivate : public QAbstractNativeEventFilter\n{\n    QxtGlobalShortcut *q_ptr = nullptr;\n    Q_DECLARE_PUBLIC(QxtGlobalShortcut)\npublic:\n    QxtGlobalShortcutPrivate(QxtGlobalShortcut *qq);\n    ~QxtGlobalShortcutPrivate() override;\n\n    bool enabled = true;\n    Qt::Key key = Qt::Key(0);\n    Qt::KeyboardModifiers mods = Qt::NoModifier;\n\n#ifndef Q_OS_MACOS\n    static int ref;\n#endif // Q_OS_MACOS\n\n    bool setShortcut(const QKeySequence &shortcut);\n    bool unsetShortcut();\n\n    bool nativeEventFilter(const QByteArray &eventType, void *message, NativeEventFilterResult *result) override;\n\n    static bool isSupported();\n    static bool activateShortcut(quint32 nativeKey, quint32 nativeMods);\n\nprivate:\n    static quint32 nativeKeycode(Qt::Key keycode);\n    static quint32 nativeModifiers(Qt::KeyboardModifiers modifiers);\n\n    static bool registerShortcut(quint32 nativeKey, quint32 nativeMods);\n    static bool unregisterShortcut(quint32 nativeKey, quint32 nativeMods);\n\n    static QHash<QPair<quint32, quint32>, QxtGlobalShortcut *> shortcuts;\n};\n\n#endif // QXTGLOBALSHORTCUT_P_H\n"
  },
  {
    "path": "src/libs/ui/qxtglobalshortcut/qxtglobalshortcut_win.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n/****************************************************************************\n// Copyright (C) 2006 - 2011, the LibQxt project.\n** See the Qxt AUTHORS file for a list of authors and copyright holders.\n** All rights reserved.\n**\n** Redistribution and use in source and binary forms, with or without\n** modification, are permitted provided that the following conditions are met:\n**     * Redistributions of source code must retain the above copyright\n**       notice, this list of conditions and the following disclaimer.\n**     * Redistributions in binary form must reproduce the above copyright\n**       notice, this list of conditions and the following disclaimer in the\n**       documentation and/or other materials provided with the distribution.\n**     * Neither the name of the LibQxt project nor the\n**       names of its contributors may be used to endorse or promote products\n**       derived from this software without specific prior written permission.\n**\n** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n** DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY\n** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\n** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n**\n** <http://libqxt.org>  <foundation@libqxt.org>\n*****************************************************************************/\n\n#include \"qxtglobalshortcut_p.h\"\n\n#include <QGuiApplication>\n\n#include <qt_windows.h>\n\nbool QxtGlobalShortcutPrivate::isSupported()\n{\n    return QGuiApplication::platformName() == QLatin1String(\"windows\");\n}\n\nbool QxtGlobalShortcutPrivate::nativeEventFilter(const QByteArray &eventType,\n                                                 void *message, NativeEventFilterResult *result)\n{\n    Q_UNUSED(eventType)\n    Q_UNUSED(result)\n\n    MSG *msg = static_cast<MSG *>(message);\n    if (msg->message == WM_HOTKEY) {\n        const quint32 keycode = HIWORD(msg->lParam);\n        const quint32 modifiers = LOWORD(msg->lParam);\n        activateShortcut(keycode, modifiers);\n    }\n\n    return false;\n}\n\nquint32 QxtGlobalShortcutPrivate::nativeModifiers(Qt::KeyboardModifiers modifiers)\n{\n    // MOD_ALT, MOD_CONTROL, (MOD_KEYUP), MOD_SHIFT, MOD_WIN\n    quint32 native = 0;\n    if (modifiers & Qt::ShiftModifier)\n        native |= MOD_SHIFT;\n    if (modifiers & Qt::ControlModifier)\n        native |= MOD_CONTROL;\n    if (modifiers & Qt::AltModifier)\n        native |= MOD_ALT;\n    if (modifiers & Qt::MetaModifier)\n        native |= MOD_WIN;\n    // TODO: resolve these?\n    //if (modifiers & Qt::KeypadModifier)\n    //if (modifiers & Qt::GroupSwitchModifier)\n    return native;\n}\n\nquint32 QxtGlobalShortcutPrivate::nativeKeycode(Qt::Key key)\n{\n    switch (key) {\n    case Qt::Key_Escape:\n        return VK_ESCAPE;\n    case Qt::Key_Tab:\n    case Qt::Key_Backtab:\n        return VK_TAB;\n    case Qt::Key_Backspace:\n        return VK_BACK;\n    case Qt::Key_Return:\n    case Qt::Key_Enter:\n        return VK_RETURN;\n    case Qt::Key_Insert:\n        return VK_INSERT;\n    case Qt::Key_Delete:\n        return VK_DELETE;\n    case Qt::Key_Pause:\n        return VK_PAUSE;\n    case Qt::Key_Print:\n        return VK_PRINT;\n    case Qt::Key_Clear:\n        return VK_CLEAR;\n    case Qt::Key_Home:\n        return VK_HOME;\n    case Qt::Key_End:\n        return VK_END;\n    case Qt::Key_Left:\n        return VK_LEFT;\n    case Qt::Key_Up:\n        return VK_UP;\n    case Qt::Key_Right:\n        return VK_RIGHT;\n    case Qt::Key_Down:\n        return VK_DOWN;\n    case Qt::Key_PageUp:\n        return VK_PRIOR;\n    case Qt::Key_PageDown:\n        return VK_NEXT;\n    case Qt::Key_F1:\n        return VK_F1;\n    case Qt::Key_F2:\n        return VK_F2;\n    case Qt::Key_F3:\n        return VK_F3;\n    case Qt::Key_F4:\n        return VK_F4;\n    case Qt::Key_F5:\n        return VK_F5;\n    case Qt::Key_F6:\n        return VK_F6;\n    case Qt::Key_F7:\n        return VK_F7;\n    case Qt::Key_F8:\n        return VK_F8;\n    case Qt::Key_F9:\n        return VK_F9;\n    case Qt::Key_F10:\n        return VK_F10;\n    case Qt::Key_F11:\n        return VK_F11;\n    case Qt::Key_F12:\n        return VK_F12;\n    case Qt::Key_F13:\n        return VK_F13;\n    case Qt::Key_F14:\n        return VK_F14;\n    case Qt::Key_F15:\n        return VK_F15;\n    case Qt::Key_F16:\n        return VK_F16;\n    case Qt::Key_F17:\n        return VK_F17;\n    case Qt::Key_F18:\n        return VK_F18;\n    case Qt::Key_F19:\n        return VK_F19;\n    case Qt::Key_F20:\n        return VK_F20;\n    case Qt::Key_F21:\n        return VK_F21;\n    case Qt::Key_F22:\n        return VK_F22;\n    case Qt::Key_F23:\n        return VK_F23;\n    case Qt::Key_F24:\n        return VK_F24;\n    case Qt::Key_Space:\n        return VK_SPACE;\n    case Qt::Key_Asterisk:\n        return VK_MULTIPLY;\n    case Qt::Key_Plus:\n        return VK_ADD;\n    case Qt::Key_Comma:\n        return VK_SEPARATOR;\n    case Qt::Key_Minus:\n        return VK_SUBTRACT;\n    case Qt::Key_Slash:\n        return VK_DIVIDE;\n    case Qt::Key_MediaNext:\n        return VK_MEDIA_NEXT_TRACK;\n    case Qt::Key_MediaPrevious:\n        return VK_MEDIA_PREV_TRACK;\n    case Qt::Key_MediaPlay:\n        return VK_MEDIA_PLAY_PAUSE;\n    case Qt::Key_MediaStop:\n        return VK_MEDIA_STOP;\n        // couldn't find those in VK_*\n        //case Qt::Key_MediaLast:\n        //case Qt::Key_MediaRecord:\n    case Qt::Key_VolumeDown:\n        return VK_VOLUME_DOWN;\n    case Qt::Key_VolumeUp:\n        return VK_VOLUME_UP;\n    case Qt::Key_VolumeMute:\n        return VK_VOLUME_MUTE;\n\n        // numbers\n    case Qt::Key_0:\n    case Qt::Key_1:\n    case Qt::Key_2:\n    case Qt::Key_3:\n    case Qt::Key_4:\n    case Qt::Key_5:\n    case Qt::Key_6:\n    case Qt::Key_7:\n    case Qt::Key_8:\n    case Qt::Key_9:\n        return key;\n\n        // letters\n    case Qt::Key_A:\n    case Qt::Key_B:\n    case Qt::Key_C:\n    case Qt::Key_D:\n    case Qt::Key_E:\n    case Qt::Key_F:\n    case Qt::Key_G:\n    case Qt::Key_H:\n    case Qt::Key_I:\n    case Qt::Key_J:\n    case Qt::Key_K:\n    case Qt::Key_L:\n    case Qt::Key_M:\n    case Qt::Key_N:\n    case Qt::Key_O:\n    case Qt::Key_P:\n    case Qt::Key_Q:\n    case Qt::Key_R:\n    case Qt::Key_S:\n    case Qt::Key_T:\n    case Qt::Key_U:\n    case Qt::Key_V:\n    case Qt::Key_W:\n    case Qt::Key_X:\n    case Qt::Key_Y:\n    case Qt::Key_Z:\n        return key;\n\n    default:\n        return 0;\n    }\n}\n\nbool QxtGlobalShortcutPrivate::registerShortcut(quint32 nativeKey, quint32 nativeMods)\n{\n    return RegisterHotKey(0, nativeMods ^ nativeKey, nativeMods, nativeKey);\n}\n\nbool QxtGlobalShortcutPrivate::unregisterShortcut(quint32 nativeKey, quint32 nativeMods)\n{\n    return UnregisterHotKey(0, nativeMods ^ nativeKey);\n}\n"
  },
  {
    "path": "src/libs/ui/qxtglobalshortcut/qxtglobalshortcut_x11.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n/****************************************************************************\n// Copyright (C) 2006 - 2011, the LibQxt project.\n** See the Qxt AUTHORS file for a list of authors and copyright holders.\n** All rights reserved.\n**\n** Redistribution and use in source and binary forms, with or without\n** modification, are permitted provided that the following conditions are met:\n**     * Redistributions of source code must retain the above copyright\n**       notice, this list of conditions and the following disclaimer.\n**     * Redistributions in binary form must reproduce the above copyright\n**       notice, this list of conditions and the following disclaimer in the\n**       documentation and/or other materials provided with the distribution.\n**     * Neither the name of the LibQxt project nor the\n**       names of its contributors may be used to endorse or promote products\n**       derived from this software without specific prior written permission.\n**\n** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n** DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY\n** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\n** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n**\n** <http://libqxt.org>  <foundation@libqxt.org>\n*****************************************************************************/\n\n#include \"qxtglobalshortcut_p.h\"\n\n#include <QGuiApplication>\n#include <QKeySequence>\n#include <QScopedPointer>\n#include <QVector>\n\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n#include <QX11Info>\n#else\n#include <QtGui/private/qtx11extras_p.h>\n#endif\n\n#include <X11/Xlib.h>\n#include <xcb/xcb.h>\n#include <xcb/xcb_keysyms.h>\n\nnamespace {\nconstexpr quint32 maskModifiers[] = {\n    0, XCB_MOD_MASK_2, XCB_MOD_MASK_LOCK, (XCB_MOD_MASK_2 | XCB_MOD_MASK_LOCK)\n};\n} // namespace\n\nbool QxtGlobalShortcutPrivate::isSupported()\n{\n    return QGuiApplication::platformName() == QLatin1String(\"xcb\");\n}\n\nbool QxtGlobalShortcutPrivate::nativeEventFilter(const QByteArray &eventType,\n                                                 void *message, NativeEventFilterResult *result)\n{\n    Q_UNUSED(result)\n    if (eventType != \"xcb_generic_event_t\")\n        return false;\n\n    auto event = static_cast<xcb_generic_event_t *>(message);\n    if ((event->response_type & ~0x80) != XCB_KEY_PRESS)\n        return false;\n\n    auto keyPressEvent = static_cast<xcb_key_press_event_t *>(message);\n\n    // Avoid keyboard freeze\n    xcb_connection_t *xcbConnection = QX11Info::connection();\n    xcb_allow_events(xcbConnection, XCB_ALLOW_REPLAY_KEYBOARD, keyPressEvent->time);\n    xcb_flush(xcbConnection);\n\n    unsigned int keycode = keyPressEvent->detail;\n    unsigned int keystate = 0;\n    if (keyPressEvent->state & XCB_MOD_MASK_1)\n        keystate |= XCB_MOD_MASK_1;\n    if (keyPressEvent->state & XCB_MOD_MASK_CONTROL)\n        keystate |= XCB_MOD_MASK_CONTROL;\n    if (keyPressEvent->state & XCB_MOD_MASK_4)\n        keystate |= XCB_MOD_MASK_4;\n    if (keyPressEvent->state & XCB_MOD_MASK_SHIFT)\n        keystate |= XCB_MOD_MASK_SHIFT;\n\n    return activateShortcut(keycode, keystate);\n}\n\nquint32 QxtGlobalShortcutPrivate::nativeModifiers(Qt::KeyboardModifiers modifiers)\n{\n    quint32 native = 0;\n    if (modifiers & Qt::ShiftModifier)\n        native |= XCB_MOD_MASK_SHIFT;\n    if (modifiers & Qt::ControlModifier)\n        native |= XCB_MOD_MASK_CONTROL;\n    if (modifiers & Qt::AltModifier)\n        native |= XCB_MOD_MASK_1;\n    if (modifiers & Qt::MetaModifier)\n        native |= XCB_MOD_MASK_4;\n\n    return native;\n}\n\nquint32 QxtGlobalShortcutPrivate::nativeKeycode(Qt::Key key)\n{\n    quint32 native = 0;\n\n    KeySym keysym = XStringToKeysym(QKeySequence(key).toString().toLatin1().data());\n    if (keysym == XCB_NO_SYMBOL)\n        keysym = static_cast<ushort>(key);\n\n    xcb_key_symbols_t *xcbKeySymbols = xcb_key_symbols_alloc(QX11Info::connection());\n\n    QScopedPointer<xcb_keycode_t, QScopedPointerPodDeleter> keycodes(\n                xcb_key_symbols_get_keycode(xcbKeySymbols, keysym));\n\n    if (!keycodes.isNull())\n        native = keycodes.data()[0]; // Use the first keycode\n\n    xcb_key_symbols_free(xcbKeySymbols);\n\n    return native;\n}\n\nbool QxtGlobalShortcutPrivate::registerShortcut(quint32 nativeKey, quint32 nativeMods)\n{\n    xcb_connection_t *xcbConnection = QX11Info::connection();\n\n    QList<xcb_void_cookie_t> xcbCookies;\n    for (quint32 maskMods : maskModifiers) {\n        xcbCookies << xcb_grab_key_checked(xcbConnection, 1, QX11Info::appRootWindow(),\n                                           nativeMods | maskMods, nativeKey,\n                                           XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC);\n    }\n\n    bool failed = false;\n    for (xcb_void_cookie_t cookie : std::as_const(xcbCookies)) {\n        QScopedPointer<xcb_generic_error_t, QScopedPointerPodDeleter> error(xcb_request_check(xcbConnection, cookie));\n        failed = !error.isNull();\n    }\n\n    if (failed)\n        unregisterShortcut(nativeKey, nativeMods);\n\n    return !failed;\n}\n\nbool QxtGlobalShortcutPrivate::unregisterShortcut(quint32 nativeKey, quint32 nativeMods)\n{\n    xcb_connection_t *xcbConnection = QX11Info::connection();\n\n    QList<xcb_void_cookie_t> xcbCookies;\n    for (quint32 maskMods : maskModifiers) {\n        xcb_ungrab_key(xcbConnection, nativeKey, QX11Info::appRootWindow(), nativeMods | maskMods);\n    }\n\n    bool failed = false;\n    for (xcb_void_cookie_t cookie : xcbCookies) {\n        QScopedPointer<xcb_generic_error_t, QScopedPointerPodDeleter> error(xcb_request_check(xcbConnection, cookie));\n        failed = !error.isNull();\n    }\n\n    return !failed;\n}\n"
  },
  {
    "path": "src/libs/ui/searchitemdelegate.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"searchitemdelegate.h\"\n\n#include <QAbstractItemView>\n#include <QFontMetrics>\n#include <QHelpEvent>\n#include <QPainter>\n#include <QPainterPath>\n#include <QToolTip>\n\n#include <algorithm>\n\nusing namespace Zeal::WidgetUi;\n\nSearchItemDelegate::SearchItemDelegate(QObject *parent)\n    : QStyledItemDelegate(parent)\n{\n}\n\nQList<int> SearchItemDelegate::decorationRoles() const\n{\n    return m_decorationRoles;\n}\n\nvoid SearchItemDelegate::setDecorationRoles(const QList<int> &roles)\n{\n    m_decorationRoles = roles;\n}\n\nint SearchItemDelegate::textHighlightRole() const\n{\n    return m_textHighlightRole;\n}\n\nvoid SearchItemDelegate::setTextHighlightRole(int role)\n{\n    m_textHighlightRole = role;\n}\n\nbool SearchItemDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view,\n                                   const QStyleOptionViewItem &option, const QModelIndex &index)\n{\n    if (event->type() != QEvent::ToolTip)\n        return QStyledItemDelegate::helpEvent(event, view, option, index);\n\n    if (sizeHint(option, index).width() < view->visualRect(index).width()) {\n        QToolTip::hideText();\n        return QStyledItemDelegate::helpEvent(event, view, option, index);\n    }\n\n    QToolTip::showText(event->globalPos(), index.data().toString(), view);\n    return true;\n}\n\nvoid SearchItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,\n                               const QModelIndex &index) const\n{\n    QStyleOptionViewItem opt(option);\n\n    QStyle *style = opt.widget->style();\n\n    // Find decoration roles with data present.\n    QList<int> roles;\n    for (int role : m_decorationRoles) {\n        if (!index.data(role).isNull())\n            roles.append(role);\n    }\n\n    // TODO: Implemented via initStyleOption() overload\n    if (!roles.isEmpty()) {\n        opt.features |= QStyleOptionViewItem::HasDecoration;\n        opt.icon = index.data(roles.first()).value<QIcon>();\n\n        // Constrain decoration size to the style's small icon size to ensure consistent icon sizing.\n        const int maxSize = style->pixelMetric(QStyle::PM_SmallIconSize, &opt, opt.widget);\n        opt.decorationSize = {std::min(opt.decorationSize.width(), maxSize),\n                              std::min(opt.decorationSize.height(), maxSize)};\n    }\n\n    style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);\n\n    const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &opt, opt.widget) + 1;\n\n    if (!roles.isEmpty()) {\n        QIcon::Mode mode = QIcon::Normal;\n        if (!(opt.state & QStyle::State_Enabled))\n            mode = QIcon::Disabled;\n        else if (opt.state & QStyle::State_Selected)\n            mode = QIcon::Selected;\n        const QIcon::State state = (opt.state & QStyle::State_Open) ? QIcon::On : QIcon::Off;\n\n        // All icons are sized after the first one.\n        QRect iconRect = style->subElementRect(QStyle::SE_ItemViewItemDecoration, &opt, opt.widget);\n        // Undo RTL mirroring\n        iconRect = style->visualRect(opt.direction, opt.rect, iconRect);\n        const int dx = iconRect.width() + margin;\n\n        for (int i = 1; i < roles.size(); ++i) {\n            opt.decorationSize.rwidth() += dx;\n            iconRect.translate(dx, 0);\n            // Redo RTL mirroring\n            const auto iconVisualRect = style->visualRect(opt.direction, opt.rect, iconRect);\n\n            const QIcon icon = index.data(roles[i]).value<QIcon>();\n            icon.paint(painter, iconVisualRect, opt.decorationAlignment, mode, state);\n        }\n    }\n\n    // This should not happen unless a docset is corrupted.\n    if (index.data().isNull())\n        return;\n\n    // Match QCommonStyle behavior.\n    opt.features |= QStyleOptionViewItem::HasDisplay;\n    opt.text = index.data().toString();\n    const QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &opt, opt.widget)\n            .adjusted(margin, 0, -margin, 0);\n    const QFontMetrics &fm = opt.fontMetrics;\n    const QString elidedText = fm.elidedText(opt.text, opt.textElideMode, textRect.width());\n\n    // Get pre-computed match positions from model for highlighting.\n    const QVector<int> matchPositions = index.data(m_textHighlightRole).value<QVector<int>>();\n    if (!matchPositions.isEmpty()) {\n        painter->save();\n        painter->setRenderHint(QPainter::Antialiasing);\n        painter->setPen(QColor::fromRgb(255, 253, 0));\n\n        const QColor highlightColor\n                = (opt.state & (QStyle::State_Selected | QStyle::State_HasFocus))\n                ? QColor::fromRgb(255, 255, 100, 20) : QColor::fromRgb(255, 255, 100, 120);\n\n        {\n            // Group consecutive positions to reduce number of highlight rectangles\n            const int matchCount = matchPositions.size();\n\n            int startPos = matchPositions[0];\n            int endPos = matchPositions[0];\n\n            for (int i = 1; i <= matchCount; ++i) {\n                const bool isLast = (i == matchCount);\n                const bool isConsecutive = !isLast && (matchPositions[i] == endPos + 1);\n\n                if (isConsecutive) {\n                    endPos = matchPositions[i];\n                    continue;\n                }\n\n                // Draw highlight for [startPos, endPos]\n                // Use FULL text (opt.text) for position calculation, not elided text\n                const int highlightStart = startPos;\n                const int highlightLen = endPos - startPos + 1;\n\n                QRect highlightRect = textRect.adjusted(fm.horizontalAdvance(opt.text.left(highlightStart)), 2, 0, -2);\n                highlightRect.setWidth(fm.horizontalAdvance(opt.text.mid(highlightStart, highlightLen)));\n\n                // Clip highlight to visible text area (handles elided text correctly)\n                highlightRect = highlightRect.intersected(textRect.adjusted(0, 2, 0, -2));\n\n                // Only draw if rectangle is valid after clipping\n                if (highlightRect.isValid() && !highlightRect.isEmpty()) {\n                    QPainterPath path;\n                    path.addRoundedRect(highlightRect, 2, 2);\n                    painter->fillPath(path, highlightColor);\n                    painter->drawPath(path);\n                }\n\n                if (!isLast) {\n                    startPos = matchPositions[i];\n                    endPos = matchPositions[i];\n                }\n            }\n        }\n\n        painter->restore();\n    }\n\n    painter->save();\n\n#ifdef Q_OS_WINDOWS\n    // QWindowsVistaStyle overrides highlight color.\n    if (style->objectName() == QLatin1String(\"windowsvista\")) {\n        opt.palette.setColor(QPalette::All, QPalette::HighlightedText,\n                             opt.palette.color(QPalette::Active, QPalette::Text));\n    }\n#endif\n\n    const QPalette::ColorGroup cg = (opt.state & QStyle::State_Active)\n            ? QPalette::Normal : QPalette::Inactive;\n\n    if (opt.state & QStyle::State_Selected)\n        painter->setPen(opt.palette.color(cg, QPalette::HighlightedText));\n    else\n        painter->setPen(opt.palette.color(cg, QPalette::Text));\n\n    // Constant LeftToRight because we don't need to flip it any further.\n    // Vertically align the text in the middle to match QCommonStyle behaviour.\n    const auto alignedRect = QStyle::alignedRect(Qt::LeftToRight, opt.displayAlignment,\n                                                 QSize(textRect.size().width(), fm.height()), textRect);\n    const auto textPoint = QPoint(alignedRect.x(), alignedRect.y() + fm.ascent());\n    // Force LTR, so that BiDi won't reorder ellipsis to the left.\n    painter->drawText(textPoint, elidedText, Qt::TextFlag::TextForceLeftToRight, 0);\n    painter->restore();\n}\n\nQSize SearchItemDelegate::sizeHint(const QStyleOptionViewItem &option,\n                                   const QModelIndex &index) const\n{\n    QStyleOptionViewItem opt(option);\n\n    QStyle *style = opt.widget->style();\n\n    // Constrain decoration size to the style's small icon size to ensure consistent icon sizing.\n    const int maxSize = style->pixelMetric(QStyle::PM_SmallIconSize, &opt, opt.widget);\n    opt.decorationSize = {std::min(opt.decorationSize.width(), maxSize),\n                          std::min(opt.decorationSize.height(), maxSize)};\n\n    QSize size = QStyledItemDelegate::sizeHint(opt, index);\n    size.setWidth(0);\n\n    const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &opt, opt.widget) + 1;\n\n    // Find decoration roles with data present.\n    QList<int> roles;\n    for (int role : m_decorationRoles) {\n        if (!index.data(role).isNull())\n            roles.append(role);\n    }\n\n    if (!roles.isEmpty()) {\n        size.rwidth() = (opt.decorationSize.width() + margin) * roles.size() + margin;\n    }\n\n    size.rwidth() += opt.fontMetrics.horizontalAdvance(index.data().toString()) + margin * 2;\n\n    return size;\n}\n"
  },
  {
    "path": "src/libs/ui/searchitemdelegate.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_WIDGETUI_SEARCHITEMDELEGATE_H\n#define ZEAL_WIDGETUI_SEARCHITEMDELEGATE_H\n\n#include <QStyledItemDelegate>\n\nnamespace Zeal {\nnamespace WidgetUi {\n\nclass SearchItemDelegate : public QStyledItemDelegate\n{\n    Q_OBJECT\npublic:\n    explicit SearchItemDelegate(QObject *parent = nullptr);\n\n    QList<int> decorationRoles() const;\n    void setDecorationRoles(const QList<int> &roles);\n\n    int textHighlightRole() const;\n    void setTextHighlightRole(int role);\n\n    bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option,\n                   const QModelIndex &index) override;\n    void paint(QPainter *painter, const QStyleOptionViewItem &option,\n               const QModelIndex &index) const override;\n    QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;\n\nprivate:\n    QList<int> m_decorationRoles = {Qt::DecorationRole};\n    int m_textHighlightRole = -1;\n};\n\n} // namespace WidgetUi\n} // namespace Zeal\n\n#endif // ZEAL_WIDGETUI_SEARCHITEMDELEGATE_H\n"
  },
  {
    "path": "src/libs/ui/searchsidebar.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"searchsidebar.h\"\n\n#include \"searchitemdelegate.h\"\n#include \"widgets/layouthelper.h\"\n#include \"widgets/searchedit.h\"\n#include \"widgets/toolbarframe.h\"\n\n#include <core/application.h>\n#include <core/settings.h>\n#include <registry/docset.h>\n#include <registry/docsetregistry.h>\n#include <registry/itemdatarole.h>\n#include <registry/listmodel.h>\n#include <registry/searchmodel.h>\n#include <registry/searchquery.h>\n\n#include <QCoreApplication>\n#include <QEvent>\n#include <QKeyEvent>\n#include <QListView>\n#include <QScrollBar>\n#include <QShortcut>\n#include <QSplitter>\n#include <QTimer>\n#include <QTreeView>\n#include <QVBoxLayout>\n\nusing namespace Zeal;\nusing namespace Zeal::WidgetUi;\n\nSearchSidebar::SearchSidebar(QWidget *parent)\n    : SearchSidebar(nullptr, parent)\n{\n}\n\nSearchSidebar::SearchSidebar(const SearchSidebar *other, QWidget *parent)\n    : Sidebar::View(parent)\n{\n    // Setup search result view.\n    m_treeView = new QTreeView();\n    m_treeView->setFrameShape(QFrame::NoFrame);\n    m_treeView->setHeaderHidden(true);\n    m_treeView->setUniformRowHeights(true);\n\n    // A stylesheet (even an empty one) wraps the widget in QStyleSheetStyle,\n    // which properly re-polishes on dynamic color scheme changes.\n    m_treeView->setStyleSheet(QStringLiteral(\"QTreeView {}\"));\n\n#ifdef Q_OS_MACOS\n    m_treeView->setAttribute(Qt::WA_MacShowFocusRect, false);\n#endif\n\n    // Save expanded items.\n    connect(m_treeView, &QTreeView::expanded, this, [this](const QModelIndex &index) {\n        if (m_expandedIndexList.indexOf(index) == -1) {\n            m_expandedIndexList.append(index);\n        }\n    });\n    connect(m_treeView, &QTreeView::collapsed, this, [this](const QModelIndex &index) {\n        m_expandedIndexList.removeOne(index);\n    });\n\n    auto delegate = new SearchItemDelegate(m_treeView);\n    delegate->setDecorationRoles({Registry::ItemDataRole::DocsetIconRole, Qt::DecorationRole});\n    delegate->setTextHighlightRole(Registry::ItemDataRole::MatchPositionsRole);\n    m_treeView->setItemDelegate(delegate);\n\n    connect(m_treeView, &QTreeView::activated, this, &SearchSidebar::navigateToIndexAndActivate);\n    connect(m_treeView, &QTreeView::clicked, this, &SearchSidebar::navigateToIndex);\n\n    // Setup Alt+Up, Alt+Down, etc shortcuts.\n    const auto keyList = {Qt::Key_Up, Qt::Key_Down,\n                          Qt::Key_PageUp, Qt::Key_PageDown,\n                          Qt::Key_Home, Qt::Key_End};\n    for (const auto key : keyList) {\n        auto shortcut = new QShortcut(key | Qt::AltModifier, this);\n        connect(shortcut, &QShortcut::activated, this, [this, key]() {\n            QKeyEvent event(QKeyEvent::KeyPress, key, Qt::NoModifier);\n            QCoreApplication::sendEvent(m_treeView, &event);\n        });\n    }\n\n    // Setup page TOC view.\n    // TODO: Move to a separate Sidebar View.\n    m_pageTocView = new QListView();\n    m_pageTocView->setFrameShape(QFrame::NoFrame);\n    m_pageTocView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);\n    m_pageTocView->setItemDelegate(new SearchItemDelegate(m_pageTocView));\n    m_pageTocView->setStyleSheet(QStringLiteral(\"QListView {}\"));\n    m_pageTocView->setUniformItemSizes(true);\n    m_pageTocView->setVisible(false);\n#ifdef Q_OS_MACOS\n    m_pageTocView->setAttribute(Qt::WA_MacShowFocusRect, false);\n#endif\n\n    m_pageTocModel = new Registry::SearchModel(this);\n    connect(m_pageTocModel, &Registry::SearchModel::updated, this, [this]() {\n        if (m_pageTocModel->isEmpty()) {\n            m_pageTocView->hide();\n        } else {\n            m_pageTocView->show();\n            m_splitter->restoreState(Core::Application::instance()->settings()->tocSplitterState);\n        }\n    });\n    m_pageTocView->setModel(m_pageTocModel);\n\n    connect(m_pageTocView, &QListView::activated, this, &SearchSidebar::navigateToIndexAndActivate);\n    connect(m_pageTocView, &QListView::clicked, this, &SearchSidebar::navigateToIndex);\n\n    // Setup search input box.\n    m_searchEdit = new SearchEdit();\n    m_searchEdit->installEventFilter(this);\n    setupSearchBoxCompletions();\n\n    // Clone state if `other` is provided.\n    if (other) {\n        if (other->m_searchEdit->text().isEmpty()) {\n            m_searchModel = new Registry::SearchModel(this);\n            setTreeViewModel(Core::Application::instance()->docsetRegistry()->model(), true);\n\n            for (const QModelIndex &index : other->m_expandedIndexList) {\n                m_treeView->expand(index);\n            }\n        } else {\n            m_searchEdit->setText(other->m_searchEdit->text());\n\n            m_searchModel = other->m_searchModel->clone(this);\n            setTreeViewModel(m_searchModel, false);\n        }\n\n        // Clone current selection. Signals must be disabled to avoid crash in the event handler.\n        m_treeView->selectionModel()->blockSignals(true);\n\n        for (const QModelIndex &index : other->m_treeView->selectionModel()->selectedIndexes()) {\n            m_treeView->selectionModel()->select(index, QItemSelectionModel::Select);\n        }\n\n        m_treeView->selectionModel()->blockSignals(false);\n\n        // Cannot update position until layout geometry is calculated, so set it in showEvent().\n        m_pendingVerticalPosition = other->m_treeView->verticalScrollBar()->value();\n    } else {\n        m_searchModel = new Registry::SearchModel(this);\n        setTreeViewModel(Core::Application::instance()->docsetRegistry()->model(), true);\n    }\n\n    connect(m_searchEdit, &QLineEdit::textChanged, this, [this](const QString &text) {\n        QItemSelectionModel *oldSelectionModel = m_treeView->selectionModel();\n\n        if (text.isEmpty()) {\n            setTreeViewModel(Core::Application::instance()->docsetRegistry()->model(), true);\n        } else {\n            setTreeViewModel(m_searchModel, false);\n        }\n\n        QItemSelectionModel *newSelectionModel = m_treeView->selectionModel();\n        if (newSelectionModel != oldSelectionModel) {\n            // TODO: Remove once QTBUG-49966 is addressed.\n            if (oldSelectionModel) {\n                oldSelectionModel->deleteLater();\n            }\n\n            // Connect to the new selection model.\n            connect(m_treeView->selectionModel(), &QItemSelectionModel::selectionChanged,\n                    this, &SearchSidebar::navigateToSelectionWithDelay);\n        }\n\n        m_treeView->reset();\n\n        Core::Application::instance()->docsetRegistry()->search(text);\n    });\n\n    auto toolBarLayout = new QVBoxLayout();\n    toolBarLayout->setContentsMargins(4, 0, 4, 0);\n    toolBarLayout->setSpacing(4);\n\n    toolBarLayout->addWidget(m_searchEdit);\n\n    auto toolBarFrame = new ToolBarFrame();\n    toolBarFrame->setLayout(toolBarLayout);\n\n    // Setup splitter.\n    m_splitter = new QSplitter();\n    m_splitter->setOrientation(Qt::Vertical);\n    m_splitter->addWidget(m_treeView);\n    m_splitter->addWidget(m_pageTocView);\n    connect(m_splitter, &QSplitter::splitterMoved, this, [this]() {\n        Core::Application::instance()->settings()->tocSplitterState = m_splitter->saveState();\n    });\n\n    // Setup main layout.\n    auto layout = LayoutHelper::createBorderlessLayout<QVBoxLayout>();\n    layout->addWidget(toolBarFrame);\n    layout->addWidget(m_splitter);\n    setLayout(layout);\n\n    // Setup delayed navigation to a page until user makes a pause in typing a search query.\n    m_delayedNavigationTimer = new QTimer(this);\n    m_delayedNavigationTimer->setInterval(400);\n    m_delayedNavigationTimer->setSingleShot(true);\n    connect(m_delayedNavigationTimer, &QTimer::timeout, this, [this]() {\n        const QModelIndex index = m_delayedNavigationTimer->property(\"index\").toModelIndex();\n        if (!index.isValid()) {\n            return;\n        }\n\n        navigateToIndex(index);\n    });\n\n    // Setup Docset Registry.\n    auto registry = Core::Application::instance()->docsetRegistry();\n    using Registry::DocsetRegistry;\n    connect(registry, &DocsetRegistry::searchCompleted,\n            this, [this](const QList<Registry::SearchResult> &results) {\n        if (!isVisible()) {\n            return;\n        }\n\n        m_delayedNavigationTimer->stop();\n\n        m_searchModel->setResults(results);\n\n        const QModelIndex index = m_searchModel->index(0, 0, QModelIndex());\n        m_treeView->setCurrentIndex(index);\n        m_delayedNavigationTimer->setProperty(\"index\", index);\n        m_delayedNavigationTimer->start();\n    });\n\n    connect(registry, &DocsetRegistry::docsetAboutToBeUnloaded, this, [this](const QString &name) {\n        if (isVisible()) {\n            // Disable updates because removeSearchResultWithName can\n            // call {begin,end}RemoveRows multiple times, and cause\n            // degradation of UI responsiveness.\n            m_treeView->setUpdatesEnabled(false);\n            m_searchModel->removeSearchResultWithName(name);\n            m_treeView->setUpdatesEnabled(true);\n        } else {\n            m_searchModel->removeSearchResultWithName(name);\n        }\n\n        setupSearchBoxCompletions();\n    });\n\n    connect(registry, &DocsetRegistry::docsetLoaded, this, [this](const QString &) {\n        setupSearchBoxCompletions();\n    });\n}\n\nvoid SearchSidebar::setTreeViewModel(QAbstractItemModel *model, bool isRootDecorated)\n{\n    QItemSelectionModel *oldSelectionModel = m_treeView->selectionModel();\n\n    m_treeView->setModel(model);\n    m_treeView->setRootIsDecorated(isRootDecorated);\n\n    // Hide all but the first column.\n    for (int i = 1; i < model->columnCount(); ++i) {\n        m_treeView->setColumnHidden(i, true);\n    }\n\n    QItemSelectionModel *newSelectionModel = m_treeView->selectionModel();\n    if (newSelectionModel != oldSelectionModel) {\n        // TODO: Remove once QTBUG-49966 is addressed.\n        if (oldSelectionModel) {\n            oldSelectionModel->deleteLater();\n        }\n\n        // Connect to the new selection model.\n        connect(m_treeView->selectionModel(), &QItemSelectionModel::selectionChanged,\n                this, &SearchSidebar::navigateToSelectionWithDelay);\n    }\n}\n\nSearchSidebar *SearchSidebar::clone(QWidget *parent) const\n{\n    return new SearchSidebar(this, parent);\n}\n\nRegistry::SearchModel *SearchSidebar::pageTocModel() const\n{\n    return m_pageTocModel;\n}\n\nvoid SearchSidebar::focusSearchEdit(bool clear)\n{\n    if (!isVisible()) {\n        m_pendingSearchEditFocus = true;\n        return;\n    }\n\n    m_searchEdit->setFocus();\n\n    if (clear) {\n        m_searchEdit->clearQuery();\n    }\n}\n\nvoid SearchSidebar::search(const Registry::SearchQuery &query)\n{\n    // TODO: Pass Registry::SearchQuery, converting to QString is dumb at this point.\n    m_searchEdit->setText(query.toString());\n}\n\nvoid SearchSidebar::navigateToIndex(const QModelIndex &index)\n{\n    // When triggered by click, cancel delayed navigation request caused by the selection change.\n    if (m_delayedNavigationTimer->isActive()\n            && m_delayedNavigationTimer->property(\"index\").toModelIndex() == index) {\n        m_delayedNavigationTimer->stop();\n    }\n\n    const QVariant url = index.data(Registry::ItemDataRole::UrlRole);\n    if (url.isNull()) {\n        return;\n    }\n\n    emit navigationRequested(url.toUrl());\n}\n\nvoid SearchSidebar::navigateToIndexAndActivate(const QModelIndex &index)\n{\n    const QVariant url = index.data(Registry::ItemDataRole::UrlRole);\n    if (url.isNull()) {\n        return;\n    }\n\n    emit navigationRequested(url.toUrl());\n    emit activated();\n}\n\nvoid SearchSidebar::navigateToSelectionWithDelay(const QItemSelection &selection)\n{\n    if (selection.isEmpty()) {\n        return;\n    }\n\n    m_delayedNavigationTimer->setProperty(\"index\", selection.indexes().first());\n    m_delayedNavigationTimer->start();\n}\n\nvoid SearchSidebar::setupSearchBoxCompletions()\n{\n    QStringList completions;\n    const auto docsets = Core::Application::instance()->docsetRegistry()->docsets();\n    for (const Registry::Docset *docset : docsets) {\n        const QStringList keywords = docset->keywords();\n        if (keywords.isEmpty())\n            continue;\n\n        completions << keywords.constFirst() + QLatin1Char(':');\n    }\n\n    if (completions.isEmpty())\n        return;\n\n    m_searchEdit->setCompletions(completions);\n}\n\nbool SearchSidebar::eventFilter(QObject *object, QEvent *event)\n{\n    if (object == m_searchEdit && event->type() == QEvent::KeyPress) {\n        auto e = static_cast<QKeyEvent *>(event);\n        switch (e->key()) {\n        case Qt::Key_Return:\n            emit activated();\n            break;\n        case Qt::Key_Home:\n        case Qt::Key_End:\n        case Qt::Key_Left:\n        case Qt::Key_Right:\n            if (!m_searchEdit->text().isEmpty()) {\n                break;\n            }\n            [[fallthrough]];\n        case Qt::Key_Down:\n        case Qt::Key_Up:\n        case Qt::Key_PageDown:\n        case Qt::Key_PageUp:\n            QCoreApplication::sendEvent(m_treeView, event);\n            break;\n\n        }\n    }\n\n    return Sidebar::View::eventFilter(object, event);\n}\n\nvoid SearchSidebar::showEvent(QShowEvent *event)\n{\n    Q_UNUSED(event)\n\n    if (m_pendingVerticalPosition > 0) {\n        m_treeView->verticalScrollBar()->setValue(m_pendingVerticalPosition);\n        m_pendingVerticalPosition = 0;\n    }\n\n    if (m_pendingSearchEditFocus) {\n        m_searchEdit->setFocus();\n        m_pendingSearchEditFocus = false;\n    }\n}\n"
  },
  {
    "path": "src/libs/ui/searchsidebar.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_WIDGETUI_SEARCHSIDEBAR_H\n#define ZEAL_WIDGETUI_SEARCHSIDEBAR_H\n\n#include <sidebar/view.h>\n\n#include <QModelIndexList>\n#include <QWidget>\n\nclass QItemSelection;\nclass QSplitter;\nclass QListView;\nclass QTimer;\nclass QTreeView;\n\nnamespace Zeal {\n\nnamespace Registry {\nclass SearchModel;\nclass SearchQuery;\n} // namespace Registry\n\nnamespace WidgetUi {\n\nclass SearchEdit;\n\nclass SearchSidebar final : public Sidebar::View\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(SearchSidebar)\npublic:\n    explicit SearchSidebar(QWidget *parent = nullptr);\n    SearchSidebar *clone(QWidget *parent = nullptr) const;\n\n    Registry::SearchModel *pageTocModel() const;\n\nsignals:\n    void activated();\n    void navigationRequested(const QUrl &url);\n\npublic slots:\n    void focusSearchEdit(bool clear = false);\n    void search(const Registry::SearchQuery &query);\n\nprivate slots:\n    void navigateToIndex(const QModelIndex &index);\n    void navigateToIndexAndActivate(const QModelIndex &index);\n    void navigateToSelectionWithDelay(const QItemSelection &selection);\n    void setupSearchBoxCompletions();\n\nprotected:\n    bool eventFilter(QObject *object, QEvent *event) override;\n    void showEvent(QShowEvent *event) override;\n\nprivate:\n    explicit SearchSidebar(const SearchSidebar *other, QWidget *parent = nullptr);\n    void setTreeViewModel(QAbstractItemModel *model, bool isRootDecorated);\n\n    SearchEdit *m_searchEdit = nullptr;\n    bool m_pendingSearchEditFocus = false;\n\n    // Index and search results tree view state.\n    QTreeView *m_treeView = nullptr;\n    QModelIndexList m_expandedIndexList;\n    int m_pendingVerticalPosition = 0;\n    Registry::SearchModel *m_searchModel = nullptr;\n\n    // TOC list view state.\n    QListView *m_pageTocView = nullptr;\n    Registry::SearchModel *m_pageTocModel = nullptr;\n\n    QSplitter *m_splitter = nullptr;\n    QTimer *m_delayedNavigationTimer = nullptr;\n};\n\n} // namespace WidgetUi\n} // namespace Zeal\n\n#endif // ZEAL_WIDGETUI_SEARCHSIDEBAR_H\n"
  },
  {
    "path": "src/libs/ui/settingsdialog.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"settingsdialog.h\"\n#include \"ui_settingsdialog.h\"\n\n#include <qxtglobalshortcut/qxtglobalshortcut.h>\n\n#include <browser/settings.h>\n#include <core/application.h>\n#include <core/settings.h>\n\n#include <QDir>\n#include <QFileDialog>\n#include <QWebEngineProfile>\n#include <QWebEngineSettings>\n\nusing namespace Zeal;\nusing namespace Zeal::WidgetUi;\n\nnamespace {\n// QFontDatabase::standardSizes() lacks some sizes, like 13, which QtWK uses by default.\nconstexpr int AvailableFontSizes[] = {9, 10, 11, 12, 13, 14, 15, 16, 17, 18,\n                                      20, 22, 24, 26, 28, 30, 32, 34, 36,\n                                      40, 44, 48, 56, 64, 72};\nconstexpr QWebEngineSettings::FontFamily BasicFontFamilies[] = {QWebEngineSettings::SerifFont,\n                                                                QWebEngineSettings::SansSerifFont,\n                                                                QWebEngineSettings::FixedFont};\n} // namespace\n\nSettingsDialog::SettingsDialog(QWidget *parent)\n    : QDialog(parent)\n    , ui(new Ui::SettingsDialog())\n{\n    ui->setupUi(this);\n    setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);\n\n    // Setup signals & slots\n    connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::saveSettings);\n    connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SettingsDialog::loadSettings);\n    connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton *button) {\n        if (button == ui->buttonBox->button(QDialogButtonBox::Apply))\n            saveSettings();\n    });\n\n    // Fonts\n    ui->defaultFontComboBox->addItem(tr(\"Serif\"), QStringLiteral(\"serif\"));\n    ui->defaultFontComboBox->addItem(tr(\"Sans-serif\"), QStringLiteral(\"sans-serif\"));\n    ui->defaultFontComboBox->addItem(tr(\"Monospace\"), QStringLiteral(\"monospace\"));\n\n    ui->minFontSizeComboBox->addItem(tr(\"None\"), 0);\n    for (int fontSize : AvailableFontSizes) {\n        ui->fontSizeComboBox->addItem(QString::number(fontSize), fontSize);\n        ui->fixedFontSizeComboBox->addItem(QString::number(fontSize), fontSize);\n        ui->minFontSizeComboBox->addItem(QString::number(fontSize), fontSize);\n    }\n\n    // Fix tab order.\n    setTabOrder(ui->defaultFontComboBox, ui->fontSizeComboBox);\n    setTabOrder(ui->fontSizeComboBox, ui->serifFontComboBox);\n\n    // Disable global shortcut settings if not supported.\n    if (!QxtGlobalShortcut::isSupported()) {\n        ui->globalHotKeyGroupBox->setEnabled(false);\n        ui->globalHotKeyGroupBox->setToolTip(tr(\"Global shortcuts are not supported on the current platform.\"));\n    }\n\n    QWebEngineSettings *webSettings = Browser::Settings::defaultProfile()->settings();\n\n    // Avoid casting in each connect.\n    auto currentIndexChangedSignal\n            = static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged);\n\n    auto syncStandardFont = [this, webSettings](QWebEngineSettings::FontFamily fontFamily,\n            const QFont &font) {\n        const int index = ui->defaultFontComboBox->currentIndex();\n        if (BasicFontFamilies[index] == fontFamily) {\n            webSettings->setFontFamily(QWebEngineSettings::StandardFont, font.family());\n        }\n    };\n\n    connect(ui->defaultFontComboBox, currentIndexChangedSignal,\n            this, [webSettings](int index) {\n        const QString fontFamily = webSettings->fontFamily(BasicFontFamilies[index]);\n        webSettings->setFontFamily(QWebEngineSettings::StandardFont, fontFamily);\n    });\n\n    connect(ui->serifFontComboBox, &QFontComboBox::currentFontChanged,\n            this, [webSettings, syncStandardFont](const QFont &font) {\n        webSettings->setFontFamily(QWebEngineSettings::SerifFont, font.family());\n        syncStandardFont(QWebEngineSettings::SerifFont, font);\n    });\n    connect(ui->sansSerifFontComboBox, &QFontComboBox::currentFontChanged,\n            this, [webSettings, syncStandardFont](const QFont &font) {\n        webSettings->setFontFamily(QWebEngineSettings::SansSerifFont, font.family());\n        syncStandardFont(QWebEngineSettings::SansSerifFont, font);\n    });\n    connect(ui->fixedFontComboBox, &QFontComboBox::currentFontChanged,\n            this, [webSettings, syncStandardFont](const QFont &font) {\n        webSettings->setFontFamily(QWebEngineSettings::FixedFont, font.family());\n        syncStandardFont(QWebEngineSettings::FixedFont, font);\n    });\n\n    connect(ui->fontSizeComboBox, currentIndexChangedSignal, this, [webSettings](int index) {\n        webSettings->setFontSize(QWebEngineSettings::DefaultFontSize, AvailableFontSizes[index]);\n    });\n    connect(ui->fixedFontSizeComboBox, currentIndexChangedSignal, this, [webSettings](int index) {\n        webSettings->setFontSize(QWebEngineSettings::DefaultFixedFontSize, AvailableFontSizes[index]);\n    });\n    connect(ui->minFontSizeComboBox, currentIndexChangedSignal, this, [webSettings](int index) {\n        const int fontSize = index == 0 ? 0 : AvailableFontSizes[index-1];\n        webSettings->setFontSize(QWebEngineSettings::MinimumFontSize, fontSize);\n    });\n\n    loadSettings();\n}\n\nSettingsDialog::~SettingsDialog()\n{\n    delete ui;\n}\n\nvoid SettingsDialog::chooseCustomCssFile()\n{\n    const QString file = QFileDialog::getOpenFileName(this, tr(\"Choose CSS File\"),\n                                                      ui->customCssFileEdit->text(),\n                                                      tr(\"CSS Files (*.css);;All Files (*.*)\"));\n    if (file.isEmpty())\n        return;\n\n    ui->customCssFileEdit->setText(QDir::toNativeSeparators(file));\n}\n\nvoid SettingsDialog::chooseDocsetStoragePath()\n{\n    QString path = QFileDialog::getExistingDirectory(this, tr(\"Open Directory\"),\n                                                     ui->docsetStorageEdit->text());\n    if (path.isEmpty()) {\n        return;\n    }\n\n#ifdef PORTABLE_BUILD\n    // Use relative path if selected directory is under the application binary path.\n    if (path.startsWith(QCoreApplication::applicationDirPath() + QLatin1String(\"/\"))) {\n        const QDir appDirPath(QCoreApplication::applicationDirPath());\n        path = appDirPath.relativeFilePath(path);\n    }\n#endif\n\n    ui->docsetStorageEdit->setText(QDir::toNativeSeparators(path));\n}\n\nvoid SettingsDialog::loadSettings()\n{\n    const Core::Settings * const settings = Core::Application::instance()->settings();\n\n    // General Tab\n    ui->startMinimizedCheckBox->setChecked(settings->startMinimized);\n    ui->checkForUpdateCheckBox->setChecked(settings->checkForUpdate);\n\n    ui->systrayGroupBox->setChecked(settings->showSystrayIcon);\n    ui->minimizeToSystrayCheckBox->setChecked(settings->minimizeToSystray);\n    ui->hideToSystrayCheckBox->setChecked(settings->hideOnClose);\n\n    ui->toolButton->setKeySequence(settings->showShortcut);\n\n    ui->docsetStorageEdit->setText(QDir::toNativeSeparators(settings->docsetPath));\n\n    // Tabs Tab\n    ui->openNewTabAfterActive->setChecked(settings->openNewTabAfterActive);\n\n    // Search Tab\n    ui->fuzzySearchCheckBox->setChecked(settings->isFuzzySearchEnabled);\n\n    // Content Tab\n    for (int i = 0; i < ui->defaultFontComboBox->count(); ++i) {\n        if (ui->defaultFontComboBox->itemData(i).toString() == settings->defaultFontFamily) {\n            ui->defaultFontComboBox->setCurrentIndex(i);\n            break;\n        }\n    }\n    ui->serifFontComboBox->setCurrentText(settings->serifFontFamily);\n    ui->sansSerifFontComboBox->setCurrentText(settings->sansSerifFontFamily);\n    ui->fixedFontComboBox->setCurrentText(settings->fixedFontFamily);\n\n    ui->fontSizeComboBox->setCurrentText(QString::number(settings->defaultFontSize));\n    ui->fixedFontSizeComboBox->setCurrentText(QString::number(settings->defaultFixedFontSize));\n    ui->minFontSizeComboBox->setCurrentText(QString::number(settings->minimumFontSize));\n\n    switch (settings->contentAppearance) {\n    case Core::Settings::ContentAppearance::Automatic:\n        ui->appearanceAutoRadioButton->setChecked(true);\n        break;\n    case Core::Settings::ContentAppearance::Light:\n        ui->appearanceLightRadioButton->setChecked(true);\n        break;\n    case Core::Settings::ContentAppearance::Dark:\n        ui->appearanceDarkRadioButton->setChecked(true);\n        break;\n    }\n\n    ui->highlightOnNavigateCheckBox->setChecked(settings->isHighlightOnNavigateEnabled);\n    ui->customCssFileEdit->setText(QDir::toNativeSeparators(settings->customCssFile));\n\n    switch (settings->externalLinkPolicy) {\n    case Core::Settings::ExternalLinkPolicy::Ask:\n        ui->radioExternalLinkAsk->setChecked(true);\n        break;\n    case Core::Settings::ExternalLinkPolicy::Open:\n        ui->radioExternalLinkOpen->setChecked(true);\n        break;\n    case Core::Settings::ExternalLinkPolicy::OpenInSystemBrowser:\n        ui->radioExternalLinkOpenDesktop->setChecked(true);\n        break;\n    }\n\n    ui->useSmoothScrollingCheckBox->setChecked(settings->isSmoothScrollingEnabled);\n\n    // Network Tab\n    switch (settings->proxyType) {\n    case Core::Settings::ProxyType::None:\n        ui->noProxySettings->setChecked(true);\n        break;\n    case Core::Settings::ProxyType::System:\n        ui->systemProxySettings->setChecked(true);\n        break;\n    case Core::Settings::ProxyType::Http:\n        ui->manualProxySettings->setChecked(true);\n        ui->proxyTypeHttpRadioButton->setChecked(true);\n        break;\n    case Core::Settings::ProxyType::Socks5:\n        ui->manualProxySettings->setChecked(true);\n        ui->proxyTypeSocks5RadioButton->setChecked(true);\n        break;\n    }\n\n    ui->proxyHostEdit->setText(settings->proxyHost);\n    ui->proxyPortEdit->setValue(settings->proxyPort);\n    ui->proxyRequiresAuthCheckBox->setChecked(settings->proxyAuthenticate);\n    ui->proxyUsernameEdit->setText(settings->proxyUserName);\n    ui->proxyPasswordEdit->setText(settings->proxyPassword);\n\n    ui->ignoreSslErrorsCheckBox->setChecked(settings->isIgnoreSslErrorsEnabled);\n}\n\nvoid SettingsDialog::saveSettings()\n{\n    Core::Settings * const settings = Core::Application::instance()->settings();\n\n    // General Tab\n    settings->startMinimized = ui->startMinimizedCheckBox->isChecked();\n    settings->checkForUpdate = ui->checkForUpdateCheckBox->isChecked();\n\n    settings->showSystrayIcon = ui->systrayGroupBox->isChecked();\n    settings->minimizeToSystray = ui->minimizeToSystrayCheckBox->isChecked();\n    settings->hideOnClose = ui->hideToSystrayCheckBox->isChecked();\n\n    settings->showShortcut = ui->toolButton->keySequence();\n\n    settings->docsetPath = QDir::fromNativeSeparators(ui->docsetStorageEdit->text());\n\n    // Tabs Tab\n    settings->openNewTabAfterActive = ui->openNewTabAfterActive->isChecked();\n\n    // Search Tab\n    settings->isFuzzySearchEnabled = ui->fuzzySearchCheckBox->isChecked();\n\n    // Content Tab\n#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)\n    // Applying dark mode requires restart.\n    ui->appearanceLabel->setText(tr(\"Appearance (requires restart):\"));\n#endif\n\n    settings->defaultFontFamily = ui->defaultFontComboBox->currentData().toString();\n    settings->serifFontFamily = ui->serifFontComboBox->currentText();\n    settings->sansSerifFontFamily = ui->sansSerifFontComboBox->currentText();\n    settings->fixedFontFamily = ui->fixedFontComboBox->currentText();\n\n    settings->defaultFontSize = ui->fontSizeComboBox->currentData().toInt();\n    settings->defaultFixedFontSize = ui->fixedFontSizeComboBox->currentData().toInt();\n    settings->minimumFontSize = ui->minFontSizeComboBox->currentData().toInt();\n\n    if (ui->appearanceAutoRadioButton->isChecked()) {\n        settings->contentAppearance = Core::Settings::ContentAppearance::Automatic;\n    } else if (ui->appearanceLightRadioButton->isChecked()) {\n        settings->contentAppearance = Core::Settings::ContentAppearance::Light;\n    } else if (ui->appearanceDarkRadioButton->isChecked()) {\n        settings->contentAppearance = Core::Settings::ContentAppearance::Dark;\n    }\n\n    settings->isHighlightOnNavigateEnabled = ui->highlightOnNavigateCheckBox->isChecked();\n    settings->customCssFile = QDir::fromNativeSeparators(ui->customCssFileEdit->text());\n\n    if (ui->radioExternalLinkAsk->isChecked()) {\n        settings->externalLinkPolicy = Core::Settings::ExternalLinkPolicy::Ask;\n    } else if (ui->radioExternalLinkOpen->isChecked()) {\n        settings->externalLinkPolicy = Core::Settings::ExternalLinkPolicy::Open;\n    } else if (ui->radioExternalLinkOpenDesktop->isChecked()) {\n        settings->externalLinkPolicy = Core::Settings::ExternalLinkPolicy::OpenInSystemBrowser;\n    }\n\n    settings->isSmoothScrollingEnabled = ui->useSmoothScrollingCheckBox->isChecked();\n\n    // Network Tab\n    // Proxy settings\n    if (ui->noProxySettings->isChecked()) {\n        settings->proxyType = Core::Settings::ProxyType::None;\n    } else if (ui->systemProxySettings->isChecked()) {\n        settings->proxyType = Core::Settings::ProxyType::System;\n    } else if (ui->manualProxySettings->isChecked()) {\n        if (ui->proxyTypeSocks5RadioButton->isChecked()) {\n            settings->proxyType = Core::Settings::ProxyType::Socks5;\n        } else {\n            settings->proxyType = Core::Settings::ProxyType::Http;\n        }\n    }\n\n    settings->proxyHost = ui->proxyHostEdit->text();\n    settings->proxyPort = ui->proxyPortEdit->text().toUShort();\n    settings->proxyAuthenticate = ui->proxyRequiresAuthCheckBox->isChecked();\n    settings->proxyUserName = ui->proxyUsernameEdit->text();\n    settings->proxyPassword = ui->proxyPasswordEdit->text();\n\n    settings->isIgnoreSslErrorsEnabled = ui->ignoreSslErrorsCheckBox->isChecked();\n\n    settings->save();\n}\n"
  },
  {
    "path": "src/libs/ui/settingsdialog.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_WIDGETUI_SETTINGSDIALOG_H\n#define ZEAL_WIDGETUI_SETTINGSDIALOG_H\n\n#include <QDialog>\n\nnamespace Zeal {\nnamespace WidgetUi {\n\nnamespace Ui {\nclass SettingsDialog;\n} // namespace Ui\n\nclass SettingsDialog : public QDialog\n{\n    Q_OBJECT\npublic:\n    explicit SettingsDialog(QWidget *parent = nullptr);\n    ~SettingsDialog() override;\n\npublic slots:\n    void chooseCustomCssFile();\n    void chooseDocsetStoragePath();\n\nprivate:\n    void loadSettings();\n    void saveSettings();\n\nprivate:\n    Ui::SettingsDialog *ui = nullptr;\n\n    friend class Ui::SettingsDialog;\n};\n\n} // namespace WidgetUi\n} // namespace Zeal\n\n#endif // ZEAL_WIDGETUI_SETTINGSDIALOG_H\n"
  },
  {
    "path": "src/libs/ui/settingsdialog.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>Zeal::WidgetUi::SettingsDialog</class>\n <widget class=\"SettingsDialog\" name=\"Zeal::WidgetUi::SettingsDialog\">\n  <property name=\"windowModality\">\n   <enum>Qt::ApplicationModal</enum>\n  </property>\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>600</width>\n    <height>700</height>\n   </rect>\n  </property>\n  <property name=\"windowTitle\">\n   <string>Preferences</string>\n  </property>\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n   <item>\n    <widget class=\"QTabWidget\" name=\"tabWidget\">\n     <property name=\"currentIndex\">\n      <number>0</number>\n     </property>\n     <widget class=\"QWidget\" name=\"generalTab\">\n      <attribute name=\"title\">\n       <string>General</string>\n      </attribute>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_7\">\n       <item>\n        <widget class=\"QGroupBox\" name=\"groupBox_3\">\n         <property name=\"title\">\n          <string>Startup</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_9\">\n          <item>\n           <widget class=\"QCheckBox\" name=\"startMinimizedCheckBox\">\n            <property name=\"text\">\n             <string>Start minimized</string>\n            </property>\n           </widget>\n          </item>\n          <item>\n           <widget class=\"QCheckBox\" name=\"checkForUpdateCheckBox\">\n            <property name=\"text\">\n             <string>Check for update</string>\n            </property>\n            <property name=\"checked\">\n             <bool>true</bool>\n            </property>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QGroupBox\" name=\"systrayGroupBox\">\n         <property name=\"title\">\n          <string>Show system tray icon</string>\n         </property>\n         <property name=\"checkable\">\n          <bool>true</bool>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_8\">\n          <item>\n           <widget class=\"QCheckBox\" name=\"minimizeToSystrayCheckBox\">\n            <property name=\"text\">\n             <string>Minimize to system tray instead of taskbar</string>\n            </property>\n           </widget>\n          </item>\n          <item>\n           <widget class=\"QCheckBox\" name=\"hideToSystrayCheckBox\">\n            <property name=\"text\">\n             <string>Hide to system tray on window close</string>\n            </property>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QGroupBox\" name=\"globalHotKeyGroupBox\">\n         <property name=\"title\">\n          <string>Global shortcuts</string>\n         </property>\n         <layout class=\"QFormLayout\" name=\"formLayout_2\">\n          <property name=\"fieldGrowthPolicy\">\n           <enum>QFormLayout::AllNonFixedFieldsGrow</enum>\n          </property>\n          <item row=\"0\" column=\"0\">\n           <widget class=\"QLabel\" name=\"label_9\">\n            <property name=\"text\">\n             <string>Show Zeal:</string>\n            </property>\n           </widget>\n          </item>\n          <item row=\"0\" column=\"1\">\n           <widget class=\"ShortcutEdit\" name=\"toolButton\">\n            <property name=\"placeholderText\">\n             <string>Click to set shortcut</string>\n            </property>\n            <property name=\"clearButtonEnabled\">\n             <bool>true</bool>\n            </property>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QGroupBox\" name=\"groupBox_4\">\n         <property name=\"title\">\n          <string>Docset storage</string>\n         </property>\n         <layout class=\"QFormLayout\" name=\"formLayout_3\">\n          <property name=\"fieldGrowthPolicy\">\n           <enum>QFormLayout::AllNonFixedFieldsGrow</enum>\n          </property>\n          <item row=\"0\" column=\"0\">\n           <widget class=\"QLabel\" name=\"label_5\">\n            <property name=\"text\">\n             <string>&amp;Directory:</string>\n            </property>\n            <property name=\"buddy\">\n             <cstring>docsetStorageEdit</cstring>\n            </property>\n           </widget>\n          </item>\n          <item row=\"0\" column=\"1\">\n           <layout class=\"QHBoxLayout\" name=\"horizontalLayout_2\">\n            <item>\n             <widget class=\"QLineEdit\" name=\"docsetStorageEdit\">\n              <property name=\"clearButtonEnabled\">\n               <bool>true</bool>\n              </property>\n             </widget>\n            </item>\n            <item>\n             <widget class=\"QPushButton\" name=\"docsetStorageBrowseButton\">\n              <property name=\"text\">\n               <string>&amp;Browse…</string>\n              </property>\n             </widget>\n            </item>\n           </layout>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <spacer name=\"verticalSpacer\">\n         <property name=\"orientation\">\n          <enum>Qt::Vertical</enum>\n         </property>\n         <property name=\"sizeHint\" stdset=\"0\">\n          <size>\n           <width>20</width>\n           <height>40</height>\n          </size>\n         </property>\n        </spacer>\n       </item>\n      </layout>\n     </widget>\n     <widget class=\"QWidget\" name=\"tabsTab\">\n      <attribute name=\"title\">\n       <string>Tabs</string>\n      </attribute>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_6\">\n       <item>\n        <widget class=\"QGroupBox\" name=\"groupBox_6\">\n         <property name=\"title\">\n          <string>Behavior</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_12\">\n          <item>\n           <widget class=\"QCheckBox\" name=\"openNewTabAfterActive\">\n            <property name=\"text\">\n             <string>Open new tab after active</string>\n            </property>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <spacer name=\"verticalSpacer_5\">\n         <property name=\"orientation\">\n          <enum>Qt::Vertical</enum>\n         </property>\n         <property name=\"sizeHint\" stdset=\"0\">\n          <size>\n           <width>20</width>\n           <height>40</height>\n          </size>\n         </property>\n        </spacer>\n       </item>\n      </layout>\n     </widget>\n     <widget class=\"QWidget\" name=\"searchTab\">\n      <attribute name=\"title\">\n       <string>Search</string>\n      </attribute>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_4\">\n       <item>\n        <widget class=\"QGroupBox\" name=\"groupBox_5\">\n         <property name=\"title\">\n          <string>Local search</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_11\">\n          <item>\n           <widget class=\"QCheckBox\" name=\"fuzzySearchCheckBox\">\n            <property name=\"text\">\n             <string>Use fuzzy search (experimental)</string>\n            </property>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <spacer name=\"verticalSpacer_4\">\n         <property name=\"orientation\">\n          <enum>Qt::Vertical</enum>\n         </property>\n         <property name=\"sizeHint\" stdset=\"0\">\n          <size>\n           <width>20</width>\n           <height>40</height>\n          </size>\n         </property>\n        </spacer>\n       </item>\n      </layout>\n     </widget>\n     <widget class=\"QWidget\" name=\"contentTab\">\n      <attribute name=\"title\">\n       <string>Content</string>\n      </attribute>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_3\">\n       <item>\n        <widget class=\"QGroupBox\" name=\"styleGroupBox\">\n         <property name=\"title\">\n          <string>Style</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_17\">\n          <item>\n           <layout class=\"QHBoxLayout\" name=\"horizontalLayout_4\">\n            <item>\n             <widget class=\"QLabel\" name=\"appearanceLabel\">\n              <property name=\"text\">\n               <string>Appearance:</string>\n              </property>\n             </widget>\n            </item>\n            <item>\n             <widget class=\"QRadioButton\" name=\"appearanceAutoRadioButton\">\n              <property name=\"text\">\n               <string>A&amp;utomatic</string>\n              </property>\n              <property name=\"checked\">\n               <bool>true</bool>\n              </property>\n             </widget>\n            </item>\n            <item>\n             <widget class=\"QRadioButton\" name=\"appearanceLightRadioButton\">\n              <property name=\"text\">\n               <string>&amp;Light</string>\n              </property>\n             </widget>\n            </item>\n            <item>\n             <widget class=\"QRadioButton\" name=\"appearanceDarkRadioButton\">\n              <property name=\"text\">\n               <string>&amp;Dark</string>\n              </property>\n             </widget>\n            </item>\n            <item>\n             <spacer name=\"horizontalSpacer_3\">\n              <property name=\"orientation\">\n               <enum>Qt::Horizontal</enum>\n              </property>\n              <property name=\"sizeHint\" stdset=\"0\">\n               <size>\n                <width>40</width>\n                <height>20</height>\n               </size>\n              </property>\n             </spacer>\n            </item>\n           </layout>\n          </item>\n          <item>\n           <layout class=\"QHBoxLayout\" name=\"horizontalLayout\">\n            <item>\n             <widget class=\"QLabel\" name=\"customCssFileLabel\">\n              <property name=\"text\">\n               <string>&amp;Custom CSS file:</string>\n              </property>\n              <property name=\"buddy\">\n               <cstring>customCssFileEdit</cstring>\n              </property>\n             </widget>\n            </item>\n            <item>\n             <widget class=\"QLineEdit\" name=\"customCssFileEdit\">\n              <property name=\"clearButtonEnabled\">\n               <bool>true</bool>\n              </property>\n             </widget>\n            </item>\n            <item>\n             <widget class=\"QPushButton\" name=\"customCssFileBrowseButton\">\n              <property name=\"text\">\n               <string>Bro&amp;wse…</string>\n              </property>\n             </widget>\n            </item>\n           </layout>\n          </item>\n          <item>\n           <widget class=\"QCheckBox\" name=\"highlightOnNavigateCheckBox\">\n            <property name=\"text\">\n             <string>&amp;Highlight on navigate</string>\n            </property>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QGroupBox\" name=\"fontsGroupBox\">\n         <property name=\"title\">\n          <string>Fonts</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_14\">\n          <item>\n           <layout class=\"QHBoxLayout\" name=\"horizontalLayout_3\" stretch=\"4,0\">\n            <item>\n             <layout class=\"QFormLayout\" name=\"formLayout\">\n              <property name=\"sizeConstraint\">\n               <enum>QLayout::SetMinimumSize</enum>\n              </property>\n              <item row=\"0\" column=\"0\">\n               <widget class=\"QLabel\" name=\"label\">\n                <property name=\"text\">\n                 <string>De&amp;fault:</string>\n                </property>\n                <property name=\"buddy\">\n                 <cstring>defaultFontComboBox</cstring>\n                </property>\n               </widget>\n              </item>\n              <item row=\"0\" column=\"1\">\n               <widget class=\"QComboBox\" name=\"defaultFontComboBox\"/>\n              </item>\n              <item row=\"1\" column=\"0\">\n               <widget class=\"QLabel\" name=\"label_4\">\n                <property name=\"text\">\n                 <string>&amp;Serif:</string>\n                </property>\n                <property name=\"buddy\">\n                 <cstring>serifFontComboBox</cstring>\n                </property>\n               </widget>\n              </item>\n              <item row=\"1\" column=\"1\">\n               <widget class=\"QFontComboBox\" name=\"serifFontComboBox\">\n                <property name=\"fontFilters\">\n                 <set>QFontComboBox::ScalableFonts</set>\n                </property>\n               </widget>\n              </item>\n              <item row=\"2\" column=\"0\">\n               <widget class=\"QLabel\" name=\"label_6\">\n                <property name=\"text\">\n                 <string>Sa&amp;ns-serif:</string>\n                </property>\n                <property name=\"buddy\">\n                 <cstring>sansSerifFontComboBox</cstring>\n                </property>\n               </widget>\n              </item>\n              <item row=\"2\" column=\"1\">\n               <widget class=\"QFontComboBox\" name=\"sansSerifFontComboBox\">\n                <property name=\"fontFilters\">\n                 <set>QFontComboBox::ScalableFonts</set>\n                </property>\n               </widget>\n              </item>\n              <item row=\"3\" column=\"0\">\n               <widget class=\"QLabel\" name=\"label_3\">\n                <property name=\"text\">\n                 <string>&amp;Monospace:</string>\n                </property>\n                <property name=\"buddy\">\n                 <cstring>fixedFontComboBox</cstring>\n                </property>\n               </widget>\n              </item>\n              <item row=\"3\" column=\"1\">\n               <widget class=\"QFontComboBox\" name=\"fixedFontComboBox\">\n                <property name=\"fontFilters\">\n                 <set>QFontComboBox::MonospacedFonts|QFontComboBox::ScalableFonts</set>\n                </property>\n               </widget>\n              </item>\n             </layout>\n            </item>\n            <item>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_16\">\n              <item>\n               <layout class=\"QFormLayout\" name=\"formLayout_6\">\n                <item row=\"0\" column=\"0\">\n                 <widget class=\"QLabel\" name=\"label_7\">\n                  <property name=\"text\">\n                   <string>Si&amp;ze:</string>\n                  </property>\n                  <property name=\"buddy\">\n                   <cstring>fontSizeComboBox</cstring>\n                  </property>\n                 </widget>\n                </item>\n                <item row=\"0\" column=\"1\">\n                 <widget class=\"QComboBox\" name=\"fontSizeComboBox\"/>\n                </item>\n               </layout>\n              </item>\n              <item>\n               <spacer name=\"verticalSpacer_6\">\n                <property name=\"orientation\">\n                 <enum>Qt::Vertical</enum>\n                </property>\n                <property name=\"sizeHint\" stdset=\"0\">\n                 <size>\n                  <width>20</width>\n                  <height>40</height>\n                 </size>\n                </property>\n               </spacer>\n              </item>\n              <item>\n               <layout class=\"QFormLayout\" name=\"formLayout_4\">\n                <item row=\"0\" column=\"0\">\n                 <widget class=\"QLabel\" name=\"label_11\">\n                  <property name=\"text\">\n                   <string>Siz&amp;e:</string>\n                  </property>\n                  <property name=\"buddy\">\n                   <cstring>fixedFontSizeComboBox</cstring>\n                  </property>\n                 </widget>\n                </item>\n                <item row=\"0\" column=\"1\">\n                 <widget class=\"QComboBox\" name=\"fixedFontSizeComboBox\"/>\n                </item>\n               </layout>\n              </item>\n             </layout>\n            </item>\n           </layout>\n          </item>\n          <item>\n           <layout class=\"QHBoxLayout\" name=\"horizontalLayout_5\">\n            <item>\n             <spacer name=\"horizontalSpacer_2\">\n              <property name=\"orientation\">\n               <enum>Qt::Horizontal</enum>\n              </property>\n              <property name=\"sizeHint\" stdset=\"0\">\n               <size>\n                <width>40</width>\n                <height>20</height>\n               </size>\n              </property>\n             </spacer>\n            </item>\n            <item>\n             <widget class=\"QLabel\" name=\"label_2\">\n              <property name=\"text\">\n               <string>Minimum f&amp;ont size:</string>\n              </property>\n              <property name=\"buddy\">\n               <cstring>minFontSizeComboBox</cstring>\n              </property>\n             </widget>\n            </item>\n            <item>\n             <widget class=\"QComboBox\" name=\"minFontSizeComboBox\"/>\n            </item>\n           </layout>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QGroupBox\" name=\"groupBox_7\">\n         <property name=\"title\">\n          <string>External Link Behavior</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_13\">\n          <item>\n           <widget class=\"QRadioButton\" name=\"radioExternalLinkAsk\">\n            <property name=\"text\">\n             <string>&amp;Ask every time</string>\n            </property>\n            <property name=\"checked\">\n             <bool>true</bool>\n            </property>\n           </widget>\n          </item>\n          <item>\n           <widget class=\"QRadioButton\" name=\"radioExternalLinkOpenDesktop\">\n            <property name=\"text\">\n             <string>Open in desktop &amp;browser</string>\n            </property>\n           </widget>\n          </item>\n          <item>\n           <widget class=\"QRadioButton\" name=\"radioExternalLinkOpen\">\n            <property name=\"text\">\n             <string>O&amp;pen in Zeal</string>\n            </property>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QGroupBox\" name=\"groupBox_2\">\n         <property name=\"title\">\n          <string>Other</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_5\">\n          <item>\n           <widget class=\"QCheckBox\" name=\"useSmoothScrollingCheckBox\">\n            <property name=\"text\">\n             <string>Use smoo&amp;th scrolling</string>\n            </property>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <spacer name=\"verticalSpacer_3\">\n         <property name=\"orientation\">\n          <enum>Qt::Vertical</enum>\n         </property>\n         <property name=\"sizeHint\" stdset=\"0\">\n          <size>\n           <width>20</width>\n           <height>40</height>\n          </size>\n         </property>\n        </spacer>\n       </item>\n      </layout>\n     </widget>\n     <widget class=\"QWidget\" name=\"networkTab\">\n      <attribute name=\"title\">\n       <string>Network</string>\n      </attribute>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\n       <item>\n        <widget class=\"QGroupBox\" name=\"groupBox\">\n         <property name=\"title\">\n          <string>Proxy Settings</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_10\">\n          <item>\n           <widget class=\"QRadioButton\" name=\"noProxySettings\">\n            <property name=\"text\">\n             <string>No prox&amp;y</string>\n            </property>\n            <property name=\"checked\">\n             <bool>false</bool>\n            </property>\n           </widget>\n          </item>\n          <item>\n           <widget class=\"QRadioButton\" name=\"systemProxySettings\">\n            <property name=\"enabled\">\n             <bool>true</bool>\n            </property>\n            <property name=\"text\">\n             <string>&amp;Use system proxy settings</string>\n            </property>\n            <property name=\"checked\">\n             <bool>true</bool>\n            </property>\n           </widget>\n          </item>\n          <item>\n           <widget class=\"QRadioButton\" name=\"manualProxySettings\">\n            <property name=\"text\">\n             <string>&amp;Manual proxy configuration</string>\n            </property>\n           </widget>\n          </item>\n          <item>\n           <widget class=\"QWidget\" name=\"manualProxySettingsGroup\" native=\"true\">\n            <property name=\"enabled\">\n             <bool>false</bool>\n            </property>\n            <layout class=\"QFormLayout\" name=\"manualProxyConfigurationLayout\">\n             <item row=\"0\" column=\"0\">\n              <widget class=\"QLabel\" name=\"proxyHostLabel\">\n               <property name=\"text\">\n                <string>Pro&amp;xy host:</string>\n               </property>\n               <property name=\"buddy\">\n                <cstring>proxyHostEdit</cstring>\n               </property>\n              </widget>\n             </item>\n             <item row=\"1\" column=\"0\">\n              <widget class=\"QLabel\" name=\"proxyTypeLabel\">\n               <property name=\"text\">\n                <string>Type:</string>\n               </property>\n              </widget>\n             </item>\n             <item row=\"1\" column=\"1\">\n              <layout class=\"QHBoxLayout\" name=\"proxyTypeLayout\">\n               <item>\n                <widget class=\"QRadioButton\" name=\"proxyTypeHttpRadioButton\">\n                 <property name=\"text\">\n                  <string>&amp;HTTP</string>\n                 </property>\n                 <property name=\"checked\">\n                  <bool>true</bool>\n                 </property>\n                 <attribute name=\"buttonGroup\">\n                  <string notr=\"true\">proxyTypeButtonGroup</string>\n                 </attribute>\n                </widget>\n               </item>\n               <item>\n                <widget class=\"QRadioButton\" name=\"proxyTypeSocks5RadioButton\">\n                 <property name=\"text\">\n                  <string>&amp;SOCKS5</string>\n                 </property>\n                 <attribute name=\"buttonGroup\">\n                  <string notr=\"true\">proxyTypeButtonGroup</string>\n                 </attribute>\n                </widget>\n               </item>\n               <item>\n                <spacer name=\"horizontalSpacer\">\n                 <property name=\"orientation\">\n                  <enum>Qt::Horizontal</enum>\n                 </property>\n                 <property name=\"sizeHint\" stdset=\"0\">\n                  <size>\n                   <width>40</width>\n                   <height>20</height>\n                  </size>\n                 </property>\n                </spacer>\n               </item>\n              </layout>\n             </item>\n             <item row=\"0\" column=\"1\">\n              <layout class=\"QHBoxLayout\" name=\"proxyHostPortLayout\">\n               <item>\n                <widget class=\"QLineEdit\" name=\"proxyHostEdit\"/>\n               </item>\n               <item>\n                <widget class=\"QLabel\" name=\"proxyPortLabel\">\n                 <property name=\"text\">\n                  <string>&amp;Port:</string>\n                 </property>\n                 <property name=\"buddy\">\n                  <cstring>proxyPortEdit</cstring>\n                 </property>\n                </widget>\n               </item>\n               <item>\n                <widget class=\"QSpinBox\" name=\"proxyPortEdit\">\n                 <property name=\"maximum\">\n                  <number>65535</number>\n                 </property>\n                </widget>\n               </item>\n              </layout>\n             </item>\n             <item row=\"2\" column=\"0\" colspan=\"2\">\n              <widget class=\"QCheckBox\" name=\"proxyRequiresAuthCheckBox\">\n               <property name=\"text\">\n                <string>&amp;Authentication required</string>\n               </property>\n              </widget>\n             </item>\n             <item row=\"3\" column=\"0\">\n              <widget class=\"QLabel\" name=\"proxyUsernameLabel\">\n               <property name=\"enabled\">\n                <bool>false</bool>\n               </property>\n               <property name=\"text\">\n                <string>User&amp;name:</string>\n               </property>\n               <property name=\"buddy\">\n                <cstring>proxyUsernameEdit</cstring>\n               </property>\n              </widget>\n             </item>\n             <item row=\"4\" column=\"0\">\n              <widget class=\"QLabel\" name=\"proxyPasswordLabel\">\n               <property name=\"enabled\">\n                <bool>false</bool>\n               </property>\n               <property name=\"text\">\n                <string>Pass&amp;word:</string>\n               </property>\n               <property name=\"buddy\">\n                <cstring>proxyPasswordEdit</cstring>\n               </property>\n              </widget>\n             </item>\n             <item row=\"3\" column=\"1\">\n              <widget class=\"QLineEdit\" name=\"proxyUsernameEdit\">\n               <property name=\"enabled\">\n                <bool>false</bool>\n               </property>\n              </widget>\n             </item>\n             <item row=\"4\" column=\"1\">\n              <widget class=\"QLineEdit\" name=\"proxyPasswordEdit\">\n               <property name=\"enabled\">\n                <bool>false</bool>\n               </property>\n               <property name=\"echoMode\">\n                <enum>QLineEdit::Password</enum>\n               </property>\n              </widget>\n             </item>\n            </layout>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QGroupBox\" name=\"groupBox_8\">\n         <property name=\"title\">\n          <string>Security</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_15\">\n          <item>\n           <widget class=\"QCheckBox\" name=\"ignoreSslErrorsCheckBox\">\n            <property name=\"text\">\n             <string>Ignore SSL errors</string>\n            </property>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <spacer name=\"verticalSpacer_2\">\n         <property name=\"orientation\">\n          <enum>Qt::Vertical</enum>\n         </property>\n         <property name=\"sizeHint\" stdset=\"0\">\n          <size>\n           <width>20</width>\n           <height>40</height>\n          </size>\n         </property>\n        </spacer>\n       </item>\n      </layout>\n     </widget>\n    </widget>\n   </item>\n   <item>\n    <widget class=\"QDialogButtonBox\" name=\"buttonBox\">\n     <property name=\"standardButtons\">\n      <set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>\n     </property>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <customwidgets>\n  <customwidget>\n   <class>ShortcutEdit</class>\n   <extends>QLineEdit</extends>\n   <header>ui/widgets/shortcutedit.h</header>\n  </customwidget>\n  <customwidget>\n   <class>SettingsDialog</class>\n   <extends>QDialog</extends>\n   <header>ui/settingsdialog.h</header>\n  </customwidget>\n </customwidgets>\n <resources/>\n <connections>\n  <connection>\n   <sender>buttonBox</sender>\n   <signal>accepted()</signal>\n   <receiver>Zeal::WidgetUi::SettingsDialog</receiver>\n   <slot>accept()</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>248</x>\n     <y>254</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>157</x>\n     <y>274</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>buttonBox</sender>\n   <signal>rejected()</signal>\n   <receiver>Zeal::WidgetUi::SettingsDialog</receiver>\n   <slot>reject()</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>316</x>\n     <y>260</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>286</x>\n     <y>274</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>customCssFileBrowseButton</sender>\n   <signal>clicked()</signal>\n   <receiver>Zeal::WidgetUi::SettingsDialog</receiver>\n   <slot>chooseCustomCssFile()</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>526</x>\n     <y>232</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>299</x>\n     <y>249</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>docsetStorageBrowseButton</sender>\n   <signal>clicked()</signal>\n   <receiver>Zeal::WidgetUi::SettingsDialog</receiver>\n   <slot>chooseDocsetStoragePath()</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>526</x>\n     <y>371</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>299</x>\n     <y>249</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>manualProxySettings</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>manualProxySettingsGroup</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>299</x>\n     <y>131</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>299</x>\n     <y>222</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>proxyRequiresAuthCheckBox</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>proxyUsernameLabel</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>299</x>\n     <y>224</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>66</x>\n     <y>250</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>proxyRequiresAuthCheckBox</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>proxyUsernameEdit</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>299</x>\n     <y>224</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>330</x>\n     <y>250</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>proxyRequiresAuthCheckBox</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>proxyPasswordLabel</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>299</x>\n     <y>224</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>65</x>\n     <y>278</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>proxyRequiresAuthCheckBox</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>proxyPasswordEdit</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>299</x>\n     <y>224</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>330</x>\n     <y>278</y>\n    </hint>\n   </hints>\n  </connection>\n </connections>\n <buttongroups>\n  <buttongroup name=\"proxyTypeButtonGroup\"/>\n </buttongroups>\n</ui>\n"
  },
  {
    "path": "src/libs/ui/sidebarviewprovider.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"sidebarviewprovider.h\"\n\n#include \"browsertab.h\"\n#include \"mainwindow.h\"\n#include \"searchsidebar.h\"\n\nusing namespace Zeal;\nusing namespace Zeal::WidgetUi;\n\nSidebarViewProvider::SidebarViewProvider(MainWindow *mainWindow)\n    : Sidebar::ViewProvider(mainWindow)\n    , m_mainWindow(mainWindow)\n{\n    connect(m_mainWindow, &MainWindow::currentTabChanged, this, &SidebarViewProvider::viewChanged, Qt::QueuedConnection);\n}\n\nSidebar::View *SidebarViewProvider::view(const QString &id) const\n{\n    if (id != QLatin1String(\"index\")) {\n        return nullptr;\n    }\n\n    if (auto tab = m_mainWindow->currentTab()) {\n        return tab->searchSidebar();\n    }\n\n    return nullptr;\n}\n"
  },
  {
    "path": "src/libs/ui/sidebarviewprovider.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_WIDGETUI_SIDEBARVIEWPROVIDER_H\n#define ZEAL_WIDGETUI_SIDEBARVIEWPROVIDER_H\n\n#include <sidebar/viewprovider.h>\n\nnamespace Zeal {\nnamespace WidgetUi {\n\nclass MainWindow;\n\nclass SidebarViewProvider : public Sidebar::ViewProvider\n{\n    Q_OBJECT\n    Q_DISABLE_COPY_MOVE(SidebarViewProvider)\npublic:\n    explicit SidebarViewProvider(MainWindow *mainWindow);\n\n    Sidebar::View *view(const QString &id = QString()) const override;\n\nprivate:\n    MainWindow *m_mainWindow = nullptr;\n};\n\n} // namespace WidgetUi\n} // namespace Zeal\n\n#endif // ZEAL_WIDGETUI_SIDEBARVIEWPROVIDER_H\n"
  },
  {
    "path": "src/libs/ui/widgets/CMakeLists.txt",
    "content": "add_library(Widgets STATIC\n    layouthelper.cpp\n    searchedit.cpp\n    shortcutedit.cpp\n    toolbarframe.cpp\n)\n\nfind_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED)\ntarget_link_libraries(Widgets PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)\n"
  },
  {
    "path": "src/libs/ui/widgets/layouthelper.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"layouthelper.h\"\n"
  },
  {
    "path": "src/libs/ui/widgets/layouthelper.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_WIDGETUI_LAYOUTHELPER_H\n#define ZEAL_WIDGETUI_LAYOUTHELPER_H\n\n#include <QLayout>\n\nnamespace Zeal {\nnamespace WidgetUi {\n\nnamespace LayoutHelper {\n\ntemplate<class Layout>\nLayout *createBorderlessLayout()\n{\n    static_assert(std::is_base_of<QLayout, Layout>::value, \"Layout must derive from QLayout\");\n    auto layout = new Layout();\n    layout->setContentsMargins(0, 0, 0, 0);\n    layout->setSpacing(0);\n    return layout;\n}\n\n} // namespace LayoutHelper\n\n} // namespace WidgetUi\n} // namespace Zeal\n\n#endif // ZEAL_WIDGETUI_LAYOUTHELPER_H\n"
  },
  {
    "path": "src/libs/ui/widgets/searchedit.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"searchedit.h\"\n\n#include <registry/searchquery.h>\n\n#include <QCompleter>\n#include <QKeyEvent>\n#include <QLabel>\n#include <QStyle>\n#include <QTimer>\n\nusing namespace Zeal;\nusing namespace Zeal::WidgetUi;\n\nSearchEdit::SearchEdit(QWidget *parent)\n    : QLineEdit(parent)\n{\n    setClearButtonEnabled(true);\n    setPlaceholderText(tr(\"Search\"));\n\n    // Force QStyleSheetStyle wrapping so dynamic color scheme changes apply.\n    setStyleSheet(QStringLiteral(\"QLineEdit {}\"));\n\n    m_completionLabel = new QLabel(this);\n    m_completionLabel->setFont(font());\n    m_completionLabel->setObjectName(QStringLiteral(\"completer\"));\n    m_completionLabel->setStyleSheet(QStringLiteral(\"QLabel#completer { color: gray; }\"));\n\n    connect(this, &SearchEdit::textChanged, this, &SearchEdit::showCompletions);\n}\n\n// Makes the line edit use autocompletion.\nvoid SearchEdit::setCompletions(const QStringList &completions)\n{\n    delete m_prefixCompleter;\n\n    m_prefixCompleter = new QCompleter(completions, this);\n    m_prefixCompleter->setCaseSensitivity(Qt::CaseInsensitive);\n    m_prefixCompleter->setCompletionMode(QCompleter::InlineCompletion);\n    m_prefixCompleter->setWidget(this);\n}\n\n// Clear input with consideration to docset filters\nvoid SearchEdit::clearQuery()\n{\n    setText(text().left(queryStart()));\n}\n\nvoid SearchEdit::selectQuery()\n{\n    if (text().isEmpty())\n        return;\n\n    const int pos = hasSelectedText() ? selectionStart() : cursorPosition();\n    const int queryPos = queryStart();\n    const int textSize = text().size();\n    if (pos >= queryPos && selectionEnd() < textSize) {\n        setSelection(queryPos, textSize);\n        return;\n    }\n\n    // Avoid some race condition which breaks Ctrl+K shortcut.\n    QTimer::singleShot(0, this, &QLineEdit::selectAll);\n}\n\nbool SearchEdit::event(QEvent *event)\n{\n    switch (event->type()) {\n    case QEvent::KeyPress: {\n        auto keyEvent = static_cast<QKeyEvent *>(event);\n        // Tab key cannot be overriden in keyPressEvent().\n        if (keyEvent->key() == Qt::Key_Tab) {\n            const QString completed = currentCompletion(text());\n            if (!completed.isEmpty()) {\n                setText(completed);\n            }\n\n            return true;\n        } else if (keyEvent->key() == Qt::Key_Escape) {\n            clearQuery();\n            return true;\n        }\n\n        break;\n    }\n    case QEvent::ShortcutOverride: {\n        // TODO: Should be obtained from the ActionManager.\n        static const QStringList focusShortcuts = {\n            QStringLiteral(\"Ctrl+K\"),\n            QStringLiteral(\"Ctrl+L\")\n        };\n\n        auto keyEvent = static_cast<QKeyEvent *>(event);\n        const int keyCode = keyEvent->key() | static_cast<int>(keyEvent->modifiers());\n        if (focusShortcuts.contains(QKeySequence(keyCode).toString())) {\n            selectQuery();\n            event->accept();\n            return true;\n        }\n\n        break;\n    }\n    default:\n        break;\n    }\n\n    return QLineEdit::event(event);\n}\n\nvoid SearchEdit::focusInEvent(QFocusEvent *event)\n{\n    QLineEdit::focusInEvent(event);\n\n    // Do not change the default behavior when focused with mouse.\n    if (event->reason() == Qt::MouseFocusReason)\n        return;\n\n    selectQuery();\n}\n\nvoid SearchEdit::showCompletions(const QString &newValue)\n{\n    if (!isVisible())\n        return;\n\n    const int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);\n    const int textWidth = fontMetrics().horizontalAdvance(newValue);\n\n    if (m_prefixCompleter)\n        m_prefixCompleter->setCompletionPrefix(text());\n\n    const QString completed = currentCompletion(newValue).mid(newValue.size());\n    const QSize labelSize(fontMetrics().horizontalAdvance(completed), size().height());\n    const int shiftX = static_cast<int>(window()->devicePixelRatioF() * (frameWidth + 2)) + textWidth;\n\n    m_completionLabel->setMinimumSize(labelSize);\n    m_completionLabel->move(shiftX, 0);\n    m_completionLabel->setText(completed);\n}\n\nQString SearchEdit::currentCompletion(const QString &text) const\n{\n    if (text.isEmpty() || !m_prefixCompleter)\n        return QString();\n\n    return m_prefixCompleter->currentCompletion();\n}\n\nint SearchEdit::queryStart() const\n{\n    const Registry::SearchQuery currentQuery = Registry::SearchQuery::fromString(text());\n    // Keep the filter for the first Escape press\n    return currentQuery.query().isEmpty() ? 0 : currentQuery.keywordPrefixSize();\n}\n"
  },
  {
    "path": "src/libs/ui/widgets/searchedit.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_WIDGETUI_SEARCHEDIT_H\n#define ZEAL_WIDGETUI_SEARCHEDIT_H\n\n#include <QLineEdit>\n\nclass QCompleter;\nclass QEvent;\nclass QLabel;\n\nnamespace Zeal {\nnamespace WidgetUi {\n\nclass SearchEdit : public QLineEdit\n{\n    Q_OBJECT\npublic:\n    explicit SearchEdit(QWidget *parent = nullptr);\n\n    void clearQuery();\n    void selectQuery();\n    void setCompletions(const QStringList &completions);\n\nprotected:\n    bool event(QEvent *event) override;\n    void focusInEvent(QFocusEvent *event) override;\n\nprivate slots:\n    void showCompletions(const QString &text);\n\nprivate:\n    QString currentCompletion(const QString &text) const;\n    int queryStart() const;\n\n    QCompleter *m_prefixCompleter = nullptr;\n    QLabel *m_completionLabel = nullptr;\n};\n\n} // namespace WidgetUi\n} // namespace Zeal\n\n#endif // ZEAL_WIDGETUI_SEARCHEDIT_H\n"
  },
  {
    "path": "src/libs/ui/widgets/shortcutedit.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"shortcutedit.h\"\n\n#include <QEvent>\n#include <QKeyEvent>\n\nusing namespace Zeal::WidgetUi;\n\nShortcutEdit::ShortcutEdit(QWidget *parent)\n    : ShortcutEdit(QString(), parent)\n{\n}\n\nShortcutEdit::ShortcutEdit(const QString &text, QWidget *parent)\n    : QLineEdit(text, parent)\n{\n    connect(this, &QLineEdit::textChanged, [this](const QString &text) {\n#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)\n        m_key = QKeySequence(text, QKeySequence::NativeText)[0];\n#else\n        m_key = QKeySequence(text, QKeySequence::NativeText)[0].toCombined();\n#endif\n    });\n}\n\nbool ShortcutEdit::event(QEvent *event)\n{\n    switch (event->type()) {\n    case QEvent::KeyPress: {\n        auto keyEvent = static_cast<QKeyEvent *>(event);\n        switch (keyEvent->key()) {\n        case Qt::Key_Alt:\n        case Qt::Key_Control:\n        case Qt::Key_Meta:\n        case Qt::Key_Shift:\n            return true;\n        default:\n            m_key = keyEvent->key();\n            m_key |= translateModifiers(keyEvent->modifiers(), keyEvent->text());\n            setText(keySequence().toString(QKeySequence::NativeText));\n        }\n\n        return true;\n    }\n    case QEvent::ShortcutOverride:\n    case QEvent::KeyRelease:\n    case QEvent::Shortcut:\n        return true;\n    default:\n        return QLineEdit::event(event);\n    }\n}\n\nQKeySequence ShortcutEdit::keySequence() const\n{\n    return QKeySequence(m_key);\n}\n\nvoid ShortcutEdit::setKeySequence(const QKeySequence &keySequence)\n{\n    setText(keySequence.toString(QKeySequence::NativeText));\n}\n\n// Inspired by QKeySequenceEditPrivate::translateModifiers()\nint ShortcutEdit::translateModifiers(Qt::KeyboardModifiers state, const QString &text)\n{\n    int modifiers = 0;\n    // The shift modifier only counts when it is not used to type a symbol\n    // that is only reachable using the shift key\n    if ((state & Qt::ShiftModifier)\n            && (text.isEmpty() || !text.at(0).isPrint()\n                || text.at(0).isLetterOrNumber() || text.at(0).isSpace())) {\n        modifiers |= Qt::ShiftModifier;\n    }\n    modifiers |= state & Qt::ControlModifier;\n    modifiers |= state & Qt::MetaModifier;\n    modifiers |= state & Qt::AltModifier;\n    return modifiers;\n}\n"
  },
  {
    "path": "src/libs/ui/widgets/shortcutedit.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2013-2014 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_WIDGETUI_SHORTCUTEDIT_H\n#define ZEAL_WIDGETUI_SHORTCUTEDIT_H\n\n#include <QLineEdit>\n\nnamespace Zeal {\nnamespace WidgetUi {\n\nclass ShortcutEdit : public QLineEdit\n{\n    Q_OBJECT\npublic:\n    explicit ShortcutEdit(QWidget *parent = nullptr);\n    explicit ShortcutEdit(const QString &text, QWidget *parent = nullptr);\n\n    bool event(QEvent *event) override;\n\n    QKeySequence keySequence() const;\n    void setKeySequence(const QKeySequence &keySequence);\n\nprivate:\n    int translateModifiers(Qt::KeyboardModifiers state, const QString &text);\n\n    int m_key = 0;\n};\n\n} // namespace WidgetUi\n} // namespace Zeal\n\n#endif // ZEAL_WIDGETUI_SHORTCUTEDIT_H\n"
  },
  {
    "path": "src/libs/ui/widgets/toolbarframe.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"toolbarframe.h\"\n\n#include <QPainter>\n\nusing namespace Zeal::WidgetUi;\n\nToolBarFrame::ToolBarFrame(QWidget *parent)\n    : QWidget(parent)\n{\n    setMaximumHeight(40);\n    setMinimumHeight(40);\n\n    // Force QStyleSheetStyle wrapping so dynamic color scheme changes apply.\n    setStyleSheet(QStringLiteral(\"ToolBarFrame {}\"));\n}\n\nvoid ToolBarFrame::paintEvent(QPaintEvent *event)\n{\n    QWidget::paintEvent(event);\n\n    // Draw a line at the bottom.\n    QPainter painter(this);\n    painter.setPen(palette().mid().color());\n    painter.drawLine(0, height() - 1, width() - 1, height() - 1);\n}\n"
  },
  {
    "path": "src/libs/ui/widgets/toolbarframe.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_WIDGETUI_TOOLBARFRAME_H\n#define ZEAL_WIDGETUI_TOOLBARFRAME_H\n\n#include <QWidget>\n\nnamespace Zeal {\nnamespace WidgetUi {\n\nclass ToolBarFrame : public QWidget\n{\n    Q_OBJECT\npublic:\n    explicit ToolBarFrame(QWidget *parent = nullptr);\n\nprivate:\n    void paintEvent(QPaintEvent *event) override;\n};\n\n} // namespace WidgetUi\n} // namespace Zeal\n\n#endif // ZEAL_WIDGETUI_TOOLBARFRAME_H\n"
  },
  {
    "path": "src/libs/util/CMakeLists.txt",
    "content": "add_library(Util STATIC\n    fuzzy.cpp\n    humanizer.cpp\n    plist.cpp\n    sqlitedatabase.cpp\n\n    # Show headers without .cpp in Qt Creator.\n    caseinsensitivemap.h\n)\n\nfind_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED)\ntarget_link_libraries(Util PRIVATE Qt${QT_VERSION_MAJOR}::Core)\n\nfind_package(SQLite3 REQUIRED)\ntarget_link_libraries(Util PRIVATE SQLite::SQLite3)\n\n# TODO: Do not export SQLite headers.\ntarget_include_directories(Util PUBLIC ${SQLite3_INCLUDE_DIRS})\n\n# Tests\nif(BUILD_TESTING)\n    add_subdirectory(tests)\nendif()\n"
  },
  {
    "path": "src/libs/util/caseinsensitivemap.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_UTIL_CASEINSENSITIVEMAP_H\n#define ZEAL_UTIL_CASEINSENSITIVEMAP_H\n\n#include <QString>\n\n#include <map>\n\nnamespace Zeal {\nnamespace Util {\n\nstruct CaseInsensitiveStringComparator\n{\n    bool operator()(const QString &lhs, const QString &rhs) const\n    {\n        return QString::compare(lhs, rhs, Qt::CaseInsensitive) < 0;\n    }\n};\n\ntemplate<typename T>\nusing CaseInsensitiveMap = std::map<QString, T, CaseInsensitiveStringComparator>;\n\n} // namespace Util\n} // namespace Zeal\n\n#endif // ZEAL_UTIL_CASEINSENSITIVEMAP_H\n"
  },
  {
    "path": "src/libs/util/fuzzy.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"fuzzy.h\"\n\n#include <algorithm>\n#include <cmath>\n#include <limits>\n\nnamespace Zeal {\nnamespace Util {\nnamespace Fuzzy {\n\nnamespace {\nconstexpr double SCORE_GAP_LEADING = -0.005;\nconstexpr double SCORE_GAP_TRAILING = -0.005;\nconstexpr double SCORE_GAP_INNER = -0.01;\nconstexpr double SCORE_MATCH_CONSECUTIVE = 1.0;\nconstexpr double SCORE_MATCH_SLASH = 0.9;\nconstexpr double SCORE_MATCH_WORD = 0.8;\nconstexpr double SCORE_MATCH_CAPITAL = 0.7;\nconstexpr double SCORE_MATCH_DOT = 0.6;\nconstexpr int FZY_MAX_LEN = 1024;\n\nvoid precomputeBonus(const QString &haystack, double *matchBonus)\n{\n    // Initialize to '/' so the first character of the haystack always receives\n    // SCORE_MATCH_SLASH (0.9), the highest boundary bonus. This mirrors fzy's\n    // original file-path design where every path component starts after a '/'.\n    // For Zeal's symbol names the first character is conceptually a word\n    // boundary, but the high bonus is kept intentionally: it strongly rewards\n    // prefix matches.\n    QChar lastCh = '/';\n    for (int i = 0; i < haystack.length(); ++i) {\n        const QChar ch = haystack[i];\n\n        if (lastCh == '/' || lastCh == '\\\\') {\n            matchBonus[i] = SCORE_MATCH_SLASH;\n        } else if (lastCh == '-' || lastCh == '_' || lastCh == ' ') {\n            matchBonus[i] = SCORE_MATCH_WORD;\n        } else if (lastCh == '.' || lastCh == ':') {\n            matchBonus[i] = SCORE_MATCH_DOT;\n        } else if (lastCh.isLower() && ch.isUpper()) {\n            matchBonus[i] = SCORE_MATCH_CAPITAL;\n        } else {\n            matchBonus[i] = 0.0;\n        }\n\n        lastCh = ch;\n    }\n}\n\n// Check if all characters in needle exist in haystack (in order, case-insensitive)\n// This is a pre-filter before running the expensive DP algorithm\nbool hasMatch(const QString &needle, const QString &haystack)\n{\n    int haystackPos = 0;\n    const int haystackLen = haystack.length();\n\n    for (int i = 0; i < needle.length(); ++i) {\n        const QChar needleCh = needle[i].toLower();\n        bool found = false;\n\n        while (haystackPos < haystackLen) {\n            if (haystack[haystackPos].toLower() == needleCh) {\n                found = true;\n                ++haystackPos;\n                break;\n            }\n            ++haystackPos;\n        }\n\n        if (!found) {\n            return false;\n        }\n    }\n\n    return true;\n}\n\n} // anonymous namespace\n\n// ============================================================================\n// High-level Qt convenience API implementation\n// ============================================================================\n\ndouble score(const QString &needle, const QString &haystack, QVector<int> *positions)\n{\n    // Pre-filter: check if all needle characters exist in haystack (performance optimization)\n    // This avoids expensive DP computation on unmatchable strings\n    if (!needle.isEmpty() && !haystack.isEmpty() && hasMatch(needle, haystack)) {\n        return computeScore(needle, haystack, positions);\n    }\n\n    // No match - return -infinity (SQL will filter with WHERE score > 0)\n    if (positions) {\n        positions->clear();\n    }\n\n    return -std::numeric_limits<double>::infinity();\n}\n\n// ============================================================================\n// Low-level API implementation\n// ============================================================================\n\ndouble computeScore(const QString &needle, const QString &haystack, QVector<int> *positions)\n{\n    const int needleLen = needle.length();\n    const int haystackLen = haystack.length();\n\n    if (needleLen == 0 || haystackLen == 0 || needleLen > haystackLen) {\n        if (positions) {\n            positions->clear();\n        }\n\n        return -std::numeric_limits<double>::infinity();\n    }\n\n    if (needleLen == haystackLen) {\n        // Equal length strings get infinity score only if they actually match (case-insensitive)\n        if (needle.compare(haystack, Qt::CaseInsensitive) == 0) {\n            if (positions) {\n                // Fill positions for exact match: [0, 1, 2, ..., n-1]\n                positions->resize(needleLen);\n                for (int i = 0; i < needleLen; ++i) {\n                    (*positions)[i] = i;\n                }\n            }\n\n            return std::numeric_limits<double>::infinity();\n        }\n\n        // Otherwise return no match - equal length non-matching strings can't fuzzy match\n        if (positions) {\n            positions->clear();\n        }\n\n        return -std::numeric_limits<double>::infinity();\n    }\n\n    if (haystackLen > FZY_MAX_LEN || needleLen > FZY_MAX_LEN) {\n        if (positions) {\n            positions->clear();\n        }\n\n        return -std::numeric_limits<double>::infinity();\n    }\n\n    double matchBonus[FZY_MAX_LEN] = {};  // Zero-initialize to satisfy static analyzer\n    precomputeBonus(haystack, matchBonus);\n\n    const double SCORE_MIN = -std::numeric_limits<double>::infinity();\n\n    // Always allocate 2D tables on heap (simpler, memory overhead negligible for typical searches)\n    double **D = new double*[needleLen];\n    double **M = new double*[needleLen];\n    for (int i = 0; i < needleLen; ++i) {\n        D[i] = new double[haystackLen];\n        M[i] = new double[haystackLen];\n    }\n\n    // Forward pass: compute scores\n    for (int i = 0; i < needleLen; ++i) {\n        double prevScore = SCORE_MIN;\n        const double gapScore = (i == needleLen - 1) ? SCORE_GAP_TRAILING : SCORE_GAP_INNER;\n\n        const QChar needleCh = needle[i].toLower();\n\n        for (int j = 0; j < haystackLen; ++j) {\n            if (needleCh == haystack[j].toLower()) {\n                double score = SCORE_MIN;\n\n                if (i == 0) {\n                    score = (j * SCORE_GAP_LEADING) + matchBonus[j];\n                } else if (j > 0) {\n                    const double prevM = M[i - 1][j - 1];\n                    const double prevD = D[i - 1][j - 1];\n                    score = std::max(prevM + matchBonus[j],\n                                prevD + SCORE_MATCH_CONSECUTIVE);\n                }\n\n                D[i][j] = score;\n                M[i][j] = prevScore = std::max(score, prevScore + gapScore);\n            } else {\n                D[i][j] = SCORE_MIN;\n                M[i][j] = prevScore = prevScore + gapScore;\n            }\n        }\n    }\n\n    const double result = M[needleLen - 1][haystackLen - 1];\n\n    // Backtrack to find positions if requested (fzy algorithm)\n    // Only backtrack if we have a valid match (not SCORE_MIN)\n    if (positions != nullptr && result != SCORE_MIN) {\n        positions->resize(needleLen);\n        bool matchRequired = false;\n\n        for (int i = needleLen - 1, j = haystackLen - 1; i >= 0; --i) {\n            for (; j >= 0; --j) {\n                // Check if this is a valid match position on the optimal path\n                if (D[i][j] != SCORE_MIN && (matchRequired || D[i][j] == M[i][j])) {\n                    // Check if we used consecutive match bonus to get here.\n                    // Use D[i][j] (score at this specific position), not M[i][j]\n                    // (global prefix optimum), which may reflect a different path entirely.\n                    matchRequired = (i > 0 && j > 0 &&\n                                   D[i][j] == D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE);\n                    (*positions)[i] = j;\n                    --j;\n                    break;\n                }\n            }\n        }\n    }\n\n    // Clean up\n    for (int i = 0; i < needleLen; ++i) {\n        delete[] D[i];\n        delete[] M[i];\n    }\n\n    delete[] D;\n    delete[] M;\n\n    return result;\n}\n\ndouble scoreFunction(const QString &needle, const QString &haystack)\n{\n    return score(needle, haystack, nullptr);\n}\n\n// Legacy C-string wrapper for SQLite callback\ndouble scoreFunction(const char *needle, const char *haystack)\n{\n    return scoreFunction(QString::fromUtf8(needle), QString::fromUtf8(haystack));\n}\n\n} // namespace Fuzzy\n} // namespace Util\n} // namespace Zeal\n"
  },
  {
    "path": "src/libs/util/fuzzy.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_UTIL_FUZZY_H\n#define ZEAL_UTIL_FUZZY_H\n\n#include <QString>\n#include <QVector> // TODO: [Qt 6] Use QList.\n\nnamespace Zeal {\nnamespace Util {\nnamespace Fuzzy {\n\n// Fuzzy matching is based on https://github.com/jhawthorn/fzy by John Hawthorn, MIT License.\n\n/**\n * @brief Computes fuzzy match score for QString inputs, optionally returning match positions\n *\n * Convenience wrapper around computeScore() with pre-filtering for performance.\n * Returns raw fzy scores - caller should filter results (e.g., WHERE score > 0 in SQL).\n *\n * Score ranges:\n * - Exact matches (needle == haystack): infinity\n * - Valid fuzzy matches: unbounded; ~0.9 + (n-1) for a perfect length-n consecutive match\n * - Poor matches: negative scores (many gaps outweigh bonuses)\n * - No match: -infinity\n *\n * @param needle Search query\n * @param haystack Text to search in\n * @param positions Optional output list of matched haystack indices for highlighting\n * @return Match score (higher is better, -infinity for no match)\n */\ndouble score(const QString &needle, const QString &haystack, QVector<int> *positions = nullptr);\n\n/**\n * @brief Computes fuzzy match score, optionally returning match positions\n *\n * Low-level scoring function using the fzy algorithm (https://github.com/jhawthorn/fzy).\n * Uses case-insensitive matching with Unicode support via Qt. When positions are requested,\n * backtracks through the DP matrices to find the exact character positions that produced\n * the best score.\n *\n * @param needle Search query\n * @param haystack Text to search in\n * @param positions Optional output list of haystack character indices\n * @return Fuzzy match score (-infinity if no match possible, infinity if needle == haystack,\n *         otherwise unbounded: ~0.9 + (n-1) for a perfect length-n consecutive match)\n */\ndouble computeScore(const QString &needle, const QString &haystack, QVector<int> *positions = nullptr);\n\n/**\n * @brief Main scoring function for use in SQLite callbacks\n *\n * Thin wrapper around score() that provides fzy-based fuzzy matching.\n * Returns raw fzy scores - use WHERE score > 0 in SQL to filter results.\n *\n * Score ranges:\n * - Exact matches (needle == haystack): infinity\n * - Valid fuzzy matches: unbounded; ~0.9 + (n-1) for a perfect length-n consecutive match\n * - Poor matches: negative scores (many gaps outweigh bonuses)\n * - No match: -infinity\n *\n * @param needle Search query\n * @param haystack Text to search in\n * @return Match score (higher is better, -infinity for no match)\n */\ndouble scoreFunction(const QString &needle, const QString &haystack);\n\n/**\n * @brief Legacy C-string wrapper for scoreFunction\n *\n * Provided for compatibility with SQLite callbacks. Converts C strings to QString\n * and calls the QString version.\n *\n * @param needle Search query (UTF-8 C string)\n * @param haystack Text to search in (UTF-8 C string)\n * @return Match score (higher is better, -infinity for no match)\n */\ndouble scoreFunction(const char *needle, const char *haystack);\n\n} // namespace Fuzzy\n} // namespace Util\n} // namespace Zeal\n\n#endif // ZEAL_UTIL_FUZZY_H\n"
  },
  {
    "path": "src/libs/util/humanizer.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"humanizer.h\"\n\n#include <QDateTime>\n\n#include <cmath>\n\nusing namespace Zeal::Util;\n\nnamespace {\n// Time thresholds in seconds.\nconstexpr double Minute = 60.0;\nconstexpr double Hour = 3600.0;\nconstexpr double Day = 86400.0;\n// constexpr double Week = 604800.0; // Not used at the moment.\nconstexpr double Month = 2629746.0; // Average month (30.44 days)\nconstexpr double Year = 31556952.0; // Average year (365.25 days)\n\nQString humanizeDuration(double seconds)\n{\n    if (seconds < 45) {\n        return Humanizer::tr(\"a few seconds\");\n    } else if (seconds < 90) {\n        return Humanizer::tr(\"a minute\");\n    } else if (seconds < 45 * Minute) {\n        const int minutes = static_cast<int>(std::round(seconds / Minute));\n        // return Humanizer::tr(\"%n minute(s)\", nullptr, minutes);\n        return Humanizer::tr(\"%1 minutes\").arg(minutes);\n    } else if (seconds < 90 * Minute) {\n        return Humanizer::tr(\"an hour\");\n    } else if (seconds < 22 * Hour) {\n        const int hours = static_cast<int>(std::round(seconds / Hour));\n        // return Humanizer::tr(\"%n hour(s)\", nullptr, hours);\n        return Humanizer::tr(\"%1 hours\").arg(hours);\n    } else if (seconds < 36 * Hour) {\n        return Humanizer::tr(\"a day\");\n    } else if (seconds < 25 * Day) {\n        const int days = static_cast<int>(std::round(seconds / Day));\n        // return Humanizer::tr(\"%n day(s)\", nullptr, days);\n        return Humanizer::tr(\"%1 days\").arg(days);\n    } else if (seconds < 45 * Day) {\n        return Humanizer::tr(\"a month\");\n    } else if (seconds < 320 * Day) {\n        const int months = static_cast<int>(std::round(seconds / Month));\n        // return Humanizer::tr(\"%n month(s)\", nullptr, months);\n        return Humanizer::tr(\"%1 months\").arg(months);\n    } else if (seconds < 548 * Day) {\n        return Humanizer::tr(\"a year\");\n    } else {\n        const int years = static_cast<int>(std::round(seconds / Year));\n        // return Humanizer::tr(\"%n year(s)\", nullptr, years);\n        return Humanizer::tr(\"%1 years\").arg(years);\n    }\n}\n} // namespace\n\nQString Humanizer::fromNow(const QDateTime& dt)\n{\n    const double seconds = QDateTime::currentDateTime().secsTo(dt);\n    const QString humanizedDuration = humanizeDuration(std::abs(seconds));\n\n    return  (seconds > 0 ? tr(\"%1 from now\") : tr(\"%1 ago\")).arg(humanizedDuration);\n}\n"
  },
  {
    "path": "src/libs/util/humanizer.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_UTIL_HUMANIZER_H\n#define ZEAL_UTIL_HUMANIZER_H\n\n#include <QCoreApplication>\n#include <QString>\n\nclass QDateTime;\n\nnamespace Zeal {\nnamespace Util {\n\nclass Humanizer\n{\n    Q_DECLARE_TR_FUNCTIONS(Humanizer)\n    Q_DISABLE_COPY_MOVE(Humanizer)\n    Humanizer() = delete;\n    ~Humanizer() = delete;\npublic:\n    static QString fromNow(const QDateTime& dt);\n};\n\n} // namespace Util\n} // namespace Zeal\n\n#endif // ZEAL_UTIL_HUMANIZER_H\n"
  },
  {
    "path": "src/libs/util/plist.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"plist.h\"\n\n#include <QFile>\n#include <QLoggingCategory>\n#include <QXmlStreamReader>\n\nusing namespace Zeal::Util;\n\nstatic Q_LOGGING_CATEGORY(log, \"zeal.util.plist\")\n\nbool Plist::read(const QString &fileName)\n{\n    QScopedPointer<QFile> file(new QFile(fileName));\n    if (!file->open(QIODevice::ReadOnly)) {\n        qCWarning(log, \"Cannot open plist file '%s'.\", qPrintable(fileName));\n        m_hasError = true;\n        return false;\n    }\n\n    QXmlStreamReader xml(file.data());\n\n    while (!xml.atEnd()) {\n        const QXmlStreamReader::TokenType token = xml.readNext();\n        if (token != QXmlStreamReader::StartElement)\n            continue;\n\n        if (xml.name() != QLatin1String(\"key\"))\n            continue; // TODO: Should it fail here?\n\n        const QString key = xml.readElementText();\n\n        // Skip whitespaces between tags\n        while (xml.readNext() == QXmlStreamReader::Characters);\n\n        if (xml.tokenType() != QXmlStreamReader::StartElement)\n            continue;\n\n        QVariant value;\n        if (xml.name() == QLatin1String(\"string\"))\n            value = xml.readElementText();\n        else if (xml.name() == QLatin1String(\"true\"))\n            value = true;\n        else if (xml.name() == QLatin1String(\"false\"))\n            value = false;\n        else\n            continue; // Skip unknown types\n\n        insert(key, value);\n    }\n\n    return !m_hasError;\n}\n\nbool Plist::hasError() const\n{\n    return m_hasError;\n}\n"
  },
  {
    "path": "src/libs/util/plist.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_UTIL_PLIST_H\n#define ZEAL_UTIL_PLIST_H\n\n#include <QHash>\n#include <QVariant>\n\nnamespace Zeal {\nnamespace Util {\n\nclass Plist : public QHash<QString, QVariant>\n{\npublic:\n    Plist() = default;\n\n    bool read(const QString &fileName);\n    bool hasError() const;\n\nprivate:\n    bool m_hasError = false;\n};\n\n} // namespace Util\n} // namespace Zeal\n\n#endif // ZEAL_UTIL_PLIST_H\n"
  },
  {
    "path": "src/libs/util/sqlitedatabase.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2016 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"sqlitedatabase.h\"\n\n#include <sqlite3.h>\n\nusing namespace Zeal::Util;\n\nnamespace {\nconstexpr char ListTablesSql[] = \"SELECT name\"\n                                 \"  FROM\"\n                                 \"    (SELECT * FROM sqlite_master UNION ALL\"\n                                 \"    SELECT * FROM sqlite_temp_master)\"\n                                 \"  WHERE type='table'\"\n                                 \"  ORDER BY name\";\nconstexpr char ListViewsSql[] = \"SELECT name\"\n                                \"  FROM\"\n                                \"    (SELECT * FROM sqlite_master UNION ALL\"\n                                \"    SELECT * FROM sqlite_temp_master)\"\n                                \"  WHERE type='view'\"\n                                \"  ORDER BY name\";\n\n// sqlite3_exec() callback used in tables() and views().\nauto ListCallback = [](void *ptr, int, char **data, char **) {\n    static_cast<QStringList *>(ptr)->append(QString::fromUtf8(data[0]));\n    return 0;\n};\n} // namespace\n\nSQLiteDatabase::SQLiteDatabase(const QString &path)\n{\n    if (sqlite3_initialize() != SQLITE_OK) {\n        return;\n    }\n\n    if (sqlite3_open16(path.constData(), &m_db) != SQLITE_OK) {\n        updateLastError();\n        close();\n    }\n}\n\nSQLiteDatabase::~SQLiteDatabase()\n{\n    finalize();\n    close();\n}\n\nbool SQLiteDatabase::isOpen() const\n{\n    return m_db != nullptr;\n}\n\nQStringList SQLiteDatabase::tables()\n{\n    if (m_db == nullptr) {\n        return {};\n    }\n\n    QStringList list;\n    char *errmsg = nullptr;\n    const int rc = sqlite3_exec(m_db, ListTablesSql, ListCallback, &list, &errmsg);\n\n    if (rc != SQLITE_OK) {\n        if (errmsg) {\n            m_lastError = QString::fromUtf8(errmsg);\n            sqlite3_free(errmsg);\n        }\n\n        return {};\n    }\n\n    return list;\n}\n\nQStringList SQLiteDatabase::views()\n{\n    if (m_db == nullptr) {\n        return {};\n    }\n\n    QStringList list;\n    char *errmsg = nullptr;\n    const int rc = sqlite3_exec(m_db, ListViewsSql, ListCallback, &list, &errmsg);\n\n    if (rc != SQLITE_OK) {\n        if (errmsg) {\n            m_lastError = QString::fromUtf8(errmsg);\n            sqlite3_free(errmsg);\n        }\n\n        return {};\n    }\n\n    return list;\n}\n\nbool SQLiteDatabase::prepare(const QString &sql)\n{\n    if (m_db == nullptr) {\n        return false;\n    }\n\n    if (m_stmt != nullptr) {\n        finalize();\n    }\n\n    m_lastError.clear();\n\n    sqlite3_mutex_enter(sqlite3_db_mutex(m_db));\n    const void *pzTail = nullptr;\n    const int res = sqlite3_prepare16_v2(m_db,\n                                         sql.constData(),\n                                         (sql.size() + 1) * 2, // 2 = sizeof(QChar)\n                                         &m_stmt,\n                                         &pzTail);\n    sqlite3_mutex_leave(sqlite3_db_mutex(m_db));\n\n    if (res != SQLITE_OK) {\n        // \"Unable to execute statement\"\n        updateLastError();\n        finalize();\n        return false;\n    }\n\n    if (pzTail && !QString(static_cast<const QChar *>(pzTail)).trimmed().isEmpty()) {\n        // Unable to execute multiple statements at a time\n        updateLastError();\n        finalize();\n        return false;\n    }\n\n    return true;\n}\n\nbool SQLiteDatabase::next()\n{\n    if (m_stmt == nullptr) {\n        return false;\n    }\n\n    sqlite3_mutex_enter(sqlite3_db_mutex(m_db));\n    const int res = sqlite3_step(m_stmt);\n    sqlite3_mutex_leave(sqlite3_db_mutex(m_db));\n\n    switch (res) {\n    case SQLITE_ROW:\n        return true;\n    case SQLITE_DONE:\n    case SQLITE_CONSTRAINT:\n    case SQLITE_ERROR:\n    case SQLITE_MISUSE:\n    case SQLITE_BUSY:\n    default:\n        updateLastError();\n    }\n\n    return false;\n}\n\nbool SQLiteDatabase::execute(const QString &sql)\n{\n    if (m_db == nullptr) {\n        return false;\n    }\n\n    m_lastError.clear();\n\n    char *errmsg = nullptr;\n    const int rc = sqlite3_exec(m_db, sql.toUtf8(), nullptr, nullptr, &errmsg);\n\n    if (rc != SQLITE_OK) {\n        if (errmsg) {\n            m_lastError = QString::fromUtf8(errmsg);\n            sqlite3_free(errmsg);\n        }\n        return false;\n    }\n\n    return true;\n}\n\nQVariant SQLiteDatabase::value(int index) const\n{\n    Q_ASSERT(index >= 0);\n\n    // sqlite3_data_count() returns 0 if m_stmt is nullptr.\n    if (index >= sqlite3_data_count(m_stmt)) {\n        return QVariant();\n    }\n\n    sqlite3_mutex_enter(sqlite3_db_mutex(m_db));\n    const int type = sqlite3_column_type(m_stmt, index);\n\n    QVariant ret;\n\n    switch (type) {\n    case SQLITE_INTEGER:\n        ret = sqlite3_column_int64(m_stmt, index);\n        break;\n    case SQLITE_NULL:\n        ret = QVariant();\n        break;\n    default:\n        ret = QString(static_cast<const QChar *>(sqlite3_column_text16(m_stmt, index)),\n                      sqlite3_column_bytes16(m_stmt, index) / 2); // 2 = sizeof(QChar)\n        break;\n    }\n\n    sqlite3_mutex_leave(sqlite3_db_mutex(m_db));\n    return ret;\n}\n\nQString SQLiteDatabase::lastError() const\n{\n    return m_lastError;\n}\n\nvoid SQLiteDatabase::close()\n{\n    sqlite3_close(m_db);\n    m_db = nullptr;\n}\n\nvoid SQLiteDatabase::finalize()\n{\n    sqlite3_mutex_enter(sqlite3_db_mutex(m_db));\n\n    sqlite3_finalize(m_stmt);\n    m_stmt = nullptr;\n\n    sqlite3_mutex_leave(sqlite3_db_mutex(m_db));\n}\n\nvoid SQLiteDatabase::updateLastError()\n{\n    if (m_db == nullptr) {\n        return;\n    }\n\n    m_lastError = QString(static_cast<const QChar *>(sqlite3_errmsg16(m_db)));\n}\n\nsqlite3 *SQLiteDatabase::handle() const\n{\n    return m_db;\n}\n"
  },
  {
    "path": "src/libs/util/sqlitedatabase.h",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// Copyright (C) 2016 Jerzy Kozera\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#ifndef ZEAL_UTIL_SQLITEDATABASE_H\n#define ZEAL_UTIL_SQLITEDATABASE_H\n\n#include <QStringList>\n#include <QVariant>\n\nstruct sqlite3;\nstruct sqlite3_stmt;\n\nnamespace Zeal {\nnamespace Util {\n\nclass SQLiteDatabase\n{\n    Q_DISABLE_COPY_MOVE(SQLiteDatabase)\npublic:\n    explicit SQLiteDatabase(const QString &path);\n    virtual ~SQLiteDatabase();\n\n    bool isOpen() const;\n\n    QStringList tables();\n    QStringList views();\n\n    bool prepare(const QString &sql);\n    bool next();\n\n    bool execute(const QString &sql);\n\n    QVariant value(int index) const;\n\n    QString lastError() const;\n\n    sqlite3 *handle() const;\n\nprivate:\n    void close();\n    void finalize();\n    void updateLastError();\n\n    sqlite3 *m_db = nullptr;\n    sqlite3_stmt *m_stmt = nullptr;\n    QString m_lastError;\n};\n\n} // namespace Util\n} // namespace Zeal\n\n#endif // ZEAL_UTIL_SQLITEDATABASE_H\n"
  },
  {
    "path": "src/libs/util/tests/CMakeLists.txt",
    "content": "find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test)\n\n# Fuzzy matching tests\nadd_executable(fuzzy_test fuzzy_test.cpp)\ntarget_link_libraries(fuzzy_test PRIVATE Util Qt::Test)\n\nadd_test(NAME fuzzy_test COMMAND fuzzy_test)\n"
  },
  {
    "path": "src/libs/util/tests/fuzzy_test.cpp",
    "content": "// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n#include \"../fuzzy.h\"\n\n#include <QtTest>\n\n#include <limits>\n\nusing namespace Zeal::Util::Fuzzy;\n\nclass FuzzyTest : public QObject\n{\n    Q_OBJECT\n\nprivate slots:\n    void testEmptyStrings();\n    void testExactMatch();\n    void testExactMatchAtWordBoundary();\n    void testFuzzyMatch();\n    void testPositionsFuzzy();\n    void testPositionsExact();\n    void testCamelCase();\n    void testUnicode();\n    void testPubTrimIssue();\n    void testPubProtIssue();\n    void testNoMatch();\n    void testScoreOrdering();\n\n    // Edge cases\n    void testSingleCharacter();\n    void testNeedleLongerThanHaystack();\n    void testEqualLengthNonMatching();\n    void testSpecialCharacters();\n    void testMaxLength();\n\n    // Bonus verification\n    void testSlashBonus();\n    void testBackslashBonus();\n    void testColonBonus();\n    void testDotBonus();\n    void testWordBoundaryBonuses();\n    void testConsecutiveBonus();\n\n    // Negative scores\n    void testManyGapsNegativeScore();\n\n    // Position handling\n    void testPositionsClearedOnNoMatch();\n    void testPositionsForInfinityScore();\n\n    // Return values\n    void testInfinityForEqualLengthMatch();\n    void testNegativeInfinityScenarios();\n\n    // Space handling\n    void testSpaceHandling();\n\n    // Backtracking regression tests\n    void testBacktrackingPrefixConflict();\n    void testBacktrackingWordBoundaryWins();\n};\n\nvoid FuzzyTest::testEmptyStrings()\n{\n    // Empty needle or haystack should return -infinity\n    QCOMPARE(score(QString(), QStringLiteral(\"test\")), -std::numeric_limits<double>::infinity());\n    QCOMPARE(score(QStringLiteral(\"test\"), QString()), -std::numeric_limits<double>::infinity());\n    QCOMPARE(score(QString(), QString()), -std::numeric_limits<double>::infinity());\n}\n\nvoid FuzzyTest::testExactMatch()\n{\n    // Exact match at start should score highest\n    double scoreStart = score(QStringLiteral(\"test\"), QStringLiteral(\"test\"));\n    double scoreMiddle = score(QStringLiteral(\"test\"), QStringLiteral(\"prefix_test\"));\n    double scoreEnd = score(QStringLiteral(\"test\"), QStringLiteral(\"prefix_middle_test\"));\n\n    QVERIFY(scoreStart > 0);\n    QVERIFY(scoreMiddle > 0);\n    QVERIFY(scoreEnd > 0);\n\n    // Start should score higher than middle or end\n    QVERIFY(scoreStart > scoreMiddle);\n    QVERIFY(scoreMiddle > scoreEnd);\n}\n\nvoid FuzzyTest::testExactMatchAtWordBoundary()\n{\n    // Match after word boundary should score higher than match in middle of word\n    double scoreWordBoundary = score(QStringLiteral(\"trim\"), QStringLiteral(\"Publisher.prototype.toTrim\"));\n    double scoreMiddle = score(QStringLiteral(\"trim\"), QStringLiteral(\"sometrimword\"));\n\n    QVERIFY(scoreWordBoundary > 0);\n    QVERIFY(scoreMiddle > 0);\n    QVERIFY(scoreWordBoundary > scoreMiddle);\n}\n\nvoid FuzzyTest::testFuzzyMatch()\n{\n    // Fuzzy match should still work\n    double fuzzyScore = score(QStringLiteral(\"abc\"), QStringLiteral(\"aXbXc\"));\n    QVERIFY(fuzzyScore > 0);\n\n    // Consecutive match should score higher than non-consecutive\n    double consecutive = score(QStringLiteral(\"abc\"), QStringLiteral(\"abc\"));\n    double nonConsecutive = score(QStringLiteral(\"abc\"), QStringLiteral(\"aXbXc\"));\n    QVERIFY(consecutive > nonConsecutive);\n}\n\nvoid FuzzyTest::testPositionsFuzzy()\n{\n    QVector<int> positions;\n    double fuzzyScore = score(QStringLiteral(\"abc\"), QStringLiteral(\"aXbXc\"), &positions);\n\n    QVERIFY(fuzzyScore > 0);\n    QCOMPARE(positions.size(), 3);\n    QCOMPARE(positions[0], 0); // 'a'\n    QCOMPARE(positions[1], 2); // 'b'\n    QCOMPARE(positions[2], 4); // 'c'\n}\n\nvoid FuzzyTest::testPositionsExact()\n{\n    QVector<int> positions;\n    double exactScore = score(QStringLiteral(\"test\"), QStringLiteral(\"prefix_test\"), &positions);\n\n    QVERIFY(exactScore > 0);\n    QCOMPARE(positions.size(), 4);\n    QCOMPARE(positions[0], 7);  // 't'\n    QCOMPARE(positions[1], 8);  // 'e'\n    QCOMPARE(positions[2], 9);  // 's'\n    QCOMPARE(positions[3], 10); // 't'\n}\n\nvoid FuzzyTest::testCamelCase()\n{\n    // Should detect camelCase word boundaries\n    QVector<int> positions;\n    double camelScore = score(QStringLiteral(\"path\"), QStringLiteral(\"HasPermissionForPath\"), &positions);\n\n    QVERIFY(camelScore > 0);\n    QCOMPARE(positions.size(), 4);\n    // Should match 'Path' at the end, not scattered through the string\n    QCOMPARE(positions[0], 16); // 'P'\n    QCOMPARE(positions[1], 17); // 'a'\n    QCOMPARE(positions[2], 18); // 't'\n    QCOMPARE(positions[3], 19); // 'h'\n}\n\nvoid FuzzyTest::testUnicode()\n{\n    // Should handle Unicode correctly\n    double unicodeScore = score(QStringLiteral(\"café\"), QStringLiteral(\"café\"));\n    QVERIFY(unicodeScore > 0);\n\n    // Case-insensitive Unicode\n    double caseScore = score(QStringLiteral(\"café\"), QStringLiteral(\"CAFÉ\"));\n    QVERIFY(caseScore > 0);\n}\n\nvoid FuzzyTest::testPubTrimIssue()\n{\n    // Regression test: \"pubtrim\" should highlight Publisher + toTrim, not prototype\n    QVector<int> positions;\n    score(QStringLiteral(\"pubtrim\"), QStringLiteral(\"Publisher.prototype.toTrim\"), &positions);\n\n    QCOMPARE(positions.size(), 7);\n    // Should match \"Pub\" from Publisher\n    QCOMPARE(positions[0], 0); // 'P'\n    QCOMPARE(positions[1], 1); // 'u'\n    QCOMPARE(positions[2], 2); // 'b'\n    // Should match \"trim\" from toTrim, NOT from prototype\n    QVERIFY(positions[3] >= 20); // 't' should be in \"toTrim\" (position 20+)\n}\n\nvoid FuzzyTest::testPubProtIssue()\n{\n    // Regression test: \"pubprot\" should highlight Publisher + prototype\n    QVector<int> positions;\n    score(QStringLiteral(\"pubprot\"), QStringLiteral(\"Publisher.prototype.fieldsToTrim\"), &positions);\n\n    QCOMPARE(positions.size(), 7);\n    // Should match \"Pub\" from Publisher\n    QCOMPARE(positions[0], 0); // 'P'\n    QCOMPARE(positions[1], 1); // 'u'\n    QCOMPARE(positions[2], 2); // 'b'\n    // Should match \"prot\" from prototype (around position 10-14), not from ToTrim\n    QVERIFY(positions[3] >= 10 && positions[3] <= 14);\n    QVERIFY(positions[6] >= 10 && positions[6] <= 14);\n}\n\nvoid FuzzyTest::testNoMatch()\n{\n    // No match should return -infinity\n    QCOMPARE(score(QStringLiteral(\"xyz\"), QStringLiteral(\"abc\")), -std::numeric_limits<double>::infinity());\n}\n\nvoid FuzzyTest::testScoreOrdering()\n{\n    // Scores should order results correctly\n    double score1 = score(QStringLiteral(\"array\"), QStringLiteral(\"Array\"));\n    double score2 = score(QStringLiteral(\"array\"), QStringLiteral(\"ArrayList\"));\n    double score3 = score(QStringLiteral(\"array\"), QStringLiteral(\"someArrayMethod\"));\n\n    // Exact match at start > camelCase word boundary > middle of word\n    QVERIFY(score1 > score2);\n    QVERIFY(score2 > score3);\n}\n\n// ============================================================================\n// Edge Cases\n// ============================================================================\n\nvoid FuzzyTest::testSingleCharacter()\n{\n    // Single character needle should work\n    double s = score(QStringLiteral(\"a\"), QStringLiteral(\"abc\"));\n    QVERIFY(s > 0);\n\n    // Single character exact match\n    double exact = score(QStringLiteral(\"a\"), QStringLiteral(\"a\"));\n    QCOMPARE(exact, std::numeric_limits<double>::infinity());\n\n    // Single character no match\n    double noMatch = score(QStringLiteral(\"z\"), QStringLiteral(\"abc\"));\n    QCOMPARE(noMatch, -std::numeric_limits<double>::infinity());\n\n    // Single character in haystack\n    double singleHay = score(QStringLiteral(\"ab\"), QStringLiteral(\"a\"));\n    QCOMPARE(singleHay, -std::numeric_limits<double>::infinity());\n}\n\nvoid FuzzyTest::testNeedleLongerThanHaystack()\n{\n    // Needle longer than haystack should return -infinity\n    double s = score(QStringLiteral(\"longstring\"), QStringLiteral(\"short\"));\n    QCOMPARE(s, -std::numeric_limits<double>::infinity());\n}\n\nvoid FuzzyTest::testEqualLengthNonMatching()\n{\n    // Equal length strings that don't match should return -infinity\n    double s = score(QStringLiteral(\"abcd\"), QStringLiteral(\"efgh\"));\n    QCOMPARE(s, -std::numeric_limits<double>::infinity());\n\n    // Case-insensitive equal length match should return infinity\n    double match = score(QStringLiteral(\"test\"), QStringLiteral(\"TEST\"));\n    QCOMPARE(match, std::numeric_limits<double>::infinity());\n}\n\nvoid FuzzyTest::testSpecialCharacters()\n{\n    // Should handle special characters\n    QVector<int> positions;\n    double s = score(QStringLiteral(\"a-b\"), QStringLiteral(\"foo-a-bar-b\"), &positions);\n    QVERIFY(s > 0);\n    QCOMPARE(positions.size(), 3);\n\n    // Underscores\n    double underscore = score(QStringLiteral(\"ab\"), QStringLiteral(\"a_b\"));\n    QVERIFY(underscore > 0);\n\n    // Spaces\n    double space = score(QStringLiteral(\"ab\"), QStringLiteral(\"a b\"));\n    QVERIFY(space > 0);\n}\n\nvoid FuzzyTest::testMaxLength()\n{\n    // FZY_MAX_LEN (1024) gates the DP algorithm only. Equal-length strings use\n    // a simple strcmp shortcut and correctly return infinity regardless of length.\n    // Use unequal lengths to exercise the DP length guard.\n    QString longNeedle(1025, 'a');\n    QString longHaystack(1026, 'a');\n\n    double s = score(longNeedle, longHaystack);\n    QCOMPARE(s, -std::numeric_limits<double>::infinity());\n\n    // Equal-length exact match at the limit should work\n    QString maxNeedle(1024, 'a');\n    QString maxHaystack(1024, 'a');\n    double atLimit = score(maxNeedle, maxHaystack);\n    QCOMPARE(atLimit, std::numeric_limits<double>::infinity());\n}\n\n// ============================================================================\n// Bonus Verification\n// ============================================================================\n\nvoid FuzzyTest::testSlashBonus()\n{\n    // Character after '/' should get SCORE_MATCH_SLASH bonus\n    double afterSlash = score(QStringLiteral(\"test\"), QStringLiteral(\"path/test\"));\n    double noSlash = score(QStringLiteral(\"test\"), QStringLiteral(\"path_test\"));\n\n    QVERIFY(afterSlash > 0);\n    QVERIFY(noSlash > 0);\n    // Slash bonus should score higher than underscore (word boundary)\n    QVERIFY(afterSlash > noSlash);\n}\n\nvoid FuzzyTest::testBackslashBonus()\n{\n    // Character after '\\\\' should get SCORE_MATCH_SLASH bonus (Windows path support)\n    double afterBackslash = score(QStringLiteral(\"test\"), QStringLiteral(\"path\\\\test\"));\n    double noBackslash = score(QStringLiteral(\"test\"), QStringLiteral(\"path_test\"));\n\n    QVERIFY(afterBackslash > 0);\n    QVERIFY(noBackslash > 0);\n    // Backslash bonus should equal slash bonus\n    QVERIFY(afterBackslash > noBackslash);\n}\n\nvoid FuzzyTest::testColonBonus()\n{\n    // Character after ':' should get SCORE_MATCH_DOT bonus (namespace delimiter)\n    double afterColon = score(QStringLiteral(\"method\"), QStringLiteral(\"Class:method\"));\n    double noColon = score(QStringLiteral(\"method\"), QStringLiteral(\"Classmethod\"));\n\n    QVERIFY(afterColon > 0);\n    QVERIFY(noColon > 0);\n    // Colon bonus should score higher than no bonus\n    QVERIFY(afterColon > noColon);\n}\n\nvoid FuzzyTest::testDotBonus()\n{\n    // Character after '.' should get SCORE_MATCH_DOT bonus\n    double afterDot = score(QStringLiteral(\"ext\"), QStringLiteral(\"file.ext\"));\n    double noDot = score(QStringLiteral(\"ext\"), QStringLiteral(\"fileext\"));\n\n    QVERIFY(afterDot > 0);\n    QVERIFY(noDot > 0);\n    QVERIFY(afterDot > noDot);\n}\n\nvoid FuzzyTest::testWordBoundaryBonuses()\n{\n    // Test different word boundary bonuses in order\n    // Slash (0.9) > Word boundary (0.8, underscore/dash/space) > Dot (0.6)\n    double slashScore = score(QStringLiteral(\"t\"), QStringLiteral(\"a/t\"));\n    double underscoreScore = score(QStringLiteral(\"t\"), QStringLiteral(\"a_t\"));\n    double dashScore = score(QStringLiteral(\"t\"), QStringLiteral(\"a-t\"));\n    double spaceScore = score(QStringLiteral(\"t\"), QStringLiteral(\"a t\"));\n    double dotScore = score(QStringLiteral(\"t\"), QStringLiteral(\"a.t\"));\n\n    // Slash should be highest\n    QVERIFY(slashScore > underscoreScore);\n    QVERIFY(slashScore > dotScore);\n\n    // Word boundaries (underscore, dash, space) should be equal and higher than dot\n    QVERIFY(underscoreScore > dotScore);\n    QVERIFY(dashScore > dotScore);\n    QVERIFY(spaceScore > dotScore);\n}\n\nvoid FuzzyTest::testConsecutiveBonus()\n{\n    // Consecutive matches should score higher than non-consecutive\n    double consecutive = score(QStringLiteral(\"abc\"), QStringLiteral(\"abc\"));\n    double withGaps = score(QStringLiteral(\"abc\"), QStringLiteral(\"axbxc\"));\n\n    QVERIFY(consecutive > 0);\n    QVERIFY(withGaps > 0);\n    QVERIFY(consecutive > withGaps);\n}\n\n// ============================================================================\n// Negative Scores\n// ============================================================================\n\nvoid FuzzyTest::testManyGapsNegativeScore()\n{\n    // The first haystack character always receives SCORE_MATCH_SLASH (0.9) because\n    // precomputeBonus initialises lastCh to '/'. Each inner gap costs -0.01, so\n    // 90+ gap characters are needed before the total score goes negative.\n    const QString haystack = QStringLiteral(\"a\") + QString(50, 'x')\n                             + QStringLiteral(\"b\") + QString(50, 'x')\n                             + QStringLiteral(\"c\");\n    double manyGaps = score(QStringLiteral(\"abc\"), haystack);\n\n    // Should still match but with negative score\n    QVERIFY(manyGaps < 0);\n    QVERIFY(manyGaps > -std::numeric_limits<double>::infinity());\n}\n\n// ============================================================================\n// Position Handling\n// ============================================================================\n\nvoid FuzzyTest::testPositionsClearedOnNoMatch()\n{\n    QVector<int> positions;\n    positions.append(1);\n    positions.append(2);\n    positions.append(3);\n\n    // No match should clear positions\n    double s = score(QStringLiteral(\"xyz\"), QStringLiteral(\"abc\"), &positions);\n\n    QCOMPARE(s, -std::numeric_limits<double>::infinity());\n    QCOMPARE(positions.size(), 0);\n}\n\nvoid FuzzyTest::testPositionsForInfinityScore()\n{\n    QVector<int> positions;\n\n    // Equal length exact match should return infinity and fill positions sequentially\n    double s = score(QStringLiteral(\"test\"), QStringLiteral(\"test\"), &positions);\n\n    QCOMPARE(s, std::numeric_limits<double>::infinity());\n    QCOMPARE(positions.size(), 4);\n    QCOMPARE(positions[0], 0);\n    QCOMPARE(positions[1], 1);\n    QCOMPARE(positions[2], 2);\n    QCOMPARE(positions[3], 3);\n}\n\n// ============================================================================\n// Return Values\n// ============================================================================\n\nvoid FuzzyTest::testInfinityForEqualLengthMatch()\n{\n    // Exact case-insensitive match of equal length strings should return infinity\n    QCOMPARE(score(QStringLiteral(\"test\"), QStringLiteral(\"test\")),\n             std::numeric_limits<double>::infinity());\n    QCOMPARE(score(QStringLiteral(\"TeSt\"), QStringLiteral(\"test\")),\n             std::numeric_limits<double>::infinity());\n    QCOMPARE(score(QStringLiteral(\"TEST\"), QStringLiteral(\"test\")),\n             std::numeric_limits<double>::infinity());\n}\n\nvoid FuzzyTest::testNegativeInfinityScenarios()\n{\n    // All scenarios that should return -infinity\n\n    // Empty strings\n    QCOMPARE(score(QString(), QStringLiteral(\"test\")),\n             -std::numeric_limits<double>::infinity());\n    QCOMPARE(score(QStringLiteral(\"test\"), QString()),\n             -std::numeric_limits<double>::infinity());\n    QCOMPARE(score(QString(), QString()),\n             -std::numeric_limits<double>::infinity());\n\n    // No match\n    QCOMPARE(score(QStringLiteral(\"xyz\"), QStringLiteral(\"abc\")),\n             -std::numeric_limits<double>::infinity());\n\n    // Needle longer than haystack\n    QCOMPARE(score(QStringLiteral(\"longer\"), QStringLiteral(\"ab\")),\n             -std::numeric_limits<double>::infinity());\n\n    // Equal length non-matching\n    QCOMPARE(score(QStringLiteral(\"abcd\"), QStringLiteral(\"efgh\")),\n             -std::numeric_limits<double>::infinity());\n\n    // Strings too long (> FZY_MAX_LEN = 1024) — use unequal lengths to reach\n    // the DP length guard (equal-length match shortcuts to +infinity)\n    QString tooLong(1025, 'a');\n    QString tooLongHaystack(1026, 'a');\n    QCOMPARE(score(tooLong, tooLongHaystack),\n             -std::numeric_limits<double>::infinity());\n}\n\n// ============================================================================\n// Space Handling\n// ============================================================================\n\nvoid FuzzyTest::testSpaceHandling()\n{\n    // Spaces are treated as literal characters in fzy algorithm\n    // They must match exactly and give word boundary bonus to the next character\n\n    QVector<int> positions;\n\n    // Space in needle matches space in haystack\n    double spaceMatch = score(QStringLiteral(\"a b\"), QStringLiteral(\"a b\"), &positions);\n    QCOMPARE(spaceMatch, std::numeric_limits<double>::infinity()); // Exact match\n    QCOMPARE(positions.size(), 3);\n    QCOMPARE(positions[0], 0); // 'a'\n    QCOMPARE(positions[1], 1); // ' '\n    QCOMPARE(positions[2], 2); // 'b'\n\n    // Space in needle does NOT match underscore in haystack\n    double noMatch = score(QStringLiteral(\"a b\"), QStringLiteral(\"a_b\"));\n    QCOMPARE(noMatch, -std::numeric_limits<double>::infinity());\n\n    // Character after space gets word boundary bonus.\n    // Note: fzy scores can be slightly negative for valid matches due to leading\n    // gap penalties — test relative ordering, not absolute sign.\n    double afterSpace = score(QStringLiteral(\"b\"), QStringLiteral(\"a b\"));\n    double noBonus = score(QStringLiteral(\"b\"), QStringLiteral(\"ab\"));\n    QVERIFY(afterSpace > -std::numeric_limits<double>::infinity());\n    QVERIFY(noBonus   > -std::numeric_limits<double>::infinity());\n    QVERIFY(afterSpace > noBonus); // Space gives bonus\n\n    // Fuzzy match with space - space must be present\n    double fuzzyWithSpace = score(QStringLiteral(\"a c\"), QStringLiteral(\"a b c\"));\n    QVERIFY(fuzzyWithSpace > 0); // Matches 'a' at 0, 'c' at 4\n\n    positions.clear();\n    score(QStringLiteral(\"a c\"), QStringLiteral(\"a b c\"), &positions);\n    QCOMPARE(positions.size(), 3);\n    QCOMPARE(positions[0], 0); // 'a'\n    QCOMPARE(positions[1], 1); // ' ' (first space — 'a'+'space' are consecutive)\n    QCOMPARE(positions[2], 4); // 'c'\n\n    // Space in needle cannot match non-space in haystack\n    double noSpaceMatch = score(QStringLiteral(\"a b\"), QStringLiteral(\"abc\"));\n    QCOMPARE(noSpaceMatch, -std::numeric_limits<double>::infinity());\n}\n\n// ============================================================================\n// Backtracking Regression Tests\n// ============================================================================\n\nvoid FuzzyTest::testBacktrackingPrefixConflict()\n{\n    // Bug: \"string\" in \"str::to_string\" was highlighting \"st\" + \"ring\" instead\n    // of the complete \"string\" at the word boundary after '_'.\n    //\n    // Root cause: backtracking used M[i][j] instead of D[i][j] to determine\n    // matchRequired. M[i][j] reflects the global prefix optimum (s,t,r at 0,1,2\n    // decaying via gaps), not the path being backtracked, so matchRequired\n    // incorrectly dropped to false and the algorithm fell back to j=0,1.\n    QVector<int> positions;\n    score(QStringLiteral(\"string\"), QStringLiteral(\"str::to_string\"), &positions);\n\n    QCOMPARE(positions.size(), 6);\n    // Must be the consecutive \"string\" from \"to_string\", not split across the string\n    QCOMPARE(positions[0], 8);  // 's'\n    QCOMPARE(positions[1], 9);  // 't'\n    QCOMPARE(positions[2], 10); // 'r'\n    QCOMPARE(positions[3], 11); // 'i'\n    QCOMPARE(positions[4], 12); // 'n'\n    QCOMPARE(positions[5], 13); // 'g'\n}\n\nvoid FuzzyTest::testBacktrackingWordBoundaryWins()\n{\n    // Bug: \"string\" in \"Static String\" was highlighting \"S\" + \"tring\" instead\n    // of the complete \"String\" word (positions 7-12).\n    //\n    // The 'S' at position 0 (slash bonus 0.9) builds up an M score that is\n    // higher than D at position 7 (word boundary bonus 0.8), causing the same\n    // matchRequired bug to drop false and fall back to j=0 for the first char.\n    QVector<int> positions;\n    score(QStringLiteral(\"string\"), QStringLiteral(\"Static String\"), &positions);\n\n    QCOMPARE(positions.size(), 6);\n    // Must be the consecutive \"String\" word, not \"S\" from \"Static\" + \"tring\"\n    QCOMPARE(positions[0], 7);  // 'S'\n    QCOMPARE(positions[1], 8);  // 't'\n    QCOMPARE(positions[2], 9);  // 'r'\n    QCOMPARE(positions[3], 10); // 'i'\n    QCOMPARE(positions[4], 11); // 'n'\n    QCOMPARE(positions[5], 12); // 'g'\n}\n\nQTEST_MAIN(FuzzyTest)\n#include \"fuzzy_test.moc\"\n"
  },
  {
    "path": "vcpkg.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json\",\n  \"builtin-baseline\": \"120deac3062162151622ca4860575a33844ba10b\",\n  \"dependencies\": [\n    {\n      \"name\": \"cpp-httplib\",\n      \"default-features\": false\n    },\n    {\n      \"name\": \"libarchive\",\n      \"default-features\": false\n    },\n    \"openssl\",\n    \"sqlite3\",\n    \"vulkan-headers\"\n  ]\n}\n"
  }
]