Repository: zealdocs/zeal Branch: main Commit: 705ae49fbbea Files: 159 Total size: 1.1 MB Directory structure: gitextract_ypzf_fv3/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── dependabot.yml │ └── workflows/ │ ├── analyze-codeql.yaml │ ├── analyze-coverity.yaml │ ├── appimage/ │ │ ├── Dockerfile │ │ ├── action.yaml │ │ └── entrypoint.sh │ ├── build-check.yaml │ ├── lock.yaml │ ├── release/ │ │ └── cliff.toml │ └── release.yaml ├── .gitignore ├── CHANGELOG.md ├── CMakeLists.txt ├── CMakePresets.json ├── COPYING ├── LICENSES/ │ ├── GPL-3.0-or-later.txt │ ├── LicenseRef-Kapeli.txt │ ├── MIT.txt │ └── MS-RL.txt ├── README.md ├── REUSE.toml ├── assets/ │ ├── CMakeLists.txt │ └── freedesktop/ │ ├── CMakeLists.txt │ ├── org.zealdocs.zeal.appdata.xml.in │ └── org.zealdocs.zeal.desktop ├── cmake/ │ ├── CodeSign.cmake │ ├── GetVersionFromGit.cmake │ └── MacOSXBundleInfo.plist.in ├── pkg/ │ ├── appimage/ │ │ ├── README.md │ │ └── appimage-amd64.yaml │ └── wix/ │ ├── cpack_post_build.cmake │ ├── cpack_pre_build.cmake │ ├── exitdialog.wxs │ ├── patch.xml │ ├── template.xml │ └── ui.wxs ├── src/ │ ├── CMakeLists.txt │ ├── app/ │ │ ├── CMakeLists.txt │ │ ├── main.cpp │ │ ├── resources/ │ │ │ ├── browser/ │ │ │ │ ├── 404.html │ │ │ │ ├── assets/ │ │ │ │ │ └── css/ │ │ │ │ │ ├── highlight.css │ │ │ │ │ └── welcome.css │ │ │ │ └── welcome.html │ │ │ ├── icons/ │ │ │ │ └── README.md │ │ │ └── zeal.icns │ │ ├── versioninfo.rc.in │ │ └── zeal.qrc │ ├── contrib/ │ │ └── cpp-httplib/ │ │ └── httplib.h │ └── libs/ │ ├── CMakeLists.txt │ ├── browser/ │ │ ├── CMakeLists.txt │ │ ├── searchtoolbar.cpp │ │ ├── searchtoolbar.h │ │ ├── settings.cpp │ │ ├── settings.h │ │ ├── urlrequestinterceptor.cpp │ │ ├── urlrequestinterceptor.h │ │ ├── webbridge.cpp │ │ ├── webbridge.h │ │ ├── webcontrol.cpp │ │ ├── webcontrol.h │ │ ├── webpage.cpp │ │ ├── webpage.h │ │ ├── webview.cpp │ │ └── webview.h │ ├── core/ │ │ ├── CMakeLists.txt │ │ ├── application.cpp │ │ ├── application.h │ │ ├── applicationsingleton.cpp │ │ ├── applicationsingleton.h │ │ ├── extractor.cpp │ │ ├── extractor.h │ │ ├── filemanager.cpp │ │ ├── filemanager.h │ │ ├── httpserver.cpp │ │ ├── httpserver.h │ │ ├── networkaccessmanager.cpp │ │ ├── networkaccessmanager.h │ │ ├── settings.cpp │ │ └── settings.h │ ├── registry/ │ │ ├── CMakeLists.txt │ │ ├── cancellationtoken.h │ │ ├── docset.cpp │ │ ├── docset.h │ │ ├── docsetmetadata.cpp │ │ ├── docsetmetadata.h │ │ ├── docsetregistry.cpp │ │ ├── docsetregistry.h │ │ ├── itemdatarole.h │ │ ├── listmodel.cpp │ │ ├── listmodel.h │ │ ├── searchmodel.cpp │ │ ├── searchmodel.h │ │ ├── searchquery.cpp │ │ ├── searchquery.h │ │ └── searchresult.h │ ├── sidebar/ │ │ ├── CMakeLists.txt │ │ ├── container.cpp │ │ ├── container.h │ │ ├── proxyview.cpp │ │ ├── proxyview.h │ │ ├── view.cpp │ │ ├── view.h │ │ ├── viewprovider.cpp │ │ └── viewprovider.h │ ├── ui/ │ │ ├── CMakeLists.txt │ │ ├── aboutdialog.cpp │ │ ├── aboutdialog.h │ │ ├── aboutdialog.ui │ │ ├── browsertab.cpp │ │ ├── browsertab.h │ │ ├── docsetlistitemdelegate.cpp │ │ ├── docsetlistitemdelegate.h │ │ ├── docsetsdialog.cpp │ │ ├── docsetsdialog.h │ │ ├── docsetsdialog.ui │ │ ├── mainwindow.cpp │ │ ├── mainwindow.h │ │ ├── qxtglobalshortcut/ │ │ │ ├── CMakeLists.txt │ │ │ ├── qxtglobalshortcut.cpp │ │ │ ├── qxtglobalshortcut.h │ │ │ ├── qxtglobalshortcut_mac.cpp │ │ │ ├── qxtglobalshortcut_noop.cpp │ │ │ ├── qxtglobalshortcut_p.h │ │ │ ├── qxtglobalshortcut_win.cpp │ │ │ └── qxtglobalshortcut_x11.cpp │ │ ├── searchitemdelegate.cpp │ │ ├── searchitemdelegate.h │ │ ├── searchsidebar.cpp │ │ ├── searchsidebar.h │ │ ├── settingsdialog.cpp │ │ ├── settingsdialog.h │ │ ├── settingsdialog.ui │ │ ├── sidebarviewprovider.cpp │ │ ├── sidebarviewprovider.h │ │ └── widgets/ │ │ ├── CMakeLists.txt │ │ ├── layouthelper.cpp │ │ ├── layouthelper.h │ │ ├── searchedit.cpp │ │ ├── searchedit.h │ │ ├── shortcutedit.cpp │ │ ├── shortcutedit.h │ │ ├── toolbarframe.cpp │ │ └── toolbarframe.h │ └── util/ │ ├── CMakeLists.txt │ ├── caseinsensitivemap.h │ ├── fuzzy.cpp │ ├── fuzzy.h │ ├── humanizer.cpp │ ├── humanizer.h │ ├── plist.cpp │ ├── plist.h │ ├── sqlitedatabase.cpp │ ├── sqlitedatabase.h │ └── tests/ │ ├── CMakeLists.txt │ └── fuzzy_test.cpp └── vcpkg.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # EditorConfig (https://editorconfig.org) root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_size = 4 indent_style = space max_line_length = 100 trim_trailing_whitespace = true [{*.cmake,CMakeLists.txt}] # TODO: indent_size = 2 indent_size = 4 [*.css] indent_size = 2 [*.json] indent_size = 2 [*.{yaml,yml}] indent_size = 2 ================================================ FILE: .gitattributes ================================================ # Autodetect text files * text=auto # Enforce text mode *.c text *.cmake text *.conf text *.cpp text *.css text *.desktop text *.h text *.html text *.in text *.json text *.md text *.qrc text *.ui text *.yaml text *.yml text .editorconfig text .gitignore text CMakeLists.txt text COPYING text # Binary files *.icns binary *.ico binary *.png binary *.woff binary *.woff2 binary ================================================ FILE: .github/CODEOWNERS ================================================ # This file allows automatic assignment of pull requests. # See https://help.github.com/articles/about-codeowners/ * @trollixx ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" commit-message: prefix: ci(github) labels: [] ================================================ FILE: .github/workflows/analyze-codeql.yaml ================================================ name: CodeQL Scan on: push: branches: [main] pull_request: # The branches below must be a subset of the branches above. branches: [main] schedule: - cron: '0 8 * * 6' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: analyze-codeql: name: Analyze runs-on: ubuntu-24.04 permissions: security-events: write steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Initialize CodeQL uses: github/codeql-action/init@v4 - name: Install Dependencies run: | sudo apt-get -y -qq update sudo apt-get -y -qq --no-install-recommends install \ cmake \ extra-cmake-modules \ libarchive-dev \ libgl1-mesa-dev \ libqt6opengl6-dev \ libsqlite3-dev \ libvulkan-dev \ libxcb-keysyms1-dev \ ninja-build \ qt6-base-private-dev \ qt6-webengine-dev \ qt6-webengine-dev-tools - name: Configure & Build uses: lukka/run-cmake@v10 with: configurePreset: ninja-multi buildPreset: ninja-multi-release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 ================================================ FILE: .github/workflows/analyze-coverity.yaml ================================================ name: Coverity Scan on: push: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: analyze-coverity: name: Analyze if: github.repository == 'zealdocs/zeal' runs-on: ubuntu-24.04 steps: - name: Checkout Repository uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install Dependencies run: | sudo apt-get -y -qq update sudo apt-get -y -qq --no-install-recommends install \ cmake \ extra-cmake-modules \ git \ libarchive-dev \ libgl1-mesa-dev \ libqt6opengl6-dev \ libsqlite3-dev \ libvulkan-dev \ libxcb-keysyms1-dev \ ninja-build \ qt6-base-private-dev \ qt6-webengine-dev \ qt6-webengine-dev-tools - name: Configure run: cmake -B build -G Ninja - name: Retrieve Application Version run: | zeal_version=$(> $GITHUB_ENV - name: Coverity Scan uses: vapier/coverity-scan-action@v1 with: command: ninja -C build version: ${{ env.ZEAL_VERSION }} email: ${{ secrets.COVERITY_SCAN_EMAIL }} token: ${{ secrets.COVERITY_SCAN_TOKEN }} - name: Upload Build Log uses: actions/upload-artifact@v7 with: name: build-log path: cov-int/build-log.txt if-no-files-found: ignore ================================================ FILE: .github/workflows/appimage/Dockerfile ================================================ FROM ubuntu:jammy # Force older pipx to use global location. ENV PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin PIPX_MAN_DIR=/usr/local/share/man RUN apt-get update -q -y \ # Install appimage-builder and appimagetool dependencies. && DEBIAN_FRONTEND="noninteractive" apt-get install -q -y --no-install-recommends \ appstream curl desktop-file-utils fakeroot file git gnupg patchelf pipx squashfs-tools zsync \ # Install appimagetool, it has to be extracted because FUSE doesn't work in containers without extra fiddling. && cd /tmp \ && curl -sLO https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage \ && chmod +x appimagetool-x86_64.AppImage \ && ./appimagetool-x86_64.AppImage --appimage-extract \ && mv squashfs-root/ /opt/appimagetool.AppDir \ && ln -s /opt/appimagetool.AppDir/AppRun /usr/local/bin/appimagetool \ && rm appimagetool-x86_64.AppImage \ && cd - \ # Install appimage-builder. && pipx install git+https://github.com/AppImageCrafters/appimage-builder.git@d96cf01e131e01b9f9e713db8efa7d20c57f6a09 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # Set entrypoint. COPY entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] ================================================ FILE: .github/workflows/appimage/action.yaml ================================================ name: 'AppImage Builder' description: 'Create an AppImage with appimage-builder.' inputs: recipe: description: 'Path to the appimage-builder recipe.' required: true default: 'AppImageBuilder.yml' apt_dependencies: description: 'List of packages to install with apt-get.' required: false runs: using: 'docker' image: 'Dockerfile' args: - ${{ inputs.recipe }} - ${{ inputs.apt_dependencies }} ================================================ FILE: .github/workflows/appimage/entrypoint.sh ================================================ #!/bin/bash # Should be in .gitignore. export APPIMAGE_BUILD_DIR=build.appimage # Install dependencies if [ ! -z ${INPUT_APT_DEPENDENCIES+x} ]; then apt-get update -q -y apt-get install -q -y --no-install-recommends ${INPUT_APT_DEPENDENCIES} fi # Run appimage-builder appimage-builder --skip-test --build-dir ${APPIMAGE_BUILD_DIR} --appdir ${APPIMAGE_BUILD_DIR}/AppDir --recipe ${INPUT_RECIPE} ================================================ FILE: .github/workflows/build-check.yaml ================================================ name: Build Check on: push: branches: [main] pull_request: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build-ubuntu: name: ${{ matrix.config.name }} runs-on: ${{ matrix.config.os }} strategy: fail-fast: false matrix: config: - name: Ubuntu 24.04 / Qt 5 os: ubuntu-24.04 qt_packages: >- libqt5x11extras5-dev qtwebengine5-dev configurePreset: ninja-multi buildPreset: ninja-multi-release - name: Ubuntu 24.04 / Qt 5 / Portable os: ubuntu-24.04 qt_packages: >- libqt5x11extras5-dev qtwebengine5-dev configurePreset: ninja-multi-portable buildPreset: ninja-multi-portable-release - name: Ubuntu 24.04 / Qt 6 os: ubuntu-24.04 qt_packages: >- libgl1-mesa-dev libqt6opengl6-dev qt6-base-private-dev qt6-webengine-dev qt6-webengine-dev-tools configurePreset: ninja-multi buildPreset: ninja-multi-release - name: Ubuntu 24.04 / Qt 6 / Portable os: ubuntu-24.04 qt_packages: >- libgl1-mesa-dev libqt6opengl6-dev qt6-base-private-dev qt6-webengine-dev qt6-webengine-dev-tools configurePreset: ninja-multi-portable buildPreset: ninja-multi-portable-release - name: Ubuntu 24.04 ARM64 / Qt 6 os: ubuntu-24.04-arm qt_packages: >- libgl1-mesa-dev libqt6opengl6-dev qt6-base-private-dev qt6-webengine-dev qt6-webengine-dev-tools configurePreset: ninja-multi buildPreset: ninja-multi-release - name: Ubuntu 24.04 ARM64 / Qt 6 / Portable os: ubuntu-24.04-arm qt_packages: >- libgl1-mesa-dev libqt6opengl6-dev qt6-base-private-dev qt6-webengine-dev qt6-webengine-dev-tools configurePreset: ninja-multi-portable buildPreset: ninja-multi-portable-release steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install Dependencies run: | sudo apt-get -y -qq update sudo apt-get -y -qq --no-install-recommends install \ cmake \ extra-cmake-modules \ libarchive-dev \ libsqlite3-dev \ libvulkan-dev \ libxcb-keysyms1-dev \ ninja-build \ ${{ matrix.config.qt_packages }} - name: Configure & Build uses: lukka/run-cmake@v10 with: configurePreset: ${{ matrix.config.configurePreset }} buildPreset: ${{ matrix.config.buildPreset }} build-windows: name: ${{ matrix.config.name }} runs-on: ${{ matrix.config.os }} strategy: fail-fast: false matrix: config: - name: Windows Server 2022 / Qt 5 os: windows-2022 arch: win64_msvc2019_64 qt_modules: >- qtwebengine qt_version: "5.15.2" configurePreset: ninja-multi-vcpkg buildPreset: ninja-multi-vcpkg-release publishArtifacts: false - name: Windows Server 2022 / Qt 5 / Portable os: windows-2022 arch: win64_msvc2019_64 qt_modules: >- qtwebengine qt_version: "5.15.2" configurePreset: ninja-multi-vcpkg-portable buildPreset: ninja-multi-vcpkg-portable-release publishArtifacts: false - name: Windows Server 2022 / Qt 6 os: windows-2022 arch: win64_msvc2022_64 qt_modules: >- qtpositioning qtwebchannel qtwebengine qt_version: "6.10.2" configurePreset: ninja-multi-vcpkg buildPreset: ninja-multi-vcpkg-release publishArtifacts: true - name: Windows Server 2022 / Qt 6 / Portable os: windows-2022 arch: win64_msvc2022_64 qt_modules: >- qtpositioning qtwebchannel qtwebengine qt_version: "6.10.2" configurePreset: ninja-multi-vcpkg-portable buildPreset: ninja-multi-vcpkg-portable-release publishArtifacts: true env: VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/.vcpkg-cache,readwrite VCPKG_BUILD_TYPE: release VCPKG_DEFAULT_TRIPLET: x64-windows steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 # Workaround for https://github.com/lukka/run-vcpkg/issues/251. - name: Prepare vcpkg binary cache uses: actions/cache@v5 with: path: ${{ github.workspace }}/.vcpkg-cache key: vcpkg-${{ matrix.config.os }}-${{ matrix.config.buildPreset }}-${{ hashFiles(format('build/{0}/vcpkg_installed/vcpkg/status', matrix.config.configurePreset)) }} restore-keys: vcpkg-${{ matrix.config.os }}-${{ matrix.config.buildPreset }}- - name: Prepare vcpkg uses: lukka/run-vcpkg@v11 with: vcpkgDirectory: ${{ github.workspace }}/.vcpkg - name: Install Qt uses: jurplel/install-qt-action@v4 with: arch: ${{ matrix.config.arch }} modules: ${{ matrix.config.qt_modules }} version: ${{ matrix.config.qt_version }} cache: true - name: Configure & Build uses: lukka/run-cmake@v10 with: configurePreset: ${{ matrix.config.configurePreset }} buildPreset: ${{ matrix.config.buildPreset }} configurePresetAdditionalArgs: "['-DVCPKG_DISABLE_COMPILER_TRACKING=ON']" - name: Retrieve Application Version run: | $zeal_version = Get-Content build/${{ matrix.config.configurePreset }}/zeal_version Write-Output "Zeal Version: $zeal_version" "ZEAL_VERSION=$zeal_version" >> $env:GITHUB_ENV - name: Package if: matrix.config.publishArtifacts run: cmake --build build --preset ${{ matrix.config.buildPreset }} --target package env: CODESIGN_CERTIFICATE_BASE64: ${{ secrets.CODESIGN_CERTIFICATE_BASE64 }} CODESIGN_PASSWORD: ${{ secrets.CODESIGN_PASSWORD }} - name: Upload ZIP Artifacts if: matrix.config.publishArtifacts uses: actions/upload-artifact@v7 with: name: zeal-${{ env.ZEAL_VERSION }}${{ matrix.config.configurePreset == 'ninja-multi-vcpkg-portable' && '-portable' || '' }}-windows-x64.zip path: | build/${{ matrix.config.configurePreset }}/zeal-*.zip build/${{ matrix.config.configurePreset }}/zeal-*.zip.sha256 - name: Upload MSI Artifacts if: matrix.config.publishArtifacts && matrix.config.configurePreset == 'ninja-multi-vcpkg' uses: actions/upload-artifact@v7 with: name: zeal-${{ env.ZEAL_VERSION }}-windows-x64.msi path: | build/${{ matrix.config.configurePreset }}/zeal-*.msi build/${{ matrix.config.configurePreset }}/zeal-*.msi.sha256 build-appimage: name: AppImage runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Build AppImage uses: ./.github/workflows/appimage/ with: recipe: pkg/appimage/appimage-amd64.yaml apt_dependencies: >- appstream build-essential cmake extra-cmake-modules libarchive-dev libayatana-appindicator3-dev libqt5x11extras5-dev libsqlite3-dev libxcb-keysyms1-dev ninja-build qtbase5-dev qtwebengine5-dev - name: Upload AppImage uses: actions/upload-artifact@v7 with: name: zeal-dev-x86_64.AppImage # TODO: Provide real version. path: zeal-*.AppImage ================================================ FILE: .github/workflows/lock.yaml ================================================ name: Lock Issues on: schedule: - cron: "0 0 * * *" workflow_dispatch: permissions: issues: write pull-requests: write concurrency: group: lock jobs: lock-issues: name: Lock Old Issues if: github.repository == 'zealdocs/zeal' runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@v6 with: issue-inactive-days: 180 issue-lock-reason: "" process-only: issues ================================================ FILE: .github/workflows/release/cliff.toml ================================================ # git-cliff ~ configuration file # https://git-cliff.org/docs/configuration # Set via GITHUB_REPO and GITHUB_TOKEN environment variables. #[remote.github] #owner = "zealdocs" #repo = "zeal" #token = "" [changelog] # Template docs: https://keats.github.io/tera/docs/. body = """\ {% if version and previous.version %} [Full Changelog]({{ self::diff_url() }}) | [Resolved Issues]({{ self::remote_url() }}/milestone/TBD?closed=1) {% endif %}\ {% for group, commits in commits | group_by(attribute="group") %} ### {{ group | striptags | trim }} {% for commit in commits %} {% if commit.github.pr_title -%} {%- set commit_message = commit.github.pr_title -%} {%- else -%} {%- set commit_message = commit.message -%} {%- endif -%} - {% if commit.scope %}**{{ commit.scope }}:** {% endif %}\ {{ commit.message | split(pat="\n") | first | trim }} \ ({{ self::commit_link(id=commit.id) }})\ {% endfor %} {% endfor %} --- {%- if github -%}\ {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} {% raw %}\n{% endraw -%} #### New Contributors {%- endif %} {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} - @{{ contributor.username }} made their first contribution {%- if contributor.pr_number %} in \ {{ self::pr_link(pr_number=contributor.pr_number) }}\ {%- endif %} {%- endfor -%} {%- endif -%} {%- macro remote_url() -%} https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} {%- endmacro -%} {%- macro diff_url() -%} {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }} {%- endmacro -%} {%- macro pr_link(pr_number) -%} [#{{ pr_number }}]({{ self::remote_url() }}/pull/{{ pr_number }}) {%- endmacro -%} {%- macro commit_link(id) -%} [`{{ id | truncate(length=7, end="") }}`]({{ self::remote_url() }}/commit/{{ id }}) {%- endmacro -%} """ # Remove the leading and trailing whitespace from the template. trim = true # Template for the changelog footer. footer = """ """ # Postprocessors. postprocessors = [] [git] # Parse the commits based on https://www.conventionalcommits.org. conventional_commits = true # Filter out the commits that are not conventional. filter_unconventional = true # Process each line of a commit as an individual commit. split_commits = false # Regex for preprocessing the commit messages. commit_preprocessors = [ # Remove issue numbers from commits. { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }, ] # Regex for parsing and grouping commits. commit_parsers = [ { message = "^feat", group = "Features" }, { message = "^fix", group = "Bug Fixes" }, { message = "^perf", group = "Performance" }, { message = "^doc", group = "Documentation" }, { message = "^build", group = "Build System" }, { message = "^ci", group = "CI/CD" }, # Skipped groups: { message = "^chore", skip = true }, { message = "^refactor", skip = true }, ] # Filter out the commits that are not matched by commit parsers. filter_commits = false # Sort the tags topologically. topo_order = false # Sort the commits inside sections by oldest/newest order. sort_commits = "oldest" ================================================ FILE: .github/workflows/release.yaml ================================================ name: Release on: push: tags: - "v*.*.*" # Required for creating GitHub release. permissions: contents: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build-windows: name: ${{ matrix.config.name }} runs-on: ${{ matrix.config.os }} strategy: fail-fast: false matrix: config: - name: Windows Server 2022 / Qt 6 os: windows-2022 arch: win64_msvc2022_64 qt_modules: >- qtpositioning qtwebchannel qtwebengine qt_version: "6.10.2" configurePreset: ninja-multi-vcpkg buildPreset: ninja-multi-vcpkg-release - name: Windows Server 2022 / Qt 6 / Portable os: windows-2022 arch: win64_msvc2022_64 qt_modules: >- qtpositioning qtwebchannel qtwebengine qt_version: "6.10.2" configurePreset: ninja-multi-vcpkg-portable buildPreset: ninja-multi-vcpkg-portable-release env: VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/.vcpkg-cache,readwrite VCPKG_BUILD_TYPE: release VCPKG_DEFAULT_TRIPLET: x64-windows ZEAL_RELEASE_BUILD: ON steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 # Workaround for https://github.com/lukka/run-vcpkg/issues/251. - name: Prepare vcpkg binary cache uses: actions/cache@v5 with: path: ${{ github.workspace }}/.vcpkg-cache key: vcpkg-${{ matrix.config.os }}-${{ matrix.config.buildPreset }}-${{ hashFiles(format('build/{0}/vcpkg_installed/vcpkg/status', matrix.config.configurePreset)) }} restore-keys: vcpkg-${{ matrix.config.os }}-${{ matrix.config.buildPreset }}- - name: Prepare vcpkg uses: lukka/run-vcpkg@v11 with: vcpkgDirectory: ${{ github.workspace }}/.vcpkg - name: Install Qt uses: jurplel/install-qt-action@v4 with: arch: ${{ matrix.config.arch }} modules: ${{ matrix.config.qt_modules }} version: ${{ matrix.config.qt_version }} cache: true - name: Configure & Build uses: lukka/run-cmake@v10 with: configurePreset: ${{ matrix.config.configurePreset }} buildPreset: ${{ matrix.config.buildPreset }} configurePresetAdditionalArgs: "['-DVCPKG_DISABLE_COMPILER_TRACKING=ON']" - name: Retrieve Application Version run: | $zeal_version = Get-Content build/${{ matrix.config.configurePreset }}/zeal_version Write-Output "Zeal Version: $zeal_version" "ZEAL_VERSION=$zeal_version" >> $env:GITHUB_ENV - name: Package run: cmake --build build --preset ${{ matrix.config.buildPreset }} --target package env: CODESIGN_CERTIFICATE_BASE64: ${{ secrets.CODESIGN_CERTIFICATE_BASE64 }} CODESIGN_PASSWORD: ${{ secrets.CODESIGN_PASSWORD }} - name: Update GitHub Release uses: softprops/action-gh-release@v2 with: draft: true # Only upload the following artifacts: # - Portable 7-Zip and ZIP packages. # - Non-portable MSI package. files: | build/${{ matrix.config.configurePreset }}/zeal-${{ env.ZEAL_VERSION }}-portable-windows-x64.* build/${{ matrix.config.configurePreset }}/zeal-${{ env.ZEAL_VERSION }}-windows-x64.msi* build-appimage: name: AppImage runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Build AppImage uses: ./.github/workflows/appimage/ with: recipe: pkg/appimage/appimage-amd64.yaml apt_dependencies: >- appstream build-essential cmake extra-cmake-modules libayatana-appindicator3-dev libarchive-dev libqt5x11extras5-dev libsqlite3-dev libxcb-keysyms1-dev ninja-build qtbase5-dev qtwebengine5-dev - name: Generate Digest Files run: for file in zeal-*.AppImage*; do sha256sum $file > $file.sha256; done - name: Update GitHub Release uses: softprops/action-gh-release@v2 with: draft: true fail_on_unmatched_files: true files: | zeal-*.AppImage* build-ubuntu: name: ${{ matrix.config.name }} runs-on: ${{ matrix.config.os }} strategy: fail-fast: false matrix: config: - name: Ubuntu 24.04 / Source os: ubuntu-24.04 qt_packages: >- libgl1-mesa-dev libqt6opengl6-dev qt6-base-private-dev qt6-webengine-dev qt6-webengine-dev-tools configurePreset: ninja-multi buildPreset: ninja-multi-release env: ZEAL_RELEASE_BUILD: ON steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install Dependencies run: | sudo apt-get -y -qq update sudo apt-get -y -qq --no-install-recommends install \ cmake \ extra-cmake-modules \ libarchive-dev \ libsqlite3-dev \ libvulkan-dev \ libxcb-keysyms1-dev \ ninja-build \ ${{ matrix.config.qt_packages }} - name: Configure & Package Source uses: lukka/run-cmake@v10 with: configurePreset: ${{ matrix.config.configurePreset }} buildPreset: ${{ matrix.config.buildPreset }} buildPresetAdditionalArgs: "['--target package_source']" - name: Generate Changelog uses: orhun/git-cliff-action@v4 id: git-cliff with: args: --latest config: .github/workflows/release/cliff.toml env: GITHUB_REPO: ${{ github.repository }} - name: Update GitHub Release uses: softprops/action-gh-release@v2 with: draft: true fail_on_unmatched_files: true body: ${{ steps.git-cliff.outputs.content }} # Only upload the following artifacts: # - Source packages. files: | build/${{ matrix.config.configurePreset }}/zeal-*.* ================================================ FILE: .gitignore ================================================ # C++ objects and libs *.a *.dll *.dylib *.la *.lai *.lo *.o *.slo *.so # CMake build.*/ build/ CMakeLists.txt.user CMakeUserPresets.json # AppImage Builder *.AppImage *.AppImage.zsync appimage-builder-cache/ squashfs-root/ # WiX Toolset *.msi *.wixobj *.wixpdb # Qt Creator *.autosave # VS Code .vscode/ # Linux appdata /assets/freedesktop/org.zealdocs.zeal.appdata.xml ================================================ FILE: CHANGELOG.md ================================================ # Changelog The version history is available on [GitHub Releases](https://github.com/zealdocs/zeal/releases). ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.16.3) # CMake options. set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) set(CMAKE_DISABLE_SOURCE_CHANGES ON) set(CMAKE_ERROR_DEPRECATED TRUE) set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") project(Zeal VERSION 0.8.0 DESCRIPTION "A simple documentation browser." HOMEPAGE_URL "https://zealdocs.org" LANGUAGES CXX ) # Set to TRUE for a tagged release. # NOTE: Don't forget to add a new release entry in the AppStream metadata! if(NOT ZEAL_RELEASE_BUILD AND DEFINED ENV{ZEAL_RELEASE_BUILD}) set(ZEAL_RELEASE_BUILD $ENV{ZEAL_RELEASE_BUILD}) endif() # Project information. set(PROJECT_COMPANY_NAME "Oleg Shparber") set(PROJECT_COPYRIGHT "© 2013-2026 Oleg Shparber and other contributors") # Find available major Qt version. It will be stored in QT_VERSION_MAJOR. if(NOT ZEAL_USE_QT5) find_package(QT NAMES Qt6 COMPONENTS Core) set(QT_MINIMUM_VERSION 6.2.0) endif() if(NOT QT_FOUND) find_package(QT NAMES Qt5 REQUIRED COMPONENTS Core) set(QT_MINIMUM_VERSION 5.15.2) endif() message(NOTICE "Detected Qt version: ${QT_VERSION}") # Determine version for dev builds. if(NOT ZEAL_RELEASE_BUILD) message(NOTICE "Building unreleased code. Proceed at your own risk!") # TODO: Add support for metadata passed from env, e.g. aur, appimage, etc. include(GetVersionFromGit) if(Zeal_GIT_VERSION_SHA) # Extra check in case we forgot to bump version in project() directive. if(NOT PROJECT_VERSION_PATCH EQUAL Zeal_GIT_VERSION_PATCH_NEXT) message(WARNING "Incorrect patch version! Forgot to bump?") endif() set(ZEAL_VERSION_SUFFIX "-dev.${Zeal_GIT_VERSION_AHEAD}+${Zeal_GIT_VERSION_SHA}") else() set(ZEAL_VERSION_SUFFIX "-dev") endif() endif() set(ZEAL_VERSION_FULL "${Zeal_VERSION}${ZEAL_VERSION_SUFFIX}") message(NOTICE "Calculated Zeal version: ${ZEAL_VERSION_FULL}") file(WRITE "${CMAKE_BINARY_DIR}/zeal_version" ${ZEAL_VERSION_FULL}) # A custom target to print the full version. # Usage: cmake --build build --preset ninja-multi-vcpkg-release --target zeal_version add_custom_target(zeal_version COMMAND ${CMAKE_COMMAND} -E echo "Zeal version: ${ZEAL_VERSION_FULL}" VERBATIM ) if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.24.0") set(CMAKE_COMPILE_WARNING_AS_ERROR ON) endif() option(BUILD_TESTING "Build the testing suite" ON) if(BUILD_TESTING) enable_testing() endif() add_subdirectory(assets) add_subdirectory(src) ================================================ FILE: CMakePresets.json ================================================ { "version": 3, "cmakeMinimumRequired": { "major": 3, "minor": 21, "patch": 0 }, "configurePresets": [ { "name": "ninja-multi", "generator": "Ninja Multi-Config", "binaryDir": "${sourceDir}/build/${presetName}", "warnings": { "deprecated": true, "dev": true, "uninitialized": true, "unusedCli": true } }, { "name": "ninja-multi-portable", "inherits": [ "ninja-multi", "config-portable" ] }, { "name": "ninja-multi-vcpkg", "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, "inherits": [ "ninja-multi" ], "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": { "type": "FILEPATH", "value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" }, "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON" } }, { "name": "ninja-multi-vcpkg-portable", "inherits": [ "ninja-multi-vcpkg", "config-portable" ] }, { "name": "config-portable", "hidden": true, "cacheVariables": { "ZEAL_PORTABLE_BUILD": { "type": "BOOL", "value": "ON" } } }, { "name": "config-release", "hidden": true, "cacheVariables": { "ZEAL_RELEASE_BUILD": { "type": "BOOL", "value": "ON" } } }, { "name": "config-testing", "hidden": true, "cacheVariables": { "BUILD_TESTING": { "type": "BOOL", "value": "ON" } } }, { "name": "ninja-multi-vcpkg-test", "inherits": [ "ninja-multi-vcpkg", "config-testing" ] } ], "buildPresets": [ { "name": "ninja-multi-debug", "configurePreset": "ninja-multi", "configuration": "Debug" }, { "name": "ninja-multi-release", "configurePreset": "ninja-multi", "configuration": "RelWithDebInfo" }, { "name": "ninja-multi-debug-portable", "configurePreset": "ninja-multi-portable", "configuration": "Debug" }, { "name": "ninja-multi-portable-release", "configurePreset": "ninja-multi-portable", "configuration": "RelWithDebInfo" }, { "name": "ninja-multi-vcpkg-debug", "configurePreset": "ninja-multi-vcpkg", "configuration": "Debug" }, { "name": "ninja-multi-vcpkg-release", "configurePreset": "ninja-multi-vcpkg", "configuration": "RelWithDebInfo" }, { "name": "ninja-multi-vcpkg-portable-debug", "configurePreset": "ninja-multi-vcpkg-portable", "configuration": "Debug" }, { "name": "ninja-multi-vcpkg-portable-release", "configurePreset": "ninja-multi-vcpkg-portable", "configuration": "RelWithDebInfo" } ], "testPresets": [ { "name": "ninja-multi-vcpkg-test-debug", "configurePreset": "ninja-multi-vcpkg-test", "configuration": "Debug", "output": { "outputOnFailure": true } } ] } ================================================ FILE: COPYING ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The 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. When 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. To 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. For 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. Developers 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. For 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. Some 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. Finally, 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. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "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. To "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. A "covered work" means either the unmodified Program or a work based on the Program. To "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. To "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. An 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. 1. Source Code. The "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. A "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. The "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. The "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. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All 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. You 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. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No 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. When 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. 4. Conveying Verbatim Copies. You 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. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You 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: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. 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". 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. 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. A 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. 6. Conveying Non-Source Forms. You 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: 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. 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. 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. 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. 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. A 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. A "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. "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. If 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). The 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. Corresponding 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. 7. Additional Terms. "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. When 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. Notwithstanding 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: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or 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 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 d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or 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. All 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. If 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. Additional 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. 8. Termination. You 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). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination 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. 9. Acceptance Not Required for Having Copies. You 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. 10. Automatic Licensing of Downstream Recipients. Each 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. An "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. You 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. 11. Patents. A "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". A 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. Each 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. In 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. If 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. If, 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. A 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. Nothing 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. 12. No Surrender of Others' Freedom. If 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. 13. Use with the GNU Affero General Public License. Notwithstanding 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. 14. Revised Versions of this License. The 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. Each 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. If 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. Later 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. 15. Disclaimer of Warranty. THERE 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. 16. Limitation of Liability. IN 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. 17. Interpretation of Sections 15 and 16. If 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. ================================================ FILE: LICENSES/GPL-3.0-or-later.txt ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright © 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The 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. When 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. To 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. For 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. Developers 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. For 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. Some 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. Finally, 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. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. “This License” refers to version 3 of the GNU General Public License. “Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. “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. To “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. A “covered work” means either the unmodified Program or a work based on the Program. To “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. To “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. An 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. 1. Source Code. The “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. A “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. The “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. The “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. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All 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. You 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. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No 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. When 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. 4. Conveying Verbatim Copies. You 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. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You 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: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. 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”. 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. 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. A 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. 6. Conveying Non-Source Forms. You 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: 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. 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. 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. 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. 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. A 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. A “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. “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. If 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). The 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. Corresponding 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. 7. Additional Terms. “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. When 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. Notwithstanding 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: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or 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 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 d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or 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. All 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. If 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. Additional 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. 8. Termination. You 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). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination 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. 9. Acceptance Not Required for Having Copies. You 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. 10. Automatic Licensing of Downstream Recipients. Each 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. An “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. You 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. 11. Patents. A “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”. A 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. Each 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. In 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. If 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. If, 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. A 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. Nothing 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. 12. No Surrender of Others' Freedom. If 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. 13. Use with the GNU Affero General Public License. Notwithstanding 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. 14. Revised Versions of this License. The 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. Each 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. If 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. Later 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. 15. Disclaimer of Warranty. THERE 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. 16. Limitation of Liability. IN 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. 17. Interpretation of Sections 15 and 16. If 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. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If 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. To 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. Copyright (C) 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. 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. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The 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”. You 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 . The 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 . ================================================ FILE: LICENSES/LicenseRef-Kapeli.txt ================================================ These files are used under permission from Bogdan Popescu (https://github.com/Kapeli). See https://github.com/Kapeli/Dash-X-Platform-Resources for more details. ================================================ FILE: LICENSES/MIT.txt ================================================ MIT License Copyright (c) Oleg Shparber, et al. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: LICENSES/MS-RL.txt ================================================ Microsoft Reciprocal License (Ms-RL) This 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. 1. Definitions The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law. A "contribution" is the original software, or any additions or changes to the software. A "contributor" is any person that distributes its contribution under this license. "Licensed patents" are a contributor's patent claims that read directly on its contribution. 2. Grant of Rights (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. (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. 3. Conditions and Limitations (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. (B) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. (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. (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. (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. (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. ================================================ FILE: README.md ================================================ # Zeal [![Changelog](https://img.shields.io/github/release/zealdocs/zeal.svg?style=flat-square)](https://github.com/zealdocs/zeal/releases) [![Gitter](https://img.shields.io/gitter/room/zealdocs/zeal.svg?style=flat-square)](https://gitter.im/zealdocs/zeal) [![IRC](https://img.shields.io/badge/chat-on%20irc-blue.svg?style=flat-square)](https://web.libera.chat/#zealdocs) [![Telegram Channel](https://img.shields.io/badge/follow-on%20telegram-179cde.svg?style=flat-square)](https://telegram.me/zealdocsapp) [![Twitter](https://img.shields.io/badge/follow-on%20twitter-1da1f2.svg?style=flat-square)](https://twitter.com/zealdocs) [![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) [![Coverity Scan](https://img.shields.io/coverity/scan/4271.svg?style=flat-square)](https://scan.coverity.com/projects/4271) Zeal is a simple offline documentation browser inspired by [Dash](https://kapeli.com/dash). ![Screenshot](https://github.com/zealdocs/zeal/assets/714940/e8443bb4-ccb9-469b-89d6-b5b3bfc7e239) ## Download Get binary builds for Windows and Linux from the [download page](https://zealdocs.org/download.html). ## How to use After installing Zeal go to `Tools->Docsets`, select the ones you want, and click the `Download` button. ## How to compile ### Build dependencies * [CMake](https://cmake.org/). * [Qt](https://www.qt.io/) version 5.15.2 or above. Required module: Qt WebEngine Widgets. * [libarchive](https://libarchive.org/). * [SQLite](https://sqlite.org/). * X11 platforms only: Qt X11 Extras and `xcb-util-keysyms`. ### Build instructions ```sh cmake -B build cmake --build build ``` More detailed instructions are available in the [wiki](https://github.com/zealdocs/zeal/wiki). ## Query & Filter docsets You can limit the search scope by using ':' to indicate the desired docsets: `java:BaseDAO` You can also search multiple docsets separating them with a comma: `python,django:string` ## Command line If you prefer, you can start Zeal with a query from the command line: `zeal python:pprint` ## Create your own docsets Follow instructions in the [Dash docset generation guide](https://kapeli.com/docsets). ## Contact and Support We want your feedback! Here's a list of different ways to contact developers and request help: * Report bugs and submit feature requests to [GitHub issues](https://github.com/zealdocs/zeal/issues). * Reach developers and other Zeal users in `#zealdocs` IRC channel on [Libera Chat](https://libera.chat) ([web client](https://web.libera.chat/#zealdocs)). * Ask any questions in our [GitHub discussions](https://github.com/zealdocs/zeal/discussions). * Do not forget to follow [@zealdocs](https://twitter.com/zealdocs) on Twitter! * Finally, for private communication shoot an email to . ## License This 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). ================================================ FILE: REUSE.toml ================================================ version = 1 # GPL-3.0-or-later for the main source code and assets. [[annotations]] path = [ "assets/**/*", "cmake/*", "pkg/**/*", "README.md", "src/**/*.ui", "src/app/resources/browser/*.html", "src/app/resources/browser/assets/css/highlight.css", "src/app/resources/browser/assets/css/welcome.css", "src/app/resources/icons/**/*", "src/app/resources/zeal.*", "src/app/zeal.qrc", # Windows CMake files. # CMake and vcpkg files. "**/*.cmake", "**/CMakeLists.txt", "CMakePresets.json", "vcpkg.json", ] SPDX-FileCopyrightText = "Oleg Shparber, et al. " SPDX-License-Identifier = "GPL-3.0-or-later" # MIT for auxiliary files. [[annotations]] path = [ ".editorconfig", ".gitattributes", ".github/**/*", ".gitignore", "CHANGELOG.md", ] SPDX-FileCopyrightText = "Oleg Shparber, et al. " SPDX-License-Identifier = "MIT" [[annotations]] path = "src/app/resources/browser/assets/css/oat.min.css" SPDX-FileCopyrightText = "Copyright (c) Kailash Nadh " SPDX-License-Identifier = "MIT" [[annotations]] path = "src/contrib/cpp-httplib/httplib.h" SPDX-FileCopyrightText = "Copyright (c) 2023 Yuji Hirose. All rights reserved." SPDX-License-Identifier = "MIT" [[annotations]] path = "src/app/resources/icons/type/*.png" SPDX-FileCopyrightText = "Bogdan Popescu, et al. " SPDX-License-Identifier = "LicenseRef-Kapeli" ================================================ FILE: assets/CMakeLists.txt ================================================ add_subdirectory(freedesktop) ================================================ FILE: assets/freedesktop/CMakeLists.txt ================================================ if(UNIX AND NOT APPLE) find_package(ECM 1.0.0 REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(ECMInstallIcons) if(QT_VERSION_MAJOR EQUAL 5) include(KDEInstallDirs) else() # Workaround until KDEInstallDirs6 is ready to use. include(GNUInstallDirs) set(KDE_INSTALL_APPDIR "${CMAKE_INSTALL_DATAROOTDIR}/applications") set(KDE_INSTALL_ICONDIR "${CMAKE_INSTALL_DATAROOTDIR}/icons") set(KDE_INSTALL_METAINFODIR "${CMAKE_INSTALL_DATAROOTDIR}/metainfo") endif() ecm_install_icons(ICONS "16-apps-zeal.png" "24-apps-zeal.png" "32-apps-zeal.png" "64-apps-zeal.png" "128-apps-zeal.png" DESTINATION ${KDE_INSTALL_ICONDIR} ) # For development builds insert an extra release in the AppStream metadata. if(NOT ZEAL_RELEASE_BUILD) string(TIMESTAMP ZEAL_APPSTREAM_DEV_RELEASE "\n ") endif() configure_file( org.zealdocs.zeal.appdata.xml.in org.zealdocs.zeal.appdata.xml ) install(FILES ${CMAKE_BINARY_DIR}/assets/freedesktop/org.zealdocs.zeal.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) install(FILES "org.zealdocs.zeal.desktop" DESTINATION ${KDE_INSTALL_APPDIR} ) endif() ================================================ FILE: assets/freedesktop/org.zealdocs.zeal.appdata.xml.in ================================================ org.zealdocs.zeal org.zealdocs.zeal.desktop Zeal CC0-1.0 GPL-3.0-or-later Oleg Shparber Documentation browser

Zeal is a simple offline documentation browser inspired by Dash. It offers access to over 200 docsets covering various libraries and APIs.

Development https://zealdocs.org/ https://github.com/zealdocs/zeal/issues https://zealdocs.org/usage.html https://go.zealdocs.org/l/contact The main window https://i.imgur.com/FvGEguY.png zeal.desktop @ZEAL_APPSTREAM_DEV_RELEASE@ https://github.com/zealdocs/zeal/releases/tag/v0.8.0 https://github.com/zealdocs/zeal/releases/tag/v0.7.2 https://github.com/zealdocs/zeal/releases/tag/v0.7.1 https://github.com/zealdocs/zeal/releases/tag/v0.7.0 https://github.com/zealdocs/zeal/releases/tag/v0.6.1 support@zealdocs.org
================================================ FILE: assets/freedesktop/org.zealdocs.zeal.desktop ================================================ [Desktop Entry] Version=1.0 Name=Zeal GenericName=Documentation Browser Comment=Simple API documentation browser Exec=zeal %u Icon=zeal Terminal=false Type=Application Categories=Development;Documentation; MimeType=x-scheme-handler/dash;x-scheme-handler/dash-plugin; StartupWMClass=Zeal ================================================ FILE: cmake/CodeSign.cmake ================================================ # # CodeSign.cmake - CMake helper for signing Windows executables # # SPDX-FileCopyrightText: Oleg Shparber, et al. # SPDX-License-Identifier: MIT # include_guard() # codesign(FILES ... # [DESCRIPTION] # [URL] # [CERTIFICATE_FILE] # [PASSWORD] # [TIMESTAMP_URL] # [QUIET] # [VERBOSE] # [DEBUG]) function(codesign) # Cleans up temporary files created during signing. macro(_cleanup) if(DEFINED _certificate_file) file(REMOVE ${_certificate_file}) endif() endmacro() # Sets '_certificate_file' variable to a temporary file path. macro(_set_temporary_certificate_file) # Determine temporary file location. Try to keep it local to the build. if(CMAKE_BINARY_DIR) set(_temp_path ${CMAKE_BINARY_DIR}) elseif(CPACK_TEMPORARY_DIRECTORY) set(_temp_path ${CPACK_TEMPORARY_DIRECTORY}) else() set(_temp_path $ENV{TEMP}) endif() set(_certificate_file "${_temp_path}/codesign.tmp") # Remove file if left from previous run. _cleanup() endmacro() if(NOT WIN32) message(FATAL_ERROR "Code signing is only supported on Windows.") endif() cmake_parse_arguments(_ARG "QUIET;VERBOSE;DEBUG" # Options. "DESCRIPTION;URL;CERTIFICATE_FILE;PASSWORD;TIMESTAMP_URL" # Single-value keywords. "FILES" # Multi-value keywords. ${ARGN} ) if(NOT _ARG_FILES) message(FATAL_ERROR "FILES argument is required.") endif() # Find signtool executable. # TODO: Add option for path to signtool.exe. # Add Windows 10 SDK paths. get_filename_component(_w10sdk_root_path "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots;KitsRoot10]" ABSOLUTE CACHE ) if(_w10sdk_root_path) file(GLOB _w10sdk_paths "${_w10sdk_root_path}/bin/10.*") list(REVERSE _w10sdk_paths) # Newest version first. # Detect target architecture. # https://learn.microsoft.com/en-us/windows/win32/winprog64/wow64-implementation-details#environment-variables if(CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64") set(_w10sdk_arch "x64") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "X86") set(_w10sdk_arch "x86") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64") set(_w10sdk_arch "arm64") else() message(WARNING "Unknown architecture: ${CMAKE_SYSTEM_PROCESSOR}.") endif() endif() # TODO: Add microsoft.windows.sdk.buildtools path. find_program(_cmd NAMES signtool PATHS ${_w10sdk_paths} PATH_SUFFIXES ${_w10sdk_arch} ) if(NOT _cmd) message(NOTICE "signtool.exe was not found, no binaries will be signed.") return() endif() message(DEBUG "Found signtool.exe: ${_cmd}") # Start constructing command. set(_cmd_args "sign") list(APPEND _cmd_args "/fd" "sha256") # Set certificate file. if(NOT _ARG_CERTIFICATE_FILE) if(CODESIGN_CERTIFICATE_FILE) if(NOT EXISTS ${CODESIGN_CERTIFICATE_FILE}) message(NOTICE "Certificate file '${CODESIGN_CERTIFICATE_FILE}' does not exist.") return() endif() set(_ARG_CERTIFICATE_FILE ${CODESIGN_CERTIFICATE_FILE}) elseif(DEFINED ENV{CODESIGN_CERTIFICATE_FILE}) if("$ENV{CODESIGN_CERTIFICATE_FILE}" STREQUAL "") message(NOTICE "CODESIGN_CERTIFICATE_FILE is set to an empty string.") return() endif() if(NOT EXISTS $ENV{CODESIGN_CERTIFICATE_FILE}) message(NOTICE "Certificate file '$ENV{CODESIGN_CERTIFICATE_FILE}' (set in CODESIGN_CERTIFICATE_FILE) does not exist.") return() endif() set(_ARG_CERTIFICATE_FILE $ENV{CODESIGN_CERTIFICATE_FILE}) elseif(DEFINED ENV{CODESIGN_CERTIFICATE}) if("$ENV{CODESIGN_CERTIFICATE}" STREQUAL "") message(NOTICE "CODESIGN_CERTIFICATE is set to an empty string.") return() endif() # Store certificate value in a temporary file for signtool to use. _set_temporary_certificate_file() file(WRITE ${_certificate_file} $ENV{CODESIGN_CERTIFICATE}) set(_ARG_CERTIFICATE_FILE ${_certificate_file}) elseif(DEFINED ENV{CODESIGN_CERTIFICATE_BASE64}) if("$ENV{CODESIGN_CERTIFICATE_BASE64}" STREQUAL "") message(NOTICE "CODESIGN_CERTIFICATE_BASE64 is set to an empty string.") return() endif() # Read base64-encoded certificate from environment variable, # decode with `certutil.exe`, and store in a temporary file # for signtool to use. # # This is useful for GitHub Actions, which cannot handle unencoded # multiline secrets. _set_temporary_certificate_file() # Save base64-encoded certificate to file. set(_certificate_base64_file "${_certificate_file}.base64") file(WRITE ${_certificate_base64_file} $ENV{CODESIGN_CERTIFICATE_BASE64}) # Decode certificate. set(_cmd_certutil_args "-decode" ${_certificate_base64_file} ${_certificate_file}) execute_process(COMMAND "certutil.exe" ${_cmd_certutil_args} RESULT_VARIABLE _rc OUTPUT_VARIABLE _stdout # For some reason certutil prints errors to stdout. # ERROR_VARIABLE _stderr ) # Remove temporary file first. file(REMOVE ${_certificate_base64_file}) if(NOT _rc EQUAL 0) message(WARNING "Failed to decode certificate: ${_stdout}") _cleanup() return() endif() unset(_rc) unset(_stdout) set(_ARG_CERTIFICATE_FILE ${_certificate_file}) else() message(NOTICE "Certificate is not provided, no binaries will be signed.") return() endif() endif() list(APPEND _cmd_args "/f" ${_ARG_CERTIFICATE_FILE}) # Set password. if(NOT _ARG_PASSWORD) if(CODESIGN_PASSWORD) set(_ARG_PASSWORD ${CODESIGN_PASSWORD}) elseif(DEFINED ENV{CODESIGN_PASSWORD}) if("$ENV{CODESIGN_PASSWORD}" STREQUAL "") message(NOTICE "CODESIGN_PASSWORD is set to an empty string. Unset if not used.") _cleanup() return() endif() set(_ARG_PASSWORD $ENV{CODESIGN_PASSWORD}) endif() endif() if(_ARG_PASSWORD) list(APPEND _cmd_args "/p" ${_ARG_PASSWORD}) endif() # Set description. if(NOT _ARG_DESCRIPTION AND PROJECT_DESCRIPTION) set(_ARG_DESCRIPTION ${PROJECT_DESCRIPTION}) endif() if(_ARG_DESCRIPTION) list(APPEND _cmd_args "/d" ${_ARG_DESCRIPTION}) endif() # Set project URL. if(NOT _ARG_URL AND PROJECT_HOMEPAGE_URL) set(_ARG_URL ${PROJECT_HOMEPAGE_URL}) endif() if(_ARG_URL) list(APPEND _cmd_args "/du" ${_ARG_URL}) endif() # Set timestamp server. if(NOT _ARG_TIMESTAMP_URL) set(_ARG_TIMESTAMP_URL "http://timestamp.digicert.com") endif() if(_ARG_TIMESTAMP_URL) list(APPEND _cmd_args "/tr" ${_ARG_TIMESTAMP_URL} "/td" "sha256") endif() # Set quiet, verbose, or debug options. if(_ARG_QUIET) list(APPEND _cmd_args "/q") endif() if(_ARG_VERBOSE) list(APPEND _cmd_args "/v") endif() if(_ARG_DEBUG) list(APPEND _cmd_args "/debug") endif() foreach(_file ${_ARG_FILES}) if(NOT EXISTS ${_file}) message(NOTICE "Cannot find file to sign: ${_file}") continue() endif() message(STATUS "Signing ${_file}...") execute_process( COMMAND "${_cmd}" ${_cmd_args} "${_file}" RESULT_VARIABLE _rc OUTPUT_VARIABLE _stdout ERROR_VARIABLE _stderr ) if(_rc EQUAL 0) message(STATUS "Successfully signed: ${_file}") else() message(NOTICE "Failed to sign: ${_stderr}") if(NOT _ARG_QUIET) message(VERBOSE ${_stdout}) endif() endif() endforeach() _cleanup() endfunction() ================================================ FILE: cmake/GetVersionFromGit.cmake ================================================ # # GetVersionFromGit.cmake - CMake helper for getting version information from Git # # SPDX-FileCopyrightText: Oleg Shparber, et al. # SPDX-License-Identifier: MIT # # Based on https://github.com/fakenmc/cmake-git-semver by Nuno Fachada. # This module is public domain, use it as it fits you best. # # This cmake module sets the project version and partial version # variables by analysing the git tag and commit history. It expects git # tags defined with semantic versioning 2.0.0 (http://semver.org/). # # The module expects the PROJECT_NAME variable to be set, and recognizes # the GIT_FOUND, GIT_EXECUTABLE and VERSION_UPDATE_FROM_GIT variables. # If Git is found and VERSION_UPDATE_FROM_GIT is set to boolean TRUE, # the project version will be updated using information fetched from the # most recent git tag and commit. Otherwise, the module will try to read # a VERSION file containing the full and partial versions. The module # will update this file each time the project version is updated. # # Once done, this module will define the following variables: # # ${PROJECT_NAME}_GIT_VERSION_STRING - Version string without metadata # such as "v2.0.0" or "v1.2.41-beta.1". This should correspond to the # most recent git tag. # ${PROJECT_NAME}_GIT_VERSION_STRING_FULL - Version string with metadata # such as "v2.0.0+3.a23fbc" or "v1.3.1-alpha.2+4.9c4fd1" # ${PROJECT_NAME}_GIT_VERSION_MAJOR - Major version integer (e.g. 2 in v2.3.1-RC.2+21.ef12c8) # ${PROJECT_NAME}_GIT_VERSION_MINOR - Minor version integer (e.g. 3 in v2.3.1-RC.2+21.ef12c8) # ${PROJECT_NAME}_GIT_VERSION_PATCH - Patch version integer (e.g. 1 in v2.3.1-RC.2+21.ef12c8) # ${PROJECT_NAME}_GIT_VERSION_TWEAK - Tweak version string (e.g. "RC.2" in v2.3.1-RC.2+21.ef12c8) # ${PROJECT_NAME}_GIT_VERSION_AHEAD - How many commits ahead of last tag (e.g. 21 in v2.3.1-RC.2+21.ef12c8) # ${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) # Only if VERSION_UPDATE_FROM_GIT is TRUE: # ${PROJECT_NAME}_VERSION - Same as ${PROJECT_NAME}_GIT_VERSION_STRING, # without the preceding 'v', e.g. "2.0.0" or "1.2.41-beta.1" # Check if .git directory is present. if(NOT IS_DIRECTORY "${CMAKE_SOURCE_DIR}/.git") message(NOTICE "Cannot find Git metadata, using static version string.") return() endif() # Check if Git executable is present. find_package(Git) if(NOT GIT_FOUND) message(NOTICE "Cannot find Git executable, using static version string.") return() endif() # Check if Git executable version is >= 2.15. Required for --is-shallow-repository argument. # See https://stackoverflow.com/a/37533086. if(GIT_VERSION_STRING VERSION_LESS "2.15") message(NOTICE "Git executable is too old (< 2.15), using static version string.") return() endif() # Detect shallow clone. execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --is-shallow-repository WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} RESULT_VARIABLE IS_SHALLOW_RESULT OUTPUT_VARIABLE IS_SHALLOW_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) if(IS_SHALLOW_RESULT AND NOT IS_SHALLOW_RESULT EQUAL 0) message(NOTICE "Cannot perform shallow clone detection, using static version string.") unset(IS_SHALLOW_RESULT) unset(IS_SHALLOW_OUTPUT) return() endif() unset(IS_SHALLOW_RESULT) if(NOT "${IS_SHALLOW_OUTPUT}" STREQUAL "false") message(NOTICE "Shallow clone detected, using static version string.") unset(IS_SHALLOW_OUTPUT) return() endif() unset(IS_SHALLOW_OUTPUT) # Get last tag from git execute_process(COMMAND ${GIT_EXECUTABLE} describe --abbrev=0 --tags WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE ${PROJECT_NAME}_GIT_VERSION_STRING OUTPUT_STRIP_TRAILING_WHITESPACE) # How many commits since the last tag execute_process(COMMAND ${GIT_EXECUTABLE} rev-list ${${PROJECT_NAME}_GIT_VERSION_STRING}^..HEAD --count WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE ${PROJECT_NAME}_GIT_VERSION_AHEAD OUTPUT_STRIP_TRAILING_WHITESPACE) # Get current commit SHA from git execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE ${PROJECT_NAME}_GIT_VERSION_SHA OUTPUT_STRIP_TRAILING_WHITESPACE) # Get partial versions into a list string(REGEX MATCHALL "-.*$|[0-9]+" ${PROJECT_NAME}_PARTIAL_VERSION_LIST ${${PROJECT_NAME}_GIT_VERSION_STRING}) # Set the version numbers list(GET ${PROJECT_NAME}_PARTIAL_VERSION_LIST 0 ${PROJECT_NAME}_GIT_VERSION_MAJOR) list(GET ${PROJECT_NAME}_PARTIAL_VERSION_LIST 1 ${PROJECT_NAME}_GIT_VERSION_MINOR) list(GET ${PROJECT_NAME}_PARTIAL_VERSION_LIST 2 ${PROJECT_NAME}_GIT_VERSION_PATCH) # Calculate next patch version. math(EXPR ${PROJECT_NAME}_GIT_VERSION_PATCH_NEXT ${${PROJECT_NAME}_GIT_VERSION_PATCH}+1) # The tweak part is optional, so check if the list contains it list(LENGTH ${PROJECT_NAME}_PARTIAL_VERSION_LIST ${PROJECT_NAME}_PARTIAL_VERSION_LIST_LEN) if (${PROJECT_NAME}_PARTIAL_VERSION_LIST_LEN GREATER 3) list(GET ${PROJECT_NAME}_PARTIAL_VERSION_LIST 3 ${PROJECT_NAME}_GIT_VERSION_TWEAK) string(SUBSTRING ${${PROJECT_NAME}_GIT_VERSION_TWEAK} 1 -1 ${PROJECT_NAME}_GIT_VERSION_TWEAK) endif() # Unset the list unset(${PROJECT_NAME}_PARTIAL_VERSION_LIST) # Set full project version string set(${PROJECT_NAME}_GIT_VERSION_STRING_FULL ${${PROJECT_NAME}_GIT_VERSION_STRING}+${${PROJECT_NAME}_GIT_VERSION_AHEAD}.${${PROJECT_NAME}_GIT_VERSION_SHA}) if(VERSION_UPDATE_FROM_GIT) # Set project version (without the preceding 'v') set(${PROJECT_NAME}_VERSION ${${PROJECT_NAME}_GIT_VERSION_MAJOR}.${${PROJECT_NAME}_GIT_VERSION_MINOR}.${${PROJECT_NAME}_GIT_VERSION_PATCH}) if (${PROJECT_NAME}_GIT_VERSION_TWEAK) set(${PROJECT_NAME}_VERSION ${${PROJECT_NAME}_VERSION}-${${PROJECT_NAME}_GIT_VERSION_TWEAK}) endif() endif() ================================================ FILE: cmake/MacOSXBundleInfo.plist.in ================================================ CFBundleDevelopmentRegion English CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} CFBundleIconFile ${MACOSX_BUNDLE_ICON_FILE} CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleLongVersionString ${MACOSX_BUNDLE_LONG_VERSION_STRING} CFBundleName ${MACOSX_BUNDLE_BUNDLE_NAME} CFBundlePackageType APPL CFBundleShortVersionString ${MACOSX_BUNDLE_SHORT_VERSION_STRING} CFBundleSignature ???? CFBundleVersion ${MACOSX_BUNDLE_BUNDLE_VERSION} CSResourcesFileMapped NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} NSPrincipalClass NSApplication NSHighResolutionCapable NSSupportsAutomaticGraphicsSwitching ================================================ FILE: pkg/appimage/README.md ================================================ # AppImage Package ## Local Testing Run Docker container: ```shell docker run -it --rm -v $(pwd):/src --entrypoint /bin/bash ubuntu:jammy ``` Install `appimage-builder` and `appimagetool` dependencies: ```shell apt-get update -q -y DEBIAN_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 ``` Install appimagetool, it has to be extracted because FUSE doesn't work in containers without extra fiddling. ```shell cd /tmp curl -sLO https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage chmod +x appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage --appimage-extract mv squashfs-root/ /opt/appimagetool.AppDir ln -s /opt/appimagetool.AppDir/AppRun /usr/local/bin/appimagetool cd - ``` Install appimage-builder. ```shell pip3 install git+https://github.com/AppImageCrafters/appimage-builder.git@669213cb730e007d5b316ed19b39691fbdcd41c4 ``` Install build dependencies: ```shell apt-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 ``` Run `appimage-builder`: ```shell cd /src appimage-builder --skip-test --build-dir build.appimage --appdir build.appimage/AppDir --recipe pkg/appimage/appimage-amd64.yaml ``` ================================================ FILE: pkg/appimage/appimage-amd64.yaml ================================================ version: 1 script: - cmake -B $BUILD_DIR/cmake-build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo - cmake --build $BUILD_DIR/cmake-build - cmake --install $BUILD_DIR/cmake-build --prefix $TARGET_APPDIR/usr - appstreamcli validate $TARGET_APPDIR/usr/share/metainfo/org.zealdocs.zeal.appdata.xml AppDir: app_info: id: org.zealdocs.zeal name: zeal icon: zeal version: 0.8.0 # TODO: Use version from CMake. exec: usr/bin/zeal exec_args: $@ runtime: env: APPDIR_LIBRARY_PATH: $APPDIR/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu QTWEBENGINE_DISABLE_SANDBOX: 1 apt: arch: amd64 sources: - sourceline: deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted universe multiverse key_url: https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920d1991bc93c - sourceline: deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted universe multiverse - sourceline: deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security main restricted universe multiverse - sourceline: deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse include: # Required Qt packages. - libqt5concurrent5 - libqt5gui5 - libqt5network5 - libqt5webchannel5 - libqt5webengine5 - libqt5webenginewidgets5 - libqt5widgets5 - libqt5x11extras5 - qt5-gtk-platformtheme - qtwayland5 # Other dependencies. - libsqlite3-0 - libarchive13 - libfontconfig1 - libfreetype6 exclude: - "*dbgsym*" - adwaita-icon-theme - dconf-service - gcc-* - gnupg - humanity-icon-theme - libsystemd0 - libwacom* - perl - perl-* - sound-theme-freedesktop - systemd - systemd-* files: exclude: - etc/systemd - lib/systemd - usr/bin/*-linux-gnu-* - usr/bin/dpkg* - usr/bin/systemd* - usr/include - usr/lib/x86_64-linux-gnu/gconv - usr/share/doc - usr/share/man AppImage: arch: x86_64 comp: zstd sign-key: None update-information: gh-releases-zsync|zealdocs|zeal|latest|zeal-*x86_64.AppImage.zsync ================================================ FILE: pkg/wix/cpack_post_build.cmake ================================================ if(CPACK_SOURCE_INSTALLED_DIRECTORIES) message(DEBUG "Skipping package signing for source package generator.") return() endif() if(NOT CPACK_GENERATOR STREQUAL "WIX") message(DEBUG "Skipping package signing for ${CPACK_GENERATOR} generator.") return() endif() include(CodeSign) codesign(FILES ${CPACK_PACKAGE_FILES} QUIET) ================================================ FILE: pkg/wix/cpack_pre_build.cmake ================================================ if(CPACK_SOURCE_INSTALLED_DIRECTORIES) message(DEBUG "Skipping package signing for source package generator.") return() endif() # TODO: Automatically generate list. set(_file_list "zeal.exe" "archive.dll" "zlib1.dll" "sqlite3.dll" ) include(CodeSign) foreach(_file ${_file_list}) codesign(FILES "${CPACK_TEMPORARY_DIRECTORY}/${_file}" QUIET) endforeach() ================================================ FILE: pkg/wix/exitdialog.wxs ================================================ ================================================ FILE: pkg/wix/patch.xml ================================================ ================================================ FILE: pkg/wix/template.xml ================================================ ProductIcon.ico LAUNCHAPPONEXIT WIX_UPGRADE_DETECTED ================================================ FILE: pkg/wix/ui.wxs ================================================ 1 "1"]]> 1 NOT Installed Installed AND PATCH 1 LicenseAccepted = "1" 1 1 NOT WIXUI_DONTVALIDATEPATH "1"]]> WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1" 1 1 NOT Installed Installed AND NOT PATCH Installed AND PATCH 1 1 1 1 ================================================ FILE: src/CMakeLists.txt ================================================ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Find includes in corresponding build directories. set(CMAKE_INCLUDE_CURRENT_DIR ON) ## Build options option(ZEAL_PORTABLE_BUILD "Build portable version") if(ZEAL_PORTABLE_BUILD) add_definitions(-DPORTABLE_BUILD) endif() ## Macros. add_definitions(-DZEAL_VERSION="${ZEAL_VERSION_FULL}") # QString options add_definitions(-DQT_USE_QSTRINGBUILDER) add_definitions(-DQT_RESTRICTED_CAST_FROM_ASCII) add_definitions(-DQT_NO_CAST_TO_ASCII) add_definitions(-DQT_NO_URL_CAST_FROM_STRING) ## Handle moc, uic, and rcc files. set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) include_directories(libs) add_subdirectory(libs) add_subdirectory(app) ================================================ FILE: src/app/CMakeLists.txt ================================================ find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Concurrent WebEngineWidgets Widgets REQUIRED) if (Qt${QT_VERSION_MAJOR}Widgets_VERSION VERSION_LESS QT_MINIMUM_VERSION) message(FATAL_ERROR "Qt version >= ${QT_MINIMUM_VERSION} is required.") endif() # Define output binary name. if(APPLE) set(_project_output_name ${CMAKE_PROJECT_NAME}) else() string(TOLOWER ${CMAKE_PROJECT_NAME} _project_output_name) endif() set(PROJECT_EXECUTABLE_NAME "${_project_output_name}${CMAKE_EXECUTABLE_SUFFIX}") message(STATUS "Project executable name: ${PROJECT_EXECUTABLE_NAME}") # Only support installing runtime dependencies with Qt >=6.5.1 (see QTBUG-111741). if(Qt${QT_VERSION_MAJOR}Widgets_VERSION VERSION_GREATER_EQUAL "6.5.1") set(_use_qt_cmake_commands TRUE) qt_standard_project_setup() endif() if(APPLE) list(APPEND App_RESOURCES resources/zeal.icns) elseif(WIN32) configure_file(versioninfo.rc.in ${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc) list(APPEND App_RESOURCES ${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc) else() set(App_RESOURCES) # Silence CMake warning. endif() if(QT_VERSION_MAJOR EQUAL 6) qt_add_executable(App WIN32 main.cpp zeal.qrc ${App_RESOURCES} ) else() add_executable(App WIN32 main.cpp zeal.qrc ${App_RESOURCES} ) endif() target_link_libraries(App PRIVATE Core Util Qt${QT_VERSION_MAJOR}::Widgets) set_target_properties(App PROPERTIES OUTPUT_NAME ${_project_output_name} RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} ) # Install Qt runtime dependencies on Windows. if(WIN32 AND _use_qt_cmake_commands) qt_generate_deploy_script( TARGET App OUTPUT_SCRIPT _qt_deploy_script CONTENT " # TODO: Run windeployqt after build. # Override deployment script's working directory. # set(QT_DEPLOY_PREFIX \"$\") qt_deploy_runtime_dependencies( EXECUTABLE \"$\" BIN_DIR . NO_TRANSLATIONS NO_COMPILER_RUNTIME )") endif() if(APPLE) set_target_properties(App PROPERTIES MACOSX_BUNDLE TRUE MACOSX_BUNDLE_BUNDLE_NAME ${CMAKE_PROJECT_NAME} MACOSX_BUNDLE_GUI_IDENTIFIER "org.zealdocs.zeal" MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} MACOSX_BUNDLE_LONG_VERSION_STRING ${PROJECT_VERSION} MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}" MACOSX_BUNDLE_ICON_FILE "zeal.icns" MACOSX_BUNDLE_COPYRIGHT ${PROJECT_COPYRIGHT} RESOURCE "resources/zeal.icns" ) elseif(WIN32) install(TARGETS App RUNTIME DESTINATION .) if(_use_qt_cmake_commands) # Install Qt runtime dependencies. install(SCRIPT ${_qt_deploy_script}) unset(_qt_deploy_script) unset(_use_qt_cmake_commands) endif() elseif(UNIX) include(GNUInstallDirs) install(TARGETS App DESTINATION ${CMAKE_INSTALL_BINDIR}) endif() # # CPack configuration. # set(CPACK_GENERATOR "7Z;ZIP") set(CPACK_VERBATIM_VARIABLES YES) # Usage: cmake --build --preset --target package # E.g. cmake --build build --preset ninja-multi-vcpkg-release --target package set(CPACK_PACKAGE_NAME ${CMAKE_PROJECT_NAME}) set(CPACK_PACKAGE_DESCRIPTION ${CMAKE_PROJECT_DESCRIPTION}) set(CPACK_PACKAGE_VENDOR ${PROJECT_COMPANY_NAME}) set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/src/app/resources/zeal.ico") # Set binary package file name. if(WIN32) if(CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64") set(_package_file_name_suffix "-windows-x64") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "X86") set(_package_file_name_suffix "-windows-x86") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64") set(_package_file_name_suffix "-windows-arm64") else() set(_package_file_name_suffix "-windows-unknown") endif() else() set(_package_file_name_suffix "") endif() if(ZEAL_PORTABLE_BUILD) string(PREPEND _package_file_name_suffix "-portable") endif() set(CPACK_PACKAGE_FILE_NAME "${_project_output_name}-${ZEAL_VERSION_FULL}${_package_file_name_suffix}") set(CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME}) set(CPACK_PACKAGE_EXECUTABLES ${_project_output_name} ${CPACK_PACKAGE_NAME}) set(CPACK_CREATE_DESKTOP_LINKS ${_project_output_name} ${CPACK_PACKAGE_NAME}) # Allow CPack to do text to RTF conversion. configure_file("${CMAKE_SOURCE_DIR}/COPYING" "${CMAKE_CURRENT_BINARY_DIR}/license.txt" COPYONLY) set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_BINARY_DIR}/license.txt") set(CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/README.md") set(CPACK_PACKAGE_CHECKSUM SHA256) if(WIN32) # CPack WiX configuration. set(CPACK_WIX_UPGRADE_GUID "5C4B6030-A1B4-4EFE-A5AF-28F6FA2E7978") set(CPACK_WIX_PROPERTY_ARPURLINFOABOUT ${CMAKE_PROJECT_HOMEPAGE_URL}) set(CPACK_WIX_PRODUCT_ICON "${CMAKE_SOURCE_DIR}/src/app/resources/zeal.ico") #set(CPACK_WIX_UI_BANNER "${CMAKE_SOURCE_DIR}/pkg/wix/banner.png") #set(CPACK_WIX_UI_DIALOG "${CMAKE_SOURCE_DIR}/pkg/wix/dialog.png") set(CPACK_WIX_EXTENSIONS "WixUtilExtension") set(CPACK_WIX_UI_REF "Zeal_InstallDir") set(CPACK_WIX_TEMPLATE "${CMAKE_SOURCE_DIR}/pkg/wix/template.xml") set(CPACK_WIX_PATCH_FILE "${CMAKE_SOURCE_DIR}/pkg/wix/patch.xml") set(CPACK_WIX_EXTRA_SOURCES "${CMAKE_SOURCE_DIR}/pkg/wix/ui.wxs" "${CMAKE_SOURCE_DIR}/pkg/wix/exitdialog.wxs" ) set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_SOURCE_DIR}/pkg/wix/cpack_pre_build.cmake") if(NOT ZEAL_PORTABLE_BUILD) list(APPEND CPACK_GENERATOR "WIX") set(CPACK_POST_BUILD_SCRIPTS "${CMAKE_SOURCE_DIR}/pkg/wix/cpack_post_build.cmake") endif() endif() # Set options for the source package. # Usage: cmake --build --target package_source set(CPACK_SOURCE_GENERATOR "TGZ;TXZ;ZIP") set(CPACK_SOURCE_PACKAGE_FILE_NAME "${_project_output_name}-${ZEAL_VERSION_FULL}") set(CPACK_SOURCE_IGNORE_FILES # Directories. ".git/" ".github/" ".vscode/" "build/" # Files. ".editorconfig" ".gitattributes" ".gitignore" ) include(CPack) ================================================ FILE: src/app/main.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WINDOWS #include #include #include // for std::ignore #endif #include using namespace Zeal; struct CommandLineParameters { bool forceMinimized; bool preventActivation; #ifdef Q_OS_WINDOWS bool attachConsole; bool registerProtocolHandlers; bool unregisterProtocolHandlers; #endif Registry::SearchQuery query; }; QString stripParameterUrl(const QString &url, const QString &scheme) { QString str = url.mid(scheme.length() + 1); if (str.startsWith(QLatin1String("//"))) { str = str.mid(2); } if (str.endsWith(QLatin1Char('/'))) { str = str.left(str.length() - 1); } return str; } CommandLineParameters parseCommandLine(const QStringList &arguments) { QCommandLineParser parser; parser.setApplicationDescription(QObject::tr("Zeal - Offline documentation browser.")); parser.addHelpOption(); parser.addVersionOption(); parser.addOptions({ {QStringLiteral("minimized"), QObject::tr("Start minimized regardless of settings.")} }); #ifdef Q_OS_WINDOWS parser.addOptions({ {QStringLiteral("attach-console"), QObject::tr("Attach console for logging.")}, {QStringLiteral("register"), QObject::tr("Register protocol handlers.")}, {QStringLiteral("unregister"), QObject::tr("Unregister protocol handlers.")} }); #endif parser.addPositionalArgument(QStringLiteral("url"), QObject::tr("dash[-plugin]:// URL")); parser.process(arguments); CommandLineParameters clParams; clParams.forceMinimized = parser.isSet(QStringLiteral("minimized")); clParams.preventActivation = false; #ifdef Q_OS_WINDOWS clParams.attachConsole = parser.isSet(QStringLiteral("attach-console")); clParams.registerProtocolHandlers = parser.isSet(QStringLiteral("register")); clParams.unregisterProtocolHandlers = parser.isSet(QStringLiteral("unregister")); if (clParams.registerProtocolHandlers && clParams.unregisterProtocolHandlers) { QTextStream(stderr) << QObject::tr("Parameter conflict: --register and --unregister.\n"); ::exit(EXIT_FAILURE); } #endif // TODO: Support dash-feed:// protocol const QString arg = QUrl::fromPercentEncoding(parser.positionalArguments().value(0).toUtf8()); if (arg.startsWith(QLatin1String("dash:"))) { clParams.query.setQuery(stripParameterUrl(arg, QStringLiteral("dash"))); } else if (arg.startsWith(QLatin1String("dash-plugin:"))) { const QUrlQuery urlQuery(stripParameterUrl(arg, QStringLiteral("dash-plugin"))); const QString keys = urlQuery.queryItemValue(QStringLiteral("keys")); if (!keys.isEmpty()) clParams.query.setKeywords(keys.split(QLatin1Char(','))); clParams.query.setQuery(urlQuery.queryItemValue(QStringLiteral("query"))); const QString preventActivation = urlQuery.queryItemValue(QStringLiteral("prevent_activation")); clParams.preventActivation = preventActivation == QLatin1String("true"); } else { clParams.query.setQuery(arg); } return clParams; } #ifdef Q_OS_WINDOWS void registerProtocolHandler(const QString &scheme, const QString &description) { const QString appPath = QDir::toNativeSeparators(QCoreApplication::applicationFilePath()); const QString regPath = QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes\\") + scheme; QScopedPointer reg(new QSettings(regPath, QSettings::NativeFormat)); reg->setValue(QStringLiteral("Default"), description); reg->setValue(QStringLiteral("URL Protocol"), QString()); reg->beginGroup(QStringLiteral("DefaultIcon")); reg->setValue(QStringLiteral("Default"), QString("%1,1").arg(appPath)); reg->endGroup(); reg->beginGroup(QStringLiteral("shell")); reg->beginGroup(QStringLiteral("open")); reg->beginGroup(QStringLiteral("command")); reg->setValue(QStringLiteral("Default"), QVariant(appPath + QLatin1String(" %1"))); } void registerProtocolHandlers(const QHash &protocols, bool force = false) { const QString regPath = QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes"); QScopedPointer reg(new QSettings(regPath, QSettings::NativeFormat)); const QStringList groups = reg->childGroups(); for (auto it = protocols.cbegin(); it != protocols.cend(); ++it) { if (force || !groups.contains(it.key())) registerProtocolHandler(it.key(), it.value()); } } void unregisterProtocolHandlers(const QHash &protocols) { const QString regPath = QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes"); QScopedPointer reg(new QSettings(regPath, QSettings::NativeFormat)); for (auto it = protocols.cbegin(); it != protocols.cend(); ++it) { reg->remove(it.key()); } } #endif int main(int argc, char *argv[]) { // Do not allow Qt version lower than the app was compiled with. QT_REQUIRE_VERSION(argc, argv, QT_VERSION_STR) QCoreApplication::setApplicationName(QStringLiteral("Zeal")); QCoreApplication::setApplicationVersion(ZEAL_VERSION); QCoreApplication::setOrganizationDomain(QStringLiteral("zealdocs.org")); QCoreApplication::setOrganizationName(QStringLiteral("Zeal")); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif // Use Fusion style on Windows 10 & 11. This enables proper dark mode support. // See https://www.qt.io/blog/dark-mode-on-windows-11-with-qt-6.5. // TODO: Make style configurable, detect -style argument. #if defined(Q_OS_WINDOWS) && (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)) const auto osName = QSysInfo::prettyProductName(); if (osName.startsWith("Windows 10") || osName.startsWith("Windows 11")) { QApplication::setStyle("fusion"); } #endif QScopedPointer qapp(new QApplication(argc, argv)); const CommandLineParameters clParams = parseCommandLine(qapp->arguments()); #ifdef Q_OS_WINDOWS const static QHash protocols = { {QStringLiteral("dash"), QStringLiteral("URL:Dash Protocol (Zeal)")}, {QStringLiteral("dash-plugin"), QStringLiteral("URL:Dash Plugin Protocol (Zeal)")} }; if (clParams.registerProtocolHandlers) { registerProtocolHandlers(protocols, clParams.registerProtocolHandlers); return EXIT_SUCCESS; } if (clParams.unregisterProtocolHandlers) { unregisterProtocolHandlers(protocols); return EXIT_SUCCESS; } #endif #ifdef Q_OS_WINDOWS if (clParams.attachConsole && AttachConsole(ATTACH_PARENT_PROCESS)) { FILE *fp = nullptr; std::ignore = freopen_s(&fp, "CONOUT$", "w", stdout); std::ignore = freopen_s(&fp, "CONOUT$", "w", stderr); std::ignore = freopen_s(&fp, "CONIN$", "r", stdin); } #endif // Q_OS_WINDOWS QScopedPointer appSingleton(new Core::ApplicationSingleton()); if (appSingleton->isSecondary()) { #ifdef Q_OS_WINDOWS ::AllowSetForegroundWindow(appSingleton->primaryPid()); #endif QByteArray ba; QDataStream out(&ba, QIODevice::WriteOnly); out << clParams.query << clParams.preventActivation; if (!appSingleton->sendMessage(ba)) { QTextStream(stderr) << "Failed to send query to the primary instance." << '\n'; return EXIT_FAILURE; } return EXIT_SUCCESS; } // Set application-wide window icon. All message boxes and other windows will use it by default. qapp->setDesktopFileName(QStringLiteral("org.zealdocs.zeal")); qapp->setWindowIcon(QIcon::fromTheme(QStringLiteral("zeal"), QIcon(QStringLiteral(":/zeal.ico")))); QDir::setSearchPaths(QStringLiteral("typeIcon"), {QStringLiteral(":/icons/type")}); QScopedPointer app(new Core::Application()); QObject::connect(appSingleton.data(), &Core::ApplicationSingleton::messageReceived, [&app](const QByteArray &data) { Registry::SearchQuery query; bool preventActivation; QDataStream in(data); in >> query >> preventActivation; app->executeQuery(query, preventActivation); }); app->showMainWindow(clParams.forceMinimized); if (!clParams.query.isEmpty()) { QTimer::singleShot(0, app.data(), [&app, clParams] { app->executeQuery(clParams.query, clParams.preventActivation); }); } return qapp->exec(); } ================================================ FILE: src/app/resources/browser/404.html ================================================ File not found

File not found

Encountered a problem? Please report!

Get in touch

Matrix

Chat with developers and other users

GitHub

Contribute to the project

================================================ FILE: src/app/resources/browser/assets/css/highlight.css ================================================ /* Highlight on navigation to an anchor. */ @keyframes targetNavigatedAnimation { from { background: #fff; } 50% { background: #ff0; } to { background: #fff; } } *:target { animation: targetNavigatedAnimation .5s linear; } ================================================ FILE: src/app/resources/browser/assets/css/welcome.css ================================================ /* Custom styles for Zeal's built-in browser pages. Depends on oat.min.css. */ body { user-select: none; min-height: 100dvh; display: flex; flex-direction: column; align-items: center; } .page { flex: 1; display: grid; grid-template-columns: 1fr 1.5fr; align-content: center; gap: 1.5rem 3rem; max-width: 720px; width: 100%; padding: 2rem; } .brand { display: flex; flex-direction: column; justify-content: center; gap: 0.25rem; } .brand h1 { font-size: 2rem; line-height: 1.1; } .brand .version { font-weight: 300; opacity: 0.5; margin-left: 0.3rem; font-size: 1rem; } .brand > p { opacity: 0.6; } .actions { display: flex; flex-direction: column; gap: 0.5rem; } .actions > h2 { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.07em; opacity: 0.45; margin-top: 0.5rem; margin-bottom: 0; } .command-block { cursor: pointer; padding: 0.625rem 0.875rem; border-radius: 6px; border: 1px solid #e2e8f0; transition: border-color 0.15s, background 0.15s; } .command-block:hover { border-color: #94a3b8; background: #f8fafc; } .command-block h3 { font-size: 0.875rem; font-weight: 600; margin-bottom: 0.125rem; } .command-block p { font-size: 0.775rem; opacity: 0.55; margin: 0; } .footer { display: flex; flex-direction: column; align-items: center; gap: 0.625rem; width: 100%; max-width: 720px; padding: 1rem 2rem; border-top: 1px solid #e2e8f0; } .icon-links { display: flex; gap: 1.25rem; align-items: center; } .icon-link { cursor: pointer; display: inline-flex; color: inherit; text-decoration: none; opacity: 0.45; transition: opacity 0.15s; } .icon-link:hover { opacity: 0.85; text-decoration: none; } .icon-link svg { width: 1.2rem; height: 1.2rem; } .report-link { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.75rem; cursor: pointer; opacity: 0.4; transition: opacity 0.15s; text-decoration: none; color: inherit; } .report-link:hover { opacity: 0.7; text-decoration: none; } .report-link svg { width: 0.85rem; height: 0.85rem; } @media (prefers-color-scheme: dark) { .command-block { border-color: #334155; } .command-block:hover { border-color: #475569; background: #1e293b; } .footer { border-color: #334155; } } ================================================ FILE: src/app/resources/browser/welcome.html ================================================ Welcome

Zeal

Docs for everyone

Customize

Docsets

Install and update docsets

Preferences

Adjust application settings

Get in touch

Matrix

Chat with developers and other users

GitHub

Contribute to the project

================================================ FILE: src/app/resources/icons/README.md ================================================ # Resources ## Dash Type Icons (`type`) Upstream repository: Optimized with: ```shell find . -type f -iname '*.png' -exec pngcrush -ow -rem allb -reduce {} \; find . -type f -name "*.png" -exec convert {} -strip {} \; optipng *.png ``` The following icons are renamed: * `Enum` to `Enumeration` * `Struct` to `Structure` ================================================ FILE: src/app/versioninfo.rc.in ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include #pragma code_page(65001) IDI_ICON1 ICON DISCARDABLE "resources/zeal.ico" #define VER_COMPANYNAME_STR "${PROJECT_COMPANY_NAME}" #define VER_FILEDESCRIPTION_STR "${CMAKE_PROJECT_DESCRIPTION}" #define VER_FILEVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH} #define VER_FILEVERSION_STR "${PROJECT_VERSION}" #define VER_INTERNALNAME_STR "${CMAKE_PROJECT_NAME}" #define VER_LEGALCOPYRIGHT_STR "${PROJECT_COPYRIGHT}" #define VER_LEGALTRADEMARKS1_STR "" #define VER_LEGALTRADEMARKS2_STR "" #define VER_ORIGINALFILENAME_STR "${PROJECT_EXECUTABLE_NAME}" #define VER_PRODUCTNAME_STR "${CMAKE_PROJECT_NAME}" #define VER_PRODUCTVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH} #define VER_PRODUCTVERSION_STR "${PROJECT_VERSION}" #ifndef DEBUG #define VER_DEBUG 0 #else #define VER_DEBUG VS_FF_DEBUG #endif VS_VERSION_INFO VERSIONINFO FILEVERSION VER_FILEVERSION PRODUCTVERSION VER_PRODUCTVERSION FILEFLAGSMASK VS_FFI_FILEFLAGSMASK //TODO: Set file flags. //FILEFLAGS (VER_PRIVATEBUILD|VER_PRERELEASE|VER_DEBUG) FILEFLAGS 0 FILEOS VOS__WINDOWS32 FILETYPE VFT_DLL FILESUBTYPE VFT2_UNKNOWN BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904B0" BEGIN VALUE "CompanyName", VER_COMPANYNAME_STR VALUE "FileDescription", VER_FILEDESCRIPTION_STR VALUE "FileVersion", VER_FILEVERSION_STR VALUE "InternalName", VER_INTERNALNAME_STR VALUE "LegalCopyright", VER_LEGALCOPYRIGHT_STR VALUE "LegalTrademarks1", VER_LEGALTRADEMARKS1_STR VALUE "LegalTrademarks2", VER_LEGALTRADEMARKS2_STR VALUE "OriginalFilename", VER_ORIGINALFILENAME_STR VALUE "ProductName", VER_PRODUCTNAME_STR VALUE "ProductVersion", VER_PRODUCTVERSION_STR END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x0409, 1200 END END ================================================ FILE: src/app/zeal.qrc ================================================ resources/zeal.ico resources/browser/404.html resources/browser/assets/css/highlight.css resources/browser/assets/css/oat.min.css resources/browser/assets/css/welcome.css resources/browser/welcome.html resources/icons/type/Abbreviation.png resources/icons/type/Abbreviation@2x.png resources/icons/type/Alias.png resources/icons/type/Alias@2x.png resources/icons/type/Annotation.png resources/icons/type/Annotation@2x.png resources/icons/type/Attribute.png resources/icons/type/Attribute@2x.png resources/icons/type/Axiom.png resources/icons/type/Axiom@2x.png resources/icons/type/Binding.png resources/icons/type/Binding@2x.png resources/icons/type/Block.png resources/icons/type/Block@2x.png resources/icons/type/Bookmark.png resources/icons/type/Bookmark@2x.png resources/icons/type/Builtin.png resources/icons/type/Builtin@2x.png resources/icons/type/Callback.png resources/icons/type/Callback@2x.png resources/icons/type/Category.png resources/icons/type/Category@2x.png resources/icons/type/Class.png resources/icons/type/Class@2x.png resources/icons/type/Collection.png resources/icons/type/Collection@2x.png resources/icons/type/Column.png resources/icons/type/Column@2x.png resources/icons/type/Command.png resources/icons/type/Command@2x.png resources/icons/type/Component.png resources/icons/type/Component@2x.png resources/icons/type/Constant.png resources/icons/type/Constant@2x.png resources/icons/type/Constructor.png resources/icons/type/Constructor@2x.png resources/icons/type/Conversion.png resources/icons/type/Conversion@2x.png resources/icons/type/Data Source.png resources/icons/type/Data Source@2x.png resources/icons/type/Database.png resources/icons/type/Database@2x.png resources/icons/type/Decorator.png resources/icons/type/Decorator@2x.png resources/icons/type/Define.png resources/icons/type/Define@2x.png resources/icons/type/Delegate.png resources/icons/type/Delegate@2x.png resources/icons/type/Device.png resources/icons/type/Device@2x.png resources/icons/type/Diagram.png resources/icons/type/Diagram@2x.png resources/icons/type/Directive.png resources/icons/type/Directive@2x.png resources/icons/type/Element.png resources/icons/type/Element@2x.png resources/icons/type/Entry.png resources/icons/type/Entry@2x.png resources/icons/type/Enumeration.png resources/icons/type/Enumeration@2x.png resources/icons/type/Environment.png resources/icons/type/Environment@2x.png resources/icons/type/Error.png resources/icons/type/Error@2x.png resources/icons/type/Event.png resources/icons/type/Event@2x.png resources/icons/type/Exception.png resources/icons/type/Exception@2x.png resources/icons/type/Expression.png resources/icons/type/Expression@2x.png resources/icons/type/Extension.png resources/icons/type/Extension@2x.png resources/icons/type/Field.png resources/icons/type/Field@2x.png resources/icons/type/File.png resources/icons/type/File@2x.png resources/icons/type/Filter.png resources/icons/type/Filter@2x.png resources/icons/type/Flag.png resources/icons/type/Flag@2x.png resources/icons/type/Foreign Key.png resources/icons/type/Foreign Key@2x.png resources/icons/type/Framework.png resources/icons/type/Framework@2x.png resources/icons/type/Function.png resources/icons/type/Function@2x.png resources/icons/type/Global.png resources/icons/type/Global@2x.png resources/icons/type/Glossary.png resources/icons/type/Glossary@2x.png resources/icons/type/Guide.png resources/icons/type/Guide@2x.png resources/icons/type/Handler.png resources/icons/type/Handler@2x.png resources/icons/type/Helper.png resources/icons/type/Helper@2x.png resources/icons/type/Hook.png resources/icons/type/Hook@2x.png resources/icons/type/Index.png resources/icons/type/Index@2x.png resources/icons/type/Indirection.png resources/icons/type/Indirection@2x.png resources/icons/type/Inductive.png resources/icons/type/Inductive@2x.png resources/icons/type/Instance.png resources/icons/type/Instance@2x.png resources/icons/type/Instruction.png resources/icons/type/Instruction@2x.png resources/icons/type/Interface.png resources/icons/type/Interface@2x.png resources/icons/type/Iterator.png resources/icons/type/Iterator@2x.png resources/icons/type/Keyword.png resources/icons/type/Keyword@2x.png resources/icons/type/Kind.png resources/icons/type/Kind@2x.png resources/icons/type/Lemma.png resources/icons/type/Lemma@2x.png resources/icons/type/Library.png resources/icons/type/Library@2x.png resources/icons/type/Literal.png resources/icons/type/Literal@2x.png resources/icons/type/Macro.png resources/icons/type/Macro@2x.png resources/icons/type/Member.png resources/icons/type/Member@2x.png resources/icons/type/Message.png resources/icons/type/Message@2x.png resources/icons/type/Method.png resources/icons/type/Method@2x.png resources/icons/type/Mixin.png resources/icons/type/Mixin@2x.png resources/icons/type/Modifier.png resources/icons/type/Modifier@2x.png resources/icons/type/Module.png resources/icons/type/Module@2x.png resources/icons/type/Namespace.png resources/icons/type/Namespace@2x.png resources/icons/type/NewSnippet.png resources/icons/type/NewSnippet@2x.png resources/icons/type/Node.png resources/icons/type/Node@2x.png resources/icons/type/Notation.png resources/icons/type/Notation@2x.png resources/icons/type/Object.png resources/icons/type/Object@2x.png resources/icons/type/Operator.png resources/icons/type/Operator@2x.png resources/icons/type/Option.png resources/icons/type/Option@2x.png resources/icons/type/Package.png resources/icons/type/Package@2x.png resources/icons/type/Parameter.png resources/icons/type/Parameter@2x.png resources/icons/type/Pattern.png resources/icons/type/Pattern@2x.png resources/icons/type/Pipe.png resources/icons/type/Pipe@2x.png resources/icons/type/Plugin.png resources/icons/type/Plugin@2x.png resources/icons/type/Procedure.png resources/icons/type/Procedure@2x.png resources/icons/type/Projection.png resources/icons/type/Projection@2x.png resources/icons/type/Property.png resources/icons/type/Property@2x.png resources/icons/type/Protocol.png resources/icons/type/Protocol@2x.png resources/icons/type/Provider.png resources/icons/type/Provider@2x.png resources/icons/type/Provisioner.png resources/icons/type/Provisioner@2x.png resources/icons/type/Query.png resources/icons/type/Query@2x.png resources/icons/type/Reference.png resources/icons/type/Reference@2x.png resources/icons/type/Register.png resources/icons/type/Register@2x.png resources/icons/type/Record.png resources/icons/type/Record@2x.png resources/icons/type/Relationship.png resources/icons/type/Relationship@2x.png resources/icons/type/Report.png resources/icons/type/Report@2x.png resources/icons/type/Request.png resources/icons/type/Request@2x.png resources/icons/type/Resource.png resources/icons/type/Resource@2x.png resources/icons/type/Role.png resources/icons/type/Role@2x.png resources/icons/type/Sample.png resources/icons/type/Sample@2x.png resources/icons/type/Schema.png resources/icons/type/Schema@2x.png resources/icons/type/Script.png resources/icons/type/Script@2x.png resources/icons/type/Section.png resources/icons/type/Section@2x.png resources/icons/type/Sender.png resources/icons/type/Sender@2x.png resources/icons/type/Service.png resources/icons/type/Service@2x.png resources/icons/type/Setting.png resources/icons/type/Setting@2x.png resources/icons/type/Shortcut.png resources/icons/type/Shortcut@2x.png resources/icons/type/Signature.png resources/icons/type/Signature@2x.png resources/icons/type/Special Form.png resources/icons/type/Special Form@2x.png resources/icons/type/State.png resources/icons/type/State@2x.png resources/icons/type/Statement.png resources/icons/type/Statement@2x.png resources/icons/type/Structure.png resources/icons/type/Structure@2x.png resources/icons/type/Style.png resources/icons/type/Style@2x.png resources/icons/type/Syntax.png resources/icons/type/Syntax@2x.png resources/icons/type/Subroutine.png resources/icons/type/Subroutine@2x.png resources/icons/type/Table.png resources/icons/type/Table@2x.png resources/icons/type/Tactic.png resources/icons/type/Tactic@2x.png resources/icons/type/Tag.png resources/icons/type/Tag@2x.png resources/icons/type/Template.png resources/icons/type/Template@2x.png resources/icons/type/Test.png resources/icons/type/Test@2x.png resources/icons/type/Trait.png resources/icons/type/Trait@2x.png resources/icons/type/Trigger.png resources/icons/type/Trigger@2x.png resources/icons/type/Type.png resources/icons/type/Type@2x.png resources/icons/type/Union.png resources/icons/type/Union@2x.png resources/icons/type/Unknown.png resources/icons/type/Unknown@2x.png resources/icons/type/Value.png resources/icons/type/Value@2x.png resources/icons/type/Variable.png resources/icons/type/Variable@2x.png resources/icons/type/Variant.png resources/icons/type/Variant@2x.png resources/icons/type/View.png resources/icons/type/View@2x.png resources/icons/type/Widget.png resources/icons/type/Widget@2x.png resources/icons/type/Word.png resources/icons/type/Word@2x.png resources/icons/logo/64x64.png resources/icons/logo/128x128.png resources/icons/logo/icon.png resources/icons/logo/icon@2x.png ================================================ FILE: src/contrib/cpp-httplib/httplib.h ================================================ // // httplib.h // // Copyright (c) 2026 Yuji Hirose. All rights reserved. // MIT License // #ifndef CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_VERSION "0.32.0" #define CPPHTTPLIB_VERSION_NUM "0x002000" /* * Platform compatibility check */ #if defined(_WIN32) && !defined(_WIN64) #if defined(_MSC_VER) #pragma message( \ "cpp-httplib doesn't support 32-bit Windows. Please use a 64-bit compiler.") #else #warning \ "cpp-httplib doesn't support 32-bit Windows. Please use a 64-bit compiler." #endif #elif defined(__SIZEOF_POINTER__) && __SIZEOF_POINTER__ < 8 #warning \ "cpp-httplib doesn't support 32-bit platforms. Please use a 64-bit compiler." #elif defined(__SIZEOF_SIZE_T__) && __SIZEOF_SIZE_T__ < 8 #warning \ "cpp-httplib doesn't support platforms where size_t is less than 64 bits." #endif #ifdef _WIN32 #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00 #error \ "cpp-httplib doesn't support Windows 8 or lower. Please use Windows 10 or later." #endif #endif /* * Configuration */ #ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND #define CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND 5 #endif #ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND #define CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND 10000 #endif #ifndef CPPHTTPLIB_KEEPALIVE_MAX_COUNT #define CPPHTTPLIB_KEEPALIVE_MAX_COUNT 100 #endif #ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND #define CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND 300 #endif #ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND #define CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND 0 #endif #ifndef CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND #define CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND 5 #endif #ifndef CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND #define CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND 0 #endif #ifndef CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND #define CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND 5 #endif #ifndef CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND #define CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND 0 #endif #ifndef CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND #define CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND 300 #endif #ifndef CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND #define CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND 0 #endif #ifndef CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND #define CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND 5 #endif #ifndef CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND #define CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND 0 #endif #ifndef CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND #define CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND 0 #endif #ifndef CPPHTTPLIB_EXPECT_100_THRESHOLD #define CPPHTTPLIB_EXPECT_100_THRESHOLD 1024 #endif #ifndef CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND #define CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND 1000 #endif #ifndef CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD #define CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD (1024 * 1024) #endif #ifndef CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND #define CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND 50 #endif #ifndef CPPHTTPLIB_IDLE_INTERVAL_SECOND #define CPPHTTPLIB_IDLE_INTERVAL_SECOND 0 #endif #ifndef CPPHTTPLIB_IDLE_INTERVAL_USECOND #ifdef _WIN32 #define CPPHTTPLIB_IDLE_INTERVAL_USECOND 1000 #else #define CPPHTTPLIB_IDLE_INTERVAL_USECOND 0 #endif #endif #ifndef CPPHTTPLIB_REQUEST_URI_MAX_LENGTH #define CPPHTTPLIB_REQUEST_URI_MAX_LENGTH 8192 #endif #ifndef CPPHTTPLIB_HEADER_MAX_LENGTH #define CPPHTTPLIB_HEADER_MAX_LENGTH 8192 #endif #ifndef CPPHTTPLIB_HEADER_MAX_COUNT #define CPPHTTPLIB_HEADER_MAX_COUNT 100 #endif #ifndef CPPHTTPLIB_REDIRECT_MAX_COUNT #define CPPHTTPLIB_REDIRECT_MAX_COUNT 20 #endif #ifndef CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT #define CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT 1024 #endif #ifndef CPPHTTPLIB_PAYLOAD_MAX_LENGTH #define CPPHTTPLIB_PAYLOAD_MAX_LENGTH (100 * 1024 * 1024) // 100MB #endif #ifndef CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH #define CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH 8192 #endif #ifndef CPPHTTPLIB_RANGE_MAX_COUNT #define CPPHTTPLIB_RANGE_MAX_COUNT 1024 #endif #ifndef CPPHTTPLIB_TCP_NODELAY #define CPPHTTPLIB_TCP_NODELAY false #endif #ifndef CPPHTTPLIB_IPV6_V6ONLY #define CPPHTTPLIB_IPV6_V6ONLY false #endif #ifndef CPPHTTPLIB_RECV_BUFSIZ #define CPPHTTPLIB_RECV_BUFSIZ size_t(16384u) #endif #ifndef CPPHTTPLIB_SEND_BUFSIZ #define CPPHTTPLIB_SEND_BUFSIZ size_t(16384u) #endif #ifndef CPPHTTPLIB_COMPRESSION_BUFSIZ #define CPPHTTPLIB_COMPRESSION_BUFSIZ size_t(16384u) #endif #ifndef CPPHTTPLIB_THREAD_POOL_COUNT #define CPPHTTPLIB_THREAD_POOL_COUNT \ ((std::max)(8u, std::thread::hardware_concurrency() > 0 \ ? std::thread::hardware_concurrency() - 1 \ : 0)) #endif #ifndef CPPHTTPLIB_RECV_FLAGS #define CPPHTTPLIB_RECV_FLAGS 0 #endif #ifndef CPPHTTPLIB_SEND_FLAGS #define CPPHTTPLIB_SEND_FLAGS 0 #endif #ifndef CPPHTTPLIB_LISTEN_BACKLOG #define CPPHTTPLIB_LISTEN_BACKLOG 5 #endif #ifndef CPPHTTPLIB_MAX_LINE_LENGTH #define CPPHTTPLIB_MAX_LINE_LENGTH 32768 #endif /* * Headers */ #ifdef _WIN32 #ifndef _CRT_SECURE_NO_WARNINGS #define _CRT_SECURE_NO_WARNINGS #endif //_CRT_SECURE_NO_WARNINGS #ifndef _CRT_NONSTDC_NO_DEPRECATE #define _CRT_NONSTDC_NO_DEPRECATE #endif //_CRT_NONSTDC_NO_DEPRECATE #if defined(_MSC_VER) #if _MSC_VER < 1900 #error Sorry, Visual Studio versions prior to 2015 are not supported #endif #pragma comment(lib, "ws2_32.lib") #ifndef _SSIZE_T_DEFINED using ssize_t = __int64; #define _SSIZE_T_DEFINED #endif #endif // _MSC_VER #ifndef S_ISREG #define S_ISREG(m) (((m) & S_IFREG) == S_IFREG) #endif // S_ISREG #ifndef S_ISDIR #define S_ISDIR(m) (((m) & S_IFDIR) == S_IFDIR) #endif // S_ISDIR #ifndef NOMINMAX #define NOMINMAX #endif // NOMINMAX #include #include #include #if defined(__has_include) #if __has_include() // afunix.h uses types declared in winsock2.h, so has to be included after it. #include #define CPPHTTPLIB_HAVE_AFUNIX_H 1 #endif #endif #ifndef WSA_FLAG_NO_HANDLE_INHERIT #define WSA_FLAG_NO_HANDLE_INHERIT 0x80 #endif using nfds_t = unsigned long; using socket_t = SOCKET; using socklen_t = int; #else // not _WIN32 #include #if !defined(_AIX) && !defined(__MVS__) #include #endif #ifdef __MVS__ #include #ifndef NI_MAXHOST #define NI_MAXHOST 1025 #endif #endif #include #include #include #ifdef __linux__ #include #undef _res // Undefine _res macro to avoid conflicts with user code (#2278) #endif #include #include #include #include #include #include #include #include using socket_t = int; #ifndef INVALID_SOCKET #define INVALID_SOCKET (-1) #endif #endif //_WIN32 #if defined(__APPLE__) #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO) || \ defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) #if TARGET_OS_MAC #include #include #endif #endif // CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO or // CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN #ifdef CPPHTTPLIB_OPENSSL_SUPPORT #ifdef _WIN32 #include // these are defined in wincrypt.h and it breaks compilation if BoringSSL is // used #undef X509_NAME #undef X509_CERT_PAIR #undef X509_EXTENSIONS #undef PKCS7_SIGNER_INFO #ifdef _MSC_VER #pragma comment(lib, "crypt32.lib") #endif #endif // _WIN32 #if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) #if TARGET_OS_MAC #include #endif #endif // CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO #include #include #include #include #if defined(_WIN32) && defined(OPENSSL_USE_APPLINK) #include #endif #include #include #if defined(OPENSSL_IS_BORINGSSL) || defined(LIBRESSL_VERSION_NUMBER) #if OPENSSL_VERSION_NUMBER < 0x1010107f #error Please use OpenSSL or a current version of BoringSSL #endif #define SSL_get1_peer_certificate SSL_get_peer_certificate #elif OPENSSL_VERSION_NUMBER < 0x30000000L #error Sorry, OpenSSL versions prior to 3.0.0 are not supported #endif #endif // CPPHTTPLIB_OPENSSL_SUPPORT #ifdef CPPHTTPLIB_MBEDTLS_SUPPORT #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #ifdef _MSC_VER #pragma comment(lib, "crypt32.lib") #endif #endif // _WIN32 #if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) #if TARGET_OS_MAC #include #endif #endif // CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN // Mbed TLS 3.x API compatibility #if MBEDTLS_VERSION_MAJOR >= 3 #define CPPHTTPLIB_MBEDTLS_V3 #endif #endif // CPPHTTPLIB_MBEDTLS_SUPPORT // Define CPPHTTPLIB_SSL_ENABLED if any SSL backend is available // This simplifies conditional compilation when adding new backends (e.g., // wolfSSL) #if defined(CPPHTTPLIB_OPENSSL_SUPPORT) || defined(CPPHTTPLIB_MBEDTLS_SUPPORT) #define CPPHTTPLIB_SSL_ENABLED #endif #ifdef CPPHTTPLIB_ZLIB_SUPPORT #include #endif #ifdef CPPHTTPLIB_BROTLI_SUPPORT #include #include #endif #ifdef CPPHTTPLIB_ZSTD_SUPPORT #include #endif /* * Declaration */ namespace httplib { namespace detail { /* * Backport std::make_unique from C++14. * * NOTE: This code came up with the following stackoverflow post: * https://stackoverflow.com/questions/10149840/c-arrays-and-make-unique * */ template typename std::enable_if::value, std::unique_ptr>::type make_unique(Args &&...args) { return std::unique_ptr(new T(std::forward(args)...)); } template typename std::enable_if::value, std::unique_ptr>::type make_unique(std::size_t n) { typedef typename std::remove_extent::type RT; return std::unique_ptr(new RT[n]); } namespace case_ignore { inline unsigned char to_lower(int c) { const static unsigned char table[256] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 215, 248, 249, 250, 251, 252, 253, 254, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, }; return table[(unsigned char)(char)c]; } inline bool equal(const std::string &a, const std::string &b) { return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [](char ca, char cb) { return to_lower(ca) == to_lower(cb); }); } struct equal_to { bool operator()(const std::string &a, const std::string &b) const { return equal(a, b); } }; struct hash { size_t operator()(const std::string &key) const { return hash_core(key.data(), key.size(), 0); } size_t hash_core(const char *s, size_t l, size_t h) const { return (l == 0) ? h : hash_core(s + 1, l - 1, // Unsets the 6 high bits of h, therefore no // overflow happens (((std::numeric_limits::max)() >> 6) & h * 33) ^ static_cast(to_lower(*s))); } }; template using unordered_set = std::unordered_set; } // namespace case_ignore // This is based on // "http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189". struct scope_exit { explicit scope_exit(std::function &&f) : exit_function(std::move(f)), execute_on_destruction{true} {} scope_exit(scope_exit &&rhs) noexcept : exit_function(std::move(rhs.exit_function)), execute_on_destruction{rhs.execute_on_destruction} { rhs.release(); } ~scope_exit() { if (execute_on_destruction) { this->exit_function(); } } void release() { this->execute_on_destruction = false; } private: scope_exit(const scope_exit &) = delete; void operator=(const scope_exit &) = delete; scope_exit &operator=(scope_exit &&) = delete; std::function exit_function; bool execute_on_destruction; }; // Simple from_chars implementation for integer and double types (C++17 // substitute) template struct from_chars_result { const char *ptr; std::errc ec; }; template inline from_chars_result from_chars(const char *first, const char *last, T &value, int base = 10) { value = 0; const char *p = first; bool negative = false; if (p != last && *p == '-') { negative = true; ++p; } if (p == last) { return {first, std::errc::invalid_argument}; } T result = 0; for (; p != last; ++p) { char c = *p; int digit = -1; if ('0' <= c && c <= '9') { digit = c - '0'; } else if ('a' <= c && c <= 'z') { digit = c - 'a' + 10; } else if ('A' <= c && c <= 'Z') { digit = c - 'A' + 10; } else { break; } if (digit < 0 || digit >= base) { break; } if (result > ((std::numeric_limits::max)() - digit) / base) { return {p, std::errc::result_out_of_range}; } result = result * base + digit; } if (p == first || (negative && p == first + 1)) { return {first, std::errc::invalid_argument}; } value = negative ? -result : result; return {p, std::errc{}}; } // from_chars for double (simple wrapper for strtod) inline from_chars_result from_chars(const char *first, const char *last, double &value) { std::string s(first, last); char *endptr = nullptr; errno = 0; value = std::strtod(s.c_str(), &endptr); if (endptr == s.c_str()) { return {first, std::errc::invalid_argument}; } if (errno == ERANGE) { return {first + (endptr - s.c_str()), std::errc::result_out_of_range}; } return {first + (endptr - s.c_str()), std::errc{}}; } } // namespace detail enum SSLVerifierResponse { // no decision has been made, use the built-in certificate verifier NoDecisionMade, // connection certificate is verified and accepted CertificateAccepted, // connection certificate was processed but is rejected CertificateRejected }; enum StatusCode { // Information responses Continue_100 = 100, SwitchingProtocol_101 = 101, Processing_102 = 102, EarlyHints_103 = 103, // Successful responses OK_200 = 200, Created_201 = 201, Accepted_202 = 202, NonAuthoritativeInformation_203 = 203, NoContent_204 = 204, ResetContent_205 = 205, PartialContent_206 = 206, MultiStatus_207 = 207, AlreadyReported_208 = 208, IMUsed_226 = 226, // Redirection messages MultipleChoices_300 = 300, MovedPermanently_301 = 301, Found_302 = 302, SeeOther_303 = 303, NotModified_304 = 304, UseProxy_305 = 305, unused_306 = 306, TemporaryRedirect_307 = 307, PermanentRedirect_308 = 308, // Client error responses BadRequest_400 = 400, Unauthorized_401 = 401, PaymentRequired_402 = 402, Forbidden_403 = 403, NotFound_404 = 404, MethodNotAllowed_405 = 405, NotAcceptable_406 = 406, ProxyAuthenticationRequired_407 = 407, RequestTimeout_408 = 408, Conflict_409 = 409, Gone_410 = 410, LengthRequired_411 = 411, PreconditionFailed_412 = 412, PayloadTooLarge_413 = 413, UriTooLong_414 = 414, UnsupportedMediaType_415 = 415, RangeNotSatisfiable_416 = 416, ExpectationFailed_417 = 417, ImATeapot_418 = 418, MisdirectedRequest_421 = 421, UnprocessableContent_422 = 422, Locked_423 = 423, FailedDependency_424 = 424, TooEarly_425 = 425, UpgradeRequired_426 = 426, PreconditionRequired_428 = 428, TooManyRequests_429 = 429, RequestHeaderFieldsTooLarge_431 = 431, UnavailableForLegalReasons_451 = 451, // Server error responses InternalServerError_500 = 500, NotImplemented_501 = 501, BadGateway_502 = 502, ServiceUnavailable_503 = 503, GatewayTimeout_504 = 504, HttpVersionNotSupported_505 = 505, VariantAlsoNegotiates_506 = 506, InsufficientStorage_507 = 507, LoopDetected_508 = 508, NotExtended_510 = 510, NetworkAuthenticationRequired_511 = 511, }; using Headers = std::unordered_multimap; using Params = std::multimap; using Match = std::smatch; using DownloadProgress = std::function; using UploadProgress = std::function; struct Response; using ResponseHandler = std::function; struct FormData { std::string name; std::string content; std::string filename; std::string content_type; Headers headers; }; struct FormField { std::string name; std::string content; Headers headers; }; using FormFields = std::multimap; using FormFiles = std::multimap; struct MultipartFormData { FormFields fields; // Text fields from multipart FormFiles files; // Files from multipart // Text field access std::string get_field(const std::string &key, size_t id = 0) const; std::vector get_fields(const std::string &key) const; bool has_field(const std::string &key) const; size_t get_field_count(const std::string &key) const; // File access FormData get_file(const std::string &key, size_t id = 0) const; std::vector get_files(const std::string &key) const; bool has_file(const std::string &key) const; size_t get_file_count(const std::string &key) const; }; struct UploadFormData { std::string name; std::string content; std::string filename; std::string content_type; }; using UploadFormDataItems = std::vector; class DataSink { public: DataSink() : os(&sb_), sb_(*this) {} DataSink(const DataSink &) = delete; DataSink &operator=(const DataSink &) = delete; DataSink(DataSink &&) = delete; DataSink &operator=(DataSink &&) = delete; std::function write; std::function is_writable; std::function done; std::function done_with_trailer; std::ostream os; private: class data_sink_streambuf final : public std::streambuf { public: explicit data_sink_streambuf(DataSink &sink) : sink_(sink) {} protected: std::streamsize xsputn(const char *s, std::streamsize n) override { sink_.write(s, static_cast(n)); return n; } private: DataSink &sink_; }; data_sink_streambuf sb_; }; using ContentProvider = std::function; using ContentProviderWithoutLength = std::function; using ContentProviderResourceReleaser = std::function; struct FormDataProvider { std::string name; ContentProviderWithoutLength provider; std::string filename; std::string content_type; }; using FormDataProviderItems = std::vector; using ContentReceiverWithProgress = std::function; using ContentReceiver = std::function; using FormDataHeader = std::function; class ContentReader { public: using Reader = std::function; using FormDataReader = std::function; ContentReader(Reader reader, FormDataReader multipart_reader) : reader_(std::move(reader)), formdata_reader_(std::move(multipart_reader)) {} bool operator()(FormDataHeader header, ContentReceiver receiver) const { return formdata_reader_(std::move(header), std::move(receiver)); } bool operator()(ContentReceiver receiver) const { return reader_(std::move(receiver)); } Reader reader_; FormDataReader formdata_reader_; }; using Range = std::pair; using Ranges = std::vector; #ifdef CPPHTTPLIB_SSL_ENABLED // TLS abstraction layer - public type definitions and API namespace tls { // Opaque handles (defined as void* for abstraction) using ctx_t = void *; using session_t = void *; using const_session_t = const void *; // For read-only session access using cert_t = void *; using ca_store_t = void *; // TLS versions enum class Version { TLS1_2 = 0x0303, TLS1_3 = 0x0304, }; // Subject Alternative Names (SAN) entry types enum class SanType { DNS, IP, EMAIL, URI, OTHER }; // SAN entry structure struct SanEntry { SanType type; std::string value; }; // Verification context for certificate verification callback struct VerifyContext { session_t session; // TLS session handle cert_t cert; // Current certificate being verified int depth; // Certificate chain depth (0 = leaf) bool preverify_ok; // OpenSSL/Mbed TLS pre-verification result long error_code; // Backend-specific error code (0 = no error) const char *error_string; // Human-readable error description // Certificate introspection methods std::string subject_cn() const; std::string issuer_name() const; bool check_hostname(const char *hostname) const; std::vector sans() const; bool validity(time_t ¬_before, time_t ¬_after) const; std::string serial() const; }; using VerifyCallback = std::function; // TlsError codes for TLS operations (backend-independent) enum class ErrorCode : int { Success = 0, WantRead, // Non-blocking: need to wait for read WantWrite, // Non-blocking: need to wait for write PeerClosed, // Peer closed the connection Fatal, // Unrecoverable error SyscallError, // System call error (check sys_errno) CertVerifyFailed, // Certificate verification failed HostnameMismatch, // Hostname verification failed }; // TLS error information struct TlsError { ErrorCode code = ErrorCode::Fatal; uint64_t backend_code = 0; // OpenSSL: ERR_get_error(), mbedTLS: return value int sys_errno = 0; // errno when SyscallError // Convert verification error code to human-readable string static std::string verify_error_to_string(long error_code); }; // RAII wrapper for peer certificate class PeerCert { public: PeerCert(); PeerCert(PeerCert &&other) noexcept; PeerCert &operator=(PeerCert &&other) noexcept; ~PeerCert(); PeerCert(const PeerCert &) = delete; PeerCert &operator=(const PeerCert &) = delete; explicit operator bool() const; std::string subject_cn() const; std::string issuer_name() const; bool check_hostname(const char *hostname) const; std::vector sans() const; bool validity(time_t ¬_before, time_t ¬_after) const; std::string serial() const; private: explicit PeerCert(cert_t cert); cert_t cert_ = nullptr; friend PeerCert get_peer_cert_from_session(const_session_t session); }; // Callback for TLS context setup (used by SSLServer constructor) using ContextSetupCallback = std::function; } // namespace tls #endif struct Request { std::string method; std::string path; std::string matched_route; Params params; Headers headers; Headers trailers; std::string body; std::string remote_addr; int remote_port = -1; std::string local_addr; int local_port = -1; // for server std::string version; std::string target; MultipartFormData form; Ranges ranges; Match matches; std::unordered_map path_params; std::function is_connection_closed = []() { return true; }; // for client std::vector accept_content_types; ResponseHandler response_handler; ContentReceiverWithProgress content_receiver; DownloadProgress download_progress; UploadProgress upload_progress; bool has_header(const std::string &key) const; std::string get_header_value(const std::string &key, const char *def = "", size_t id = 0) const; size_t get_header_value_u64(const std::string &key, size_t def = 0, size_t id = 0) const; size_t get_header_value_count(const std::string &key) const; void set_header(const std::string &key, const std::string &val); bool has_trailer(const std::string &key) const; std::string get_trailer_value(const std::string &key, size_t id = 0) const; size_t get_trailer_value_count(const std::string &key) const; bool has_param(const std::string &key) const; std::string get_param_value(const std::string &key, size_t id = 0) const; size_t get_param_value_count(const std::string &key) const; bool is_multipart_form_data() const; // private members... size_t redirect_count_ = CPPHTTPLIB_REDIRECT_MAX_COUNT; size_t content_length_ = 0; ContentProvider content_provider_; bool is_chunked_content_provider_ = false; size_t authorization_count_ = 0; std::chrono::time_point start_time_ = (std::chrono::steady_clock::time_point::min)(); #ifdef CPPHTTPLIB_SSL_ENABLED tls::const_session_t ssl = nullptr; tls::PeerCert peer_cert() const; std::string sni() const; #endif }; struct Response { std::string version; int status = -1; std::string reason; Headers headers; Headers trailers; std::string body; std::string location; // Redirect location bool has_header(const std::string &key) const; std::string get_header_value(const std::string &key, const char *def = "", size_t id = 0) const; size_t get_header_value_u64(const std::string &key, size_t def = 0, size_t id = 0) const; size_t get_header_value_count(const std::string &key) const; void set_header(const std::string &key, const std::string &val); bool has_trailer(const std::string &key) const; std::string get_trailer_value(const std::string &key, size_t id = 0) const; size_t get_trailer_value_count(const std::string &key) const; void set_redirect(const std::string &url, int status = StatusCode::Found_302); void set_content(const char *s, size_t n, const std::string &content_type); void set_content(const std::string &s, const std::string &content_type); void set_content(std::string &&s, const std::string &content_type); void set_content_provider( size_t length, const std::string &content_type, ContentProvider provider, ContentProviderResourceReleaser resource_releaser = nullptr); void set_content_provider( const std::string &content_type, ContentProviderWithoutLength provider, ContentProviderResourceReleaser resource_releaser = nullptr); void set_chunked_content_provider( const std::string &content_type, ContentProviderWithoutLength provider, ContentProviderResourceReleaser resource_releaser = nullptr); void set_file_content(const std::string &path, const std::string &content_type); void set_file_content(const std::string &path); Response() = default; Response(const Response &) = default; Response &operator=(const Response &) = default; Response(Response &&) = default; Response &operator=(Response &&) = default; ~Response() { if (content_provider_resource_releaser_) { content_provider_resource_releaser_(content_provider_success_); } } // private members... size_t content_length_ = 0; ContentProvider content_provider_; ContentProviderResourceReleaser content_provider_resource_releaser_; bool is_chunked_content_provider_ = false; bool content_provider_success_ = false; std::string file_content_path_; std::string file_content_content_type_; }; enum class Error { Success = 0, Unknown, Connection, BindIPAddress, Read, Write, ExceedRedirectCount, Canceled, SSLConnection, SSLLoadingCerts, SSLServerVerification, SSLServerHostnameVerification, UnsupportedMultipartBoundaryChars, Compression, ConnectionTimeout, ProxyConnection, ConnectionClosed, Timeout, ResourceExhaustion, TooManyFormDataFiles, ExceedMaxPayloadSize, ExceedUriMaxLength, ExceedMaxSocketDescriptorCount, InvalidRequestLine, InvalidHTTPMethod, InvalidHTTPVersion, InvalidHeaders, MultipartParsing, OpenFile, Listen, GetSockName, UnsupportedAddressFamily, HTTPParsing, InvalidRangeHeader, // For internal use only SSLPeerCouldBeClosed_, }; std::string to_string(Error error); std::ostream &operator<<(std::ostream &os, const Error &obj); class Stream { public: virtual ~Stream() = default; virtual bool is_readable() const = 0; virtual bool wait_readable() const = 0; virtual bool wait_writable() const = 0; virtual ssize_t read(char *ptr, size_t size) = 0; virtual ssize_t write(const char *ptr, size_t size) = 0; virtual void get_remote_ip_and_port(std::string &ip, int &port) const = 0; virtual void get_local_ip_and_port(std::string &ip, int &port) const = 0; virtual socket_t socket() const = 0; virtual time_t duration() const = 0; ssize_t write(const char *ptr); ssize_t write(const std::string &s); Error get_error() const { return error_; } protected: Error error_ = Error::Success; }; class TaskQueue { public: TaskQueue() = default; virtual ~TaskQueue() = default; virtual bool enqueue(std::function fn) = 0; virtual void shutdown() = 0; virtual void on_idle() {} }; class ThreadPool final : public TaskQueue { public: explicit ThreadPool(size_t n, size_t mqr = 0); ThreadPool(const ThreadPool &) = delete; ~ThreadPool() override = default; bool enqueue(std::function fn) override; void shutdown() override; private: struct worker { explicit worker(ThreadPool &pool); void operator()(); ThreadPool &pool_; }; friend struct worker; std::vector threads_; std::list> jobs_; bool shutdown_; size_t max_queued_requests_ = 0; std::condition_variable cond_; std::mutex mutex_; }; using Logger = std::function; // Forward declaration for Error type enum class Error; using ErrorLogger = std::function; using SocketOptions = std::function; void default_socket_options(socket_t sock); const char *status_message(int status); std::string to_string(Error error); std::ostream &operator<<(std::ostream &os, const Error &obj); std::string get_bearer_token_auth(const Request &req); namespace detail { class MatcherBase { public: MatcherBase(std::string pattern) : pattern_(std::move(pattern)) {} virtual ~MatcherBase() = default; const std::string &pattern() const { return pattern_; } // Match request path and populate its matches and virtual bool match(Request &request) const = 0; private: std::string pattern_; }; /** * Captures parameters in request path and stores them in Request::path_params * * Capture name is a substring of a pattern from : to /. * The rest of the pattern is matched against the request path directly * Parameters are captured starting from the next character after * the end of the last matched static pattern fragment until the next /. * * Example pattern: * "/path/fragments/:capture/more/fragments/:second_capture" * Static fragments: * "/path/fragments/", "more/fragments/" * * Given the following request path: * "/path/fragments/:1/more/fragments/:2" * the resulting capture will be * {{"capture", "1"}, {"second_capture", "2"}} */ class PathParamsMatcher final : public MatcherBase { public: PathParamsMatcher(const std::string &pattern); bool match(Request &request) const override; private: // Treat segment separators as the end of path parameter capture // Does not need to handle query parameters as they are parsed before path // matching static constexpr char separator = '/'; // Contains static path fragments to match against, excluding the '/' after // path params // Fragments are separated by path params std::vector static_fragments_; // Stores the names of the path parameters to be used as keys in the // Request::path_params map std::vector param_names_; }; /** * Performs std::regex_match on request path * and stores the result in Request::matches * * Note that regex match is performed directly on the whole request. * This means that wildcard patterns may match multiple path segments with /: * "/begin/(.*)/end" will match both "/begin/middle/end" and "/begin/1/2/end". */ class RegexMatcher final : public MatcherBase { public: RegexMatcher(const std::string &pattern) : MatcherBase(pattern), regex_(pattern) {} bool match(Request &request) const override; private: std::regex regex_; }; int close_socket(socket_t sock); ssize_t write_headers(Stream &strm, const Headers &headers); bool set_socket_opt_time(socket_t sock, int level, int optname, time_t sec, time_t usec); } // namespace detail class Server { public: using Handler = std::function; using ExceptionHandler = std::function; enum class HandlerResponse { Handled, Unhandled, }; using HandlerWithResponse = std::function; using HandlerWithContentReader = std::function; using Expect100ContinueHandler = std::function; Server(); virtual ~Server(); virtual bool is_valid() const; Server &Get(const std::string &pattern, Handler handler); Server &Post(const std::string &pattern, Handler handler); Server &Post(const std::string &pattern, HandlerWithContentReader handler); Server &Put(const std::string &pattern, Handler handler); Server &Put(const std::string &pattern, HandlerWithContentReader handler); Server &Patch(const std::string &pattern, Handler handler); Server &Patch(const std::string &pattern, HandlerWithContentReader handler); Server &Delete(const std::string &pattern, Handler handler); Server &Delete(const std::string &pattern, HandlerWithContentReader handler); Server &Options(const std::string &pattern, Handler handler); bool set_base_dir(const std::string &dir, const std::string &mount_point = std::string()); bool set_mount_point(const std::string &mount_point, const std::string &dir, Headers headers = Headers()); bool remove_mount_point(const std::string &mount_point); Server &set_file_extension_and_mimetype_mapping(const std::string &ext, const std::string &mime); Server &set_default_file_mimetype(const std::string &mime); Server &set_file_request_handler(Handler handler); template Server &set_error_handler(ErrorHandlerFunc &&handler) { return set_error_handler_core( std::forward(handler), std::is_convertible{}); } Server &set_exception_handler(ExceptionHandler handler); Server &set_pre_routing_handler(HandlerWithResponse handler); Server &set_post_routing_handler(Handler handler); Server &set_pre_request_handler(HandlerWithResponse handler); Server &set_expect_100_continue_handler(Expect100ContinueHandler handler); Server &set_logger(Logger logger); Server &set_pre_compression_logger(Logger logger); Server &set_error_logger(ErrorLogger error_logger); Server &set_address_family(int family); Server &set_tcp_nodelay(bool on); Server &set_ipv6_v6only(bool on); Server &set_socket_options(SocketOptions socket_options); Server &set_default_headers(Headers headers); Server & set_header_writer(std::function const &writer); Server &set_trusted_proxies(const std::vector &proxies); Server &set_keep_alive_max_count(size_t count); Server &set_keep_alive_timeout(time_t sec); Server &set_read_timeout(time_t sec, time_t usec = 0); template Server &set_read_timeout(const std::chrono::duration &duration); Server &set_write_timeout(time_t sec, time_t usec = 0); template Server &set_write_timeout(const std::chrono::duration &duration); Server &set_idle_interval(time_t sec, time_t usec = 0); template Server &set_idle_interval(const std::chrono::duration &duration); Server &set_payload_max_length(size_t length); bool bind_to_port(const std::string &host, int port, int socket_flags = 0); int bind_to_any_port(const std::string &host, int socket_flags = 0); bool listen_after_bind(); bool listen(const std::string &host, int port, int socket_flags = 0); bool is_running() const; void wait_until_ready() const; void stop(); void decommission(); std::function new_task_queue; protected: bool process_request(Stream &strm, const std::string &remote_addr, int remote_port, const std::string &local_addr, int local_port, bool close_connection, bool &connection_closed, const std::function &setup_request); std::atomic svr_sock_{INVALID_SOCKET}; std::vector trusted_proxies_; size_t keep_alive_max_count_ = CPPHTTPLIB_KEEPALIVE_MAX_COUNT; time_t keep_alive_timeout_sec_ = CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND; time_t read_timeout_sec_ = CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND; time_t read_timeout_usec_ = CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND; time_t write_timeout_sec_ = CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND; time_t write_timeout_usec_ = CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND; time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND; time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND; size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH; private: using Handlers = std::vector, Handler>>; using HandlersForContentReader = std::vector, HandlerWithContentReader>>; static std::unique_ptr make_matcher(const std::string &pattern); Server &set_error_handler_core(HandlerWithResponse handler, std::true_type); Server &set_error_handler_core(Handler handler, std::false_type); socket_t create_server_socket(const std::string &host, int port, int socket_flags, SocketOptions socket_options) const; int bind_internal(const std::string &host, int port, int socket_flags); bool listen_internal(); bool routing(Request &req, Response &res, Stream &strm); bool handle_file_request(Request &req, Response &res); bool check_if_not_modified(const Request &req, Response &res, const std::string &etag, time_t mtime) const; bool check_if_range(Request &req, const std::string &etag, time_t mtime) const; bool dispatch_request(Request &req, Response &res, const Handlers &handlers) const; bool dispatch_request_for_content_reader( Request &req, Response &res, ContentReader content_reader, const HandlersForContentReader &handlers) const; bool parse_request_line(const char *s, Request &req) const; void apply_ranges(const Request &req, Response &res, std::string &content_type, std::string &boundary) const; bool write_response(Stream &strm, bool close_connection, Request &req, Response &res); bool write_response_with_content(Stream &strm, bool close_connection, const Request &req, Response &res); bool write_response_core(Stream &strm, bool close_connection, const Request &req, Response &res, bool need_apply_ranges); bool write_content_with_provider(Stream &strm, const Request &req, Response &res, const std::string &boundary, const std::string &content_type); bool read_content(Stream &strm, Request &req, Response &res); bool read_content_with_content_receiver(Stream &strm, Request &req, Response &res, ContentReceiver receiver, FormDataHeader multipart_header, ContentReceiver multipart_receiver); bool read_content_core(Stream &strm, Request &req, Response &res, ContentReceiver receiver, FormDataHeader multipart_header, ContentReceiver multipart_receiver) const; virtual bool process_and_close_socket(socket_t sock); void output_log(const Request &req, const Response &res) const; void output_pre_compression_log(const Request &req, const Response &res) const; void output_error_log(const Error &err, const Request *req) const; std::atomic is_running_{false}; std::atomic is_decommissioned{false}; struct MountPointEntry { std::string mount_point; std::string base_dir; Headers headers; }; std::vector base_dirs_; std::map file_extension_and_mimetype_map_; std::string default_file_mimetype_ = "application/octet-stream"; Handler file_request_handler_; Handlers get_handlers_; Handlers post_handlers_; HandlersForContentReader post_handlers_for_content_reader_; Handlers put_handlers_; HandlersForContentReader put_handlers_for_content_reader_; Handlers patch_handlers_; HandlersForContentReader patch_handlers_for_content_reader_; Handlers delete_handlers_; HandlersForContentReader delete_handlers_for_content_reader_; Handlers options_handlers_; HandlerWithResponse error_handler_; ExceptionHandler exception_handler_; HandlerWithResponse pre_routing_handler_; Handler post_routing_handler_; HandlerWithResponse pre_request_handler_; Expect100ContinueHandler expect_100_continue_handler_; mutable std::mutex logger_mutex_; Logger logger_; Logger pre_compression_logger_; ErrorLogger error_logger_; int address_family_ = AF_UNSPEC; bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; bool ipv6_v6only_ = CPPHTTPLIB_IPV6_V6ONLY; SocketOptions socket_options_ = default_socket_options; Headers default_headers_; std::function header_writer_ = detail::write_headers; }; class Result { public: Result() = default; Result(std::unique_ptr &&res, Error err, Headers &&request_headers = Headers{}) : res_(std::move(res)), err_(err), request_headers_(std::move(request_headers)) {} // Response operator bool() const { return res_ != nullptr; } bool operator==(std::nullptr_t) const { return res_ == nullptr; } bool operator!=(std::nullptr_t) const { return res_ != nullptr; } const Response &value() const { return *res_; } Response &value() { return *res_; } const Response &operator*() const { return *res_; } Response &operator*() { return *res_; } const Response *operator->() const { return res_.get(); } Response *operator->() { return res_.get(); } // Error Error error() const { return err_; } // Request Headers bool has_request_header(const std::string &key) const; std::string get_request_header_value(const std::string &key, const char *def = "", size_t id = 0) const; size_t get_request_header_value_u64(const std::string &key, size_t def = 0, size_t id = 0) const; size_t get_request_header_value_count(const std::string &key) const; private: std::unique_ptr res_; Error err_ = Error::Unknown; Headers request_headers_; #ifdef CPPHTTPLIB_SSL_ENABLED public: Result(std::unique_ptr &&res, Error err, Headers &&request_headers, int ssl_error) : res_(std::move(res)), err_(err), request_headers_(std::move(request_headers)), ssl_error_(ssl_error) {} Result(std::unique_ptr &&res, Error err, Headers &&request_headers, int ssl_error, unsigned long ssl_backend_error) : res_(std::move(res)), err_(err), request_headers_(std::move(request_headers)), ssl_error_(ssl_error), ssl_backend_error_(ssl_backend_error) {} int ssl_error() const { return ssl_error_; } unsigned long ssl_backend_error() const { return ssl_backend_error_; } private: int ssl_error_ = 0; unsigned long ssl_backend_error_ = 0; #endif #ifdef CPPHTTPLIB_OPENSSL_SUPPORT public: [[deprecated("Use ssl_backend_error() instead")]] unsigned long ssl_openssl_error() const { return ssl_backend_error_; } #endif }; struct ClientConnection { socket_t sock = INVALID_SOCKET; bool is_open() const { return sock != INVALID_SOCKET; } ClientConnection() = default; ~ClientConnection(); ClientConnection(const ClientConnection &) = delete; ClientConnection &operator=(const ClientConnection &) = delete; ClientConnection(ClientConnection &&other) noexcept : sock(other.sock) #ifdef CPPHTTPLIB_SSL_ENABLED , session(other.session) #endif { other.sock = INVALID_SOCKET; #ifdef CPPHTTPLIB_SSL_ENABLED other.session = nullptr; #endif } ClientConnection &operator=(ClientConnection &&other) noexcept { if (this != &other) { sock = other.sock; other.sock = INVALID_SOCKET; #ifdef CPPHTTPLIB_SSL_ENABLED session = other.session; other.session = nullptr; #endif } return *this; } #ifdef CPPHTTPLIB_SSL_ENABLED tls::session_t session = nullptr; #endif }; namespace detail { struct ChunkedDecoder; struct BodyReader { Stream *stream = nullptr; bool has_content_length = false; size_t content_length = 0; size_t payload_max_length = CPPHTTPLIB_PAYLOAD_MAX_LENGTH; size_t bytes_read = 0; bool chunked = false; bool eof = false; std::unique_ptr chunked_decoder; Error last_error = Error::Success; ssize_t read(char *buf, size_t len); bool has_error() const { return last_error != Error::Success; } }; inline ssize_t read_body_content(Stream *stream, BodyReader &br, char *buf, size_t len) { (void)stream; return br.read(buf, len); } class decompressor; } // namespace detail class ClientImpl { public: explicit ClientImpl(const std::string &host); explicit ClientImpl(const std::string &host, int port); explicit ClientImpl(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path); virtual ~ClientImpl(); virtual bool is_valid() const; struct StreamHandle { std::unique_ptr response; Error error = Error::Success; StreamHandle() = default; StreamHandle(const StreamHandle &) = delete; StreamHandle &operator=(const StreamHandle &) = delete; StreamHandle(StreamHandle &&) = default; StreamHandle &operator=(StreamHandle &&) = default; ~StreamHandle() = default; bool is_valid() const { return response != nullptr && error == Error::Success; } ssize_t read(char *buf, size_t len); void parse_trailers_if_needed(); Error get_read_error() const { return body_reader_.last_error; } bool has_read_error() const { return body_reader_.has_error(); } bool trailers_parsed_ = false; private: friend class ClientImpl; ssize_t read_with_decompression(char *buf, size_t len); std::unique_ptr connection_; std::unique_ptr socket_stream_; Stream *stream_ = nullptr; detail::BodyReader body_reader_; std::unique_ptr decompressor_; std::string decompress_buffer_; size_t decompress_offset_ = 0; size_t decompressed_bytes_read_ = 0; }; // clang-format off Result Get(const std::string &path, DownloadProgress progress = nullptr); Result Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Get(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr); Result Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Get(const std::string &path, const Params ¶ms, const Headers &headers, DownloadProgress progress = nullptr); Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Head(const std::string &path); Result Head(const std::string &path, const Headers &headers); Result Post(const std::string &path); Result Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); Result Post(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Post(const std::string &path, const Params ¶ms); Result Post(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers); Result Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); 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); Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers, const Params ¶ms); Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Put(const std::string &path); Result Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Put(const std::string &path, const Params ¶ms); Result Put(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers); Result Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); 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); Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers, const Params ¶ms); Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Patch(const std::string &path); Result Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); Result Patch(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Params ¶ms); Result Patch(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); 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); Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, const Params ¶ms); Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Delete(const std::string &path, DownloadProgress progress = nullptr); Result Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr); Result Delete(const std::string &path, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr); Result Delete(const std::string &path, const Params ¶ms, DownloadProgress progress = nullptr); Result Delete(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr); Result Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr); Result Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr); Result Delete(const std::string &path, const Headers &headers, const Params ¶ms, DownloadProgress progress = nullptr); Result Options(const std::string &path); Result Options(const std::string &path, const Headers &headers); // clang-format on // Streaming API: Open a stream for reading response body incrementally // Socket ownership is transferred to StreamHandle for true streaming // Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE, etc.) StreamHandle open_stream(const std::string &method, const std::string &path, const Params ¶ms = {}, const Headers &headers = {}, const std::string &body = {}, const std::string &content_type = {}); bool send(Request &req, Response &res, Error &error); Result send(const Request &req); void stop(); std::string host() const; int port() const; size_t is_socket_open() const; socket_t socket() const; void set_hostname_addr_map(std::map addr_map); void set_default_headers(Headers headers); void set_header_writer(std::function const &writer); void set_address_family(int family); void set_tcp_nodelay(bool on); void set_ipv6_v6only(bool on); void set_socket_options(SocketOptions socket_options); void set_connection_timeout(time_t sec, time_t usec = 0); template void set_connection_timeout(const std::chrono::duration &duration); void set_read_timeout(time_t sec, time_t usec = 0); template void set_read_timeout(const std::chrono::duration &duration); void set_write_timeout(time_t sec, time_t usec = 0); template void set_write_timeout(const std::chrono::duration &duration); void set_max_timeout(time_t msec); template void set_max_timeout(const std::chrono::duration &duration); void set_basic_auth(const std::string &username, const std::string &password); void set_bearer_token_auth(const std::string &token); void set_keep_alive(bool on); void set_follow_location(bool on); void set_path_encode(bool on); void set_compress(bool on); void set_decompress(bool on); void set_payload_max_length(size_t length); void set_interface(const std::string &intf); void set_proxy(const std::string &host, int port); void set_proxy_basic_auth(const std::string &username, const std::string &password); void set_proxy_bearer_token_auth(const std::string &token); void set_logger(Logger logger); void set_error_logger(ErrorLogger error_logger); protected: struct Socket { socket_t sock = INVALID_SOCKET; // For Mbed TLS compatibility: start_time for request timeout tracking std::chrono::time_point start_time_; bool is_open() const { return sock != INVALID_SOCKET; } #ifdef CPPHTTPLIB_SSL_ENABLED tls::session_t ssl = nullptr; #endif }; virtual bool create_and_connect_socket(Socket &socket, Error &error); virtual bool ensure_socket_connection(Socket &socket, Error &error); // All of: // shutdown_ssl // shutdown_socket // close_socket // should ONLY be called when socket_mutex_ is locked. // Also, shutdown_ssl and close_socket should also NOT be called concurrently // with a DIFFERENT thread sending requests using that socket. virtual void shutdown_ssl(Socket &socket, bool shutdown_gracefully); void shutdown_socket(Socket &socket) const; void close_socket(Socket &socket); bool process_request(Stream &strm, Request &req, Response &res, bool close_connection, Error &error); bool write_content_with_provider(Stream &strm, const Request &req, Error &error) const; void copy_settings(const ClientImpl &rhs); void output_log(const Request &req, const Response &res) const; void output_error_log(const Error &err, const Request *req) const; // Socket endpoint information const std::string host_; const int port_; // Current open socket Socket socket_; mutable std::mutex socket_mutex_; std::recursive_mutex request_mutex_; // These are all protected under socket_mutex size_t socket_requests_in_flight_ = 0; std::thread::id socket_requests_are_from_thread_ = std::thread::id(); bool socket_should_be_closed_when_request_is_done_ = false; // Hostname-IP map std::map addr_map_; // Default headers Headers default_headers_; // Header writer std::function header_writer_ = detail::write_headers; // Settings std::string client_cert_path_; std::string client_key_path_; time_t connection_timeout_sec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND; time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND; time_t read_timeout_sec_ = CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND; time_t read_timeout_usec_ = CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND; time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND; time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND; time_t max_timeout_msec_ = CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND; std::string basic_auth_username_; std::string basic_auth_password_; std::string bearer_token_auth_token_; bool keep_alive_ = false; bool follow_location_ = false; bool path_encode_ = true; int address_family_ = AF_UNSPEC; bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; bool ipv6_v6only_ = CPPHTTPLIB_IPV6_V6ONLY; SocketOptions socket_options_ = nullptr; bool compress_ = false; bool decompress_ = true; size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH; bool has_payload_max_length_ = false; std::string interface_; std::string proxy_host_; int proxy_port_ = -1; std::string proxy_basic_auth_username_; std::string proxy_basic_auth_password_; std::string proxy_bearer_token_auth_token_; mutable std::mutex logger_mutex_; Logger logger_; ErrorLogger error_logger_; private: bool send_(Request &req, Response &res, Error &error); Result send_(Request &&req); socket_t create_client_socket(Error &error) const; bool read_response_line(Stream &strm, const Request &req, Response &res, bool skip_100_continue = true) const; bool write_request(Stream &strm, Request &req, bool close_connection, Error &error, bool skip_body = false); bool write_request_body(Stream &strm, Request &req, Error &error); void prepare_default_headers(Request &r, bool for_stream, const std::string &ct); bool redirect(Request &req, Response &res, Error &error); bool create_redirect_client(const std::string &scheme, const std::string &host, int port, Request &req, Response &res, const std::string &path, const std::string &location, Error &error); template void setup_redirect_client(ClientType &client); bool handle_request(Stream &strm, Request &req, Response &res, bool close_connection, Error &error); std::unique_ptr send_with_content_provider_and_receiver( Request &req, const char *body, size_t content_length, ContentProvider content_provider, ContentProviderWithoutLength content_provider_without_length, const std::string &content_type, ContentReceiver content_receiver, Error &error); Result send_with_content_provider_and_receiver( const std::string &method, const std::string &path, const Headers &headers, const char *body, size_t content_length, ContentProvider content_provider, ContentProviderWithoutLength content_provider_without_length, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress); ContentProviderWithoutLength get_multipart_content_provider( const std::string &boundary, const UploadFormDataItems &items, const FormDataProviderItems &provider_items) const; virtual bool process_socket(const Socket &socket, std::chrono::time_point start_time, std::function callback); virtual bool is_ssl() const; void transfer_socket_ownership_to_handle(StreamHandle &handle); #ifdef CPPHTTPLIB_SSL_ENABLED public: void set_digest_auth(const std::string &username, const std::string &password); void set_proxy_digest_auth(const std::string &username, const std::string &password); void set_ca_cert_path(const std::string &ca_cert_file_path, const std::string &ca_cert_dir_path = std::string()); void enable_server_certificate_verification(bool enabled); void enable_server_hostname_verification(bool enabled); protected: std::string digest_auth_username_; std::string digest_auth_password_; std::string proxy_digest_auth_username_; std::string proxy_digest_auth_password_; std::string ca_cert_file_path_; std::string ca_cert_dir_path_; bool server_certificate_verification_ = true; bool server_hostname_verification_ = true; std::string ca_cert_pem_; // Store CA cert PEM for redirect transfer int last_ssl_error_ = 0; unsigned long last_backend_error_ = 0; #endif #ifdef CPPHTTPLIB_OPENSSL_SUPPORT public: [[deprecated("Use load_ca_cert_store() instead")]] void set_ca_cert_store(X509_STORE *ca_cert_store); [[deprecated("Use tls::create_ca_store() instead")]] X509_STORE *create_ca_cert_store(const char *ca_cert, std::size_t size) const; [[deprecated("Use set_server_certificate_verifier(VerifyCallback) instead")]] virtual void set_server_certificate_verifier( std::function verifier); #endif }; class Client { public: // Universal interface explicit Client(const std::string &scheme_host_port); explicit Client(const std::string &scheme_host_port, const std::string &client_cert_path, const std::string &client_key_path); // HTTP only interface explicit Client(const std::string &host, int port); explicit Client(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path); Client(Client &&) = default; Client &operator=(Client &&) = default; ~Client(); bool is_valid() const; // clang-format off Result Get(const std::string &path, DownloadProgress progress = nullptr); Result Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Get(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr); Result Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Get(const std::string &path, const Params ¶ms, const Headers &headers, DownloadProgress progress = nullptr); Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Head(const std::string &path); Result Head(const std::string &path, const Headers &headers); Result Post(const std::string &path); Result Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); Result Post(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Post(const std::string &path, const Params ¶ms); Result Post(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers); Result Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); 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); Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers, const Params ¶ms); Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Put(const std::string &path); Result Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Put(const std::string &path, const Params ¶ms); Result Put(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers); Result Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); 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); Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers, const Params ¶ms); Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Patch(const std::string &path); Result Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); Result Patch(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Params ¶ms); Result Patch(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers); Result Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); 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); Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, const Params ¶ms); Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Delete(const std::string &path, DownloadProgress progress = nullptr); Result Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr); Result Delete(const std::string &path, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr); Result Delete(const std::string &path, const Params ¶ms, DownloadProgress progress = nullptr); Result Delete(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr); Result Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr); Result Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr); Result Delete(const std::string &path, const Headers &headers, const Params ¶ms, DownloadProgress progress = nullptr); Result Options(const std::string &path); Result Options(const std::string &path, const Headers &headers); // clang-format on // Streaming API: Open a stream for reading response body incrementally // Socket ownership is transferred to StreamHandle for true streaming // Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE, etc.) ClientImpl::StreamHandle open_stream(const std::string &method, const std::string &path, const Params ¶ms = {}, const Headers &headers = {}, const std::string &body = {}, const std::string &content_type = {}); bool send(Request &req, Response &res, Error &error); Result send(const Request &req); void stop(); std::string host() const; int port() const; size_t is_socket_open() const; socket_t socket() const; void set_hostname_addr_map(std::map addr_map); void set_default_headers(Headers headers); void set_header_writer(std::function const &writer); void set_address_family(int family); void set_tcp_nodelay(bool on); void set_socket_options(SocketOptions socket_options); void set_connection_timeout(time_t sec, time_t usec = 0); template void set_connection_timeout(const std::chrono::duration &duration); void set_read_timeout(time_t sec, time_t usec = 0); template void set_read_timeout(const std::chrono::duration &duration); void set_write_timeout(time_t sec, time_t usec = 0); template void set_write_timeout(const std::chrono::duration &duration); void set_max_timeout(time_t msec); template void set_max_timeout(const std::chrono::duration &duration); void set_basic_auth(const std::string &username, const std::string &password); void set_bearer_token_auth(const std::string &token); void set_keep_alive(bool on); void set_follow_location(bool on); void set_path_encode(bool on); void set_url_encode(bool on); void set_compress(bool on); void set_decompress(bool on); void set_payload_max_length(size_t length); void set_interface(const std::string &intf); void set_proxy(const std::string &host, int port); void set_proxy_basic_auth(const std::string &username, const std::string &password); void set_proxy_bearer_token_auth(const std::string &token); void set_logger(Logger logger); void set_error_logger(ErrorLogger error_logger); private: std::unique_ptr cli_; #ifdef CPPHTTPLIB_SSL_ENABLED public: void set_digest_auth(const std::string &username, const std::string &password); void set_proxy_digest_auth(const std::string &username, const std::string &password); void enable_server_certificate_verification(bool enabled); void enable_server_hostname_verification(bool enabled); void set_ca_cert_path(const std::string &ca_cert_file_path, const std::string &ca_cert_dir_path = std::string()); void set_ca_cert_store(tls::ca_store_t ca_cert_store); void load_ca_cert_store(const char *ca_cert, std::size_t size); void set_server_certificate_verifier(tls::VerifyCallback verifier); void set_session_verifier( std::function verifier); tls::ctx_t tls_context() const; #if defined(_WIN32) && \ !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) void enable_windows_certificate_verification(bool enabled); #endif private: bool is_ssl_ = false; #endif #ifdef CPPHTTPLIB_OPENSSL_SUPPORT public: [[deprecated("Use tls_context() instead")]] SSL_CTX *ssl_context() const; [[deprecated("Use set_session_verifier(session_t) instead")]] void set_server_certificate_verifier( std::function verifier); [[deprecated("Use Result::ssl_backend_error() instead")]] long get_verify_result() const; #endif }; #ifdef CPPHTTPLIB_SSL_ENABLED class SSLServer : public Server { public: SSLServer(const char *cert_path, const char *private_key_path, const char *client_ca_cert_file_path = nullptr, const char *client_ca_cert_dir_path = nullptr, const char *private_key_password = nullptr); struct PemMemory { const char *cert_pem; size_t cert_pem_len; const char *key_pem; size_t key_pem_len; const char *client_ca_pem; size_t client_ca_pem_len; const char *private_key_password; }; explicit SSLServer(const PemMemory &pem); // The callback receives the ctx_t handle which can be cast to the // appropriate backend type (SSL_CTX* for OpenSSL, // tls::impl::MbedTlsContext* for Mbed TLS) explicit SSLServer(const tls::ContextSetupCallback &setup_callback); ~SSLServer() override; bool is_valid() const override; bool update_certs_pem(const char *cert_pem, const char *key_pem, const char *client_ca_pem = nullptr, const char *password = nullptr); tls::ctx_t tls_context() const { return ctx_; } int ssl_last_error() const { return last_ssl_error_; } private: bool process_and_close_socket(socket_t sock) override; tls::ctx_t ctx_ = nullptr; std::mutex ctx_mutex_; int last_ssl_error_ = 0; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT public: [[deprecated("Use SSLServer(PemMemory) or " "SSLServer(ContextSetupCallback) instead")]] SSLServer(X509 *cert, EVP_PKEY *private_key, X509_STORE *client_ca_cert_store = nullptr); [[deprecated("Use SSLServer(ContextSetupCallback) instead")]] SSLServer( const std::function &setup_ssl_ctx_callback); [[deprecated("Use tls_context() instead")]] SSL_CTX *ssl_context() const; [[deprecated("Use update_certs_pem() instead")]] void update_certs(X509 *cert, EVP_PKEY *private_key, X509_STORE *client_ca_cert_store = nullptr); #endif }; class SSLClient final : public ClientImpl { public: explicit SSLClient(const std::string &host); explicit SSLClient(const std::string &host, int port); explicit SSLClient(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path, const std::string &private_key_password = std::string()); struct PemMemory { const char *cert_pem; size_t cert_pem_len; const char *key_pem; size_t key_pem_len; const char *private_key_password; }; explicit SSLClient(const std::string &host, int port, const PemMemory &pem); ~SSLClient() override; bool is_valid() const override; void set_ca_cert_store(tls::ca_store_t ca_cert_store); void load_ca_cert_store(const char *ca_cert, std::size_t size); void set_server_certificate_verifier(tls::VerifyCallback verifier); // Post-handshake session verifier (backend-independent) void set_session_verifier( std::function verifier); tls::ctx_t tls_context() const { return ctx_; } #if defined(_WIN32) && \ !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) void enable_windows_certificate_verification(bool enabled); #endif private: bool create_and_connect_socket(Socket &socket, Error &error) override; bool ensure_socket_connection(Socket &socket, Error &error) override; void shutdown_ssl(Socket &socket, bool shutdown_gracefully) override; void shutdown_ssl_impl(Socket &socket, bool shutdown_gracefully); bool process_socket(const Socket &socket, std::chrono::time_point start_time, std::function callback) override; bool is_ssl() const override; bool connect_with_proxy( Socket &sock, std::chrono::time_point start_time, Response &res, bool &success, Error &error); bool initialize_ssl(Socket &socket, Error &error); bool load_certs(); tls::ctx_t ctx_ = nullptr; std::mutex ctx_mutex_; std::once_flag initialize_cert_; long verify_result_ = 0; std::function session_verifier_; #if defined(_WIN32) && \ !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) bool enable_windows_cert_verification_ = true; #endif friend class ClientImpl; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT public: [[deprecated("Use SSLClient(host, port, PemMemory) instead")]] explicit SSLClient(const std::string &host, int port, X509 *client_cert, EVP_PKEY *client_key, const std::string &private_key_password = std::string()); [[deprecated("Use Result::ssl_backend_error() instead")]] long get_verify_result() const; [[deprecated("Use tls_context() instead")]] SSL_CTX *ssl_context() const; [[deprecated("Use set_session_verifier(session_t) instead")]] void set_server_certificate_verifier( std::function verifier) override; private: bool verify_host(X509 *server_cert) const; bool verify_host_with_subject_alt_name(X509 *server_cert) const; bool verify_host_with_common_name(X509 *server_cert) const; #endif }; #endif // CPPHTTPLIB_SSL_ENABLED namespace detail { template inline void duration_to_sec_and_usec(const T &duration, U callback) { auto sec = std::chrono::duration_cast(duration).count(); auto usec = std::chrono::duration_cast( duration - std::chrono::seconds(sec)) .count(); callback(static_cast(sec), static_cast(usec)); } template inline constexpr size_t str_len(const char (&)[N]) { return N - 1; } inline bool is_numeric(const std::string &str) { return !str.empty() && std::all_of(str.cbegin(), str.cend(), [](unsigned char c) { return std::isdigit(c); }); } inline size_t get_header_value_u64(const Headers &headers, const std::string &key, size_t def, size_t id, bool &is_invalid_value) { is_invalid_value = false; auto rng = headers.equal_range(key); auto it = rng.first; std::advance(it, static_cast(id)); if (it != rng.second) { if (is_numeric(it->second)) { return std::strtoull(it->second.data(), nullptr, 10); } else { is_invalid_value = true; } } return def; } inline size_t get_header_value_u64(const Headers &headers, const std::string &key, size_t def, size_t id) { auto dummy = false; return get_header_value_u64(headers, key, def, id, dummy); } } // namespace detail template inline Server & Server::set_read_timeout(const std::chrono::duration &duration) { detail::duration_to_sec_and_usec( duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); }); return *this; } template inline Server & Server::set_write_timeout(const std::chrono::duration &duration) { detail::duration_to_sec_and_usec( duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); }); return *this; } template inline Server & Server::set_idle_interval(const std::chrono::duration &duration) { detail::duration_to_sec_and_usec( duration, [&](time_t sec, time_t usec) { set_idle_interval(sec, usec); }); return *this; } template inline void ClientImpl::set_connection_timeout( const std::chrono::duration &duration) { detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t usec) { set_connection_timeout(sec, usec); }); } template inline void ClientImpl::set_read_timeout( const std::chrono::duration &duration) { detail::duration_to_sec_and_usec( duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); }); } template inline void ClientImpl::set_write_timeout( const std::chrono::duration &duration) { detail::duration_to_sec_and_usec( duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); }); } template inline void ClientImpl::set_max_timeout( const std::chrono::duration &duration) { auto msec = std::chrono::duration_cast(duration).count(); set_max_timeout(msec); } template inline void Client::set_connection_timeout( const std::chrono::duration &duration) { cli_->set_connection_timeout(duration); } template inline void Client::set_read_timeout(const std::chrono::duration &duration) { cli_->set_read_timeout(duration); } template inline void Client::set_write_timeout(const std::chrono::duration &duration) { cli_->set_write_timeout(duration); } inline void Client::set_max_timeout(time_t msec) { cli_->set_max_timeout(msec); } template inline void Client::set_max_timeout(const std::chrono::duration &duration) { cli_->set_max_timeout(duration); } /* * Forward declarations and types that will be part of the .h file if split into * .h + .cc. */ std::string hosted_at(const std::string &hostname); void hosted_at(const std::string &hostname, std::vector &addrs); // JavaScript-style URL encoding/decoding functions std::string encode_uri_component(const std::string &value); std::string encode_uri(const std::string &value); std::string decode_uri_component(const std::string &value); std::string decode_uri(const std::string &value); // RFC 3986 compliant URL component encoding/decoding functions std::string encode_path_component(const std::string &component); std::string decode_path_component(const std::string &component); std::string encode_query_component(const std::string &component, bool space_as_plus = true); std::string decode_query_component(const std::string &component, bool plus_as_space = true); std::string append_query_params(const std::string &path, const Params ¶ms); std::pair make_range_header(const Ranges &ranges); std::pair make_basic_authentication_header(const std::string &username, const std::string &password, bool is_proxy = false); namespace detail { #if defined(_WIN32) inline std::wstring u8string_to_wstring(const char *s) { if (!s) { return std::wstring(); } auto len = static_cast(strlen(s)); if (!len) { return std::wstring(); } auto wlen = ::MultiByteToWideChar(CP_UTF8, 0, s, len, nullptr, 0); if (!wlen) { return std::wstring(); } std::wstring ws; ws.resize(wlen); wlen = ::MultiByteToWideChar( CP_UTF8, 0, s, len, const_cast(reinterpret_cast(ws.data())), wlen); if (wlen != static_cast(ws.size())) { ws.clear(); } return ws; } #endif struct FileStat { FileStat(const std::string &path); bool is_file() const; bool is_dir() const; time_t mtime() const; size_t size() const; private: #if defined(_WIN32) struct _stat st_; #else struct stat st_; #endif int ret_ = -1; }; std::string make_host_and_port_string(const std::string &host, int port, bool is_ssl); std::string trim_copy(const std::string &s); void divide( const char *data, std::size_t size, char d, std::function fn); void divide( const std::string &str, char d, std::function fn); void split(const char *b, const char *e, char d, std::function fn); void split(const char *b, const char *e, char d, size_t m, std::function fn); bool process_client_socket( socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec, std::chrono::time_point start_time, std::function callback); socket_t create_client_socket(const std::string &host, const std::string &ip, int port, int address_family, bool tcp_nodelay, bool ipv6_v6only, SocketOptions socket_options, time_t connection_timeout_sec, time_t connection_timeout_usec, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, const std::string &intf, Error &error); const char *get_header_value(const Headers &headers, const std::string &key, const char *def, size_t id); std::string params_to_query_str(const Params ¶ms); void parse_query_text(const char *data, std::size_t size, Params ¶ms); void parse_query_text(const std::string &s, Params ¶ms); bool parse_multipart_boundary(const std::string &content_type, std::string &boundary); bool parse_range_header(const std::string &s, Ranges &ranges); bool parse_accept_header(const std::string &s, std::vector &content_types); int close_socket(socket_t sock); ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags); ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags); enum class EncodingType { None = 0, Gzip, Brotli, Zstd }; EncodingType encoding_type(const Request &req, const Response &res); class BufferStream final : public Stream { public: BufferStream() = default; ~BufferStream() override = default; bool is_readable() const override; bool wait_readable() const override; bool wait_writable() const override; ssize_t read(char *ptr, size_t size) override; ssize_t write(const char *ptr, size_t size) override; void get_remote_ip_and_port(std::string &ip, int &port) const override; void get_local_ip_and_port(std::string &ip, int &port) const override; socket_t socket() const override; time_t duration() const override; const std::string &get_buffer() const; private: std::string buffer; size_t position = 0; }; class compressor { public: virtual ~compressor() = default; typedef std::function Callback; virtual bool compress(const char *data, size_t data_length, bool last, Callback callback) = 0; }; class decompressor { public: virtual ~decompressor() = default; virtual bool is_valid() const = 0; typedef std::function Callback; virtual bool decompress(const char *data, size_t data_length, Callback callback) = 0; }; class nocompressor final : public compressor { public: ~nocompressor() override = default; bool compress(const char *data, size_t data_length, bool /*last*/, Callback callback) override; }; #ifdef CPPHTTPLIB_ZLIB_SUPPORT class gzip_compressor final : public compressor { public: gzip_compressor(); ~gzip_compressor() override; bool compress(const char *data, size_t data_length, bool last, Callback callback) override; private: bool is_valid_ = false; z_stream strm_; }; class gzip_decompressor final : public decompressor { public: gzip_decompressor(); ~gzip_decompressor() override; bool is_valid() const override; bool decompress(const char *data, size_t data_length, Callback callback) override; private: bool is_valid_ = false; z_stream strm_; }; #endif #ifdef CPPHTTPLIB_BROTLI_SUPPORT class brotli_compressor final : public compressor { public: brotli_compressor(); ~brotli_compressor(); bool compress(const char *data, size_t data_length, bool last, Callback callback) override; private: BrotliEncoderState *state_ = nullptr; }; class brotli_decompressor final : public decompressor { public: brotli_decompressor(); ~brotli_decompressor(); bool is_valid() const override; bool decompress(const char *data, size_t data_length, Callback callback) override; private: BrotliDecoderResult decoder_r; BrotliDecoderState *decoder_s = nullptr; }; #endif #ifdef CPPHTTPLIB_ZSTD_SUPPORT class zstd_compressor : public compressor { public: zstd_compressor(); ~zstd_compressor(); bool compress(const char *data, size_t data_length, bool last, Callback callback) override; private: ZSTD_CCtx *ctx_ = nullptr; }; class zstd_decompressor : public decompressor { public: zstd_decompressor(); ~zstd_decompressor(); bool is_valid() const override; bool decompress(const char *data, size_t data_length, Callback callback) override; private: ZSTD_DCtx *ctx_ = nullptr; }; #endif // NOTE: until the read size reaches `fixed_buffer_size`, use `fixed_buffer` // to store data. The call can set memory on stack for performance. class stream_line_reader { public: stream_line_reader(Stream &strm, char *fixed_buffer, size_t fixed_buffer_size); const char *ptr() const; size_t size() const; bool end_with_crlf() const; bool getline(); private: void append(char c); Stream &strm_; char *fixed_buffer_; const size_t fixed_buffer_size_; size_t fixed_buffer_used_size_ = 0; std::string growable_buffer_; }; bool parse_trailers(stream_line_reader &line_reader, Headers &dest, const Headers &src_headers); struct ChunkedDecoder { Stream &strm; size_t chunk_remaining = 0; bool finished = false; char line_buf[64]; size_t last_chunk_total = 0; size_t last_chunk_offset = 0; explicit ChunkedDecoder(Stream &s); ssize_t read_payload(char *buf, size_t len, size_t &out_chunk_offset, size_t &out_chunk_total); bool parse_trailers_into(Headers &dest, const Headers &src_headers); }; class mmap { public: mmap(const char *path); ~mmap(); bool open(const char *path); void close(); bool is_open() const; size_t size() const; const char *data() const; private: #if defined(_WIN32) HANDLE hFile_ = NULL; HANDLE hMapping_ = NULL; #else int fd_ = -1; #endif size_t size_ = 0; void *addr_ = nullptr; bool is_open_empty_file = false; }; // NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5 namespace fields { bool is_token_char(char c); bool is_token(const std::string &s); bool is_field_name(const std::string &s); bool is_vchar(char c); bool is_obs_text(char c); bool is_field_vchar(char c); bool is_field_content(const std::string &s); bool is_field_value(const std::string &s); } // namespace fields } // namespace detail /* * TLS Abstraction Layer Declarations */ #ifdef CPPHTTPLIB_SSL_ENABLED // TLS abstraction layer - backend-specific type declarations #ifdef CPPHTTPLIB_MBEDTLS_SUPPORT namespace tls { namespace impl { // Mbed TLS context wrapper (holds config, entropy, DRBG, CA chain, own // cert/key). This struct is accessible via tls::impl for use in SSL context // setup callbacks (cast ctx_t to tls::impl::MbedTlsContext*). struct MbedTlsContext { mbedtls_ssl_config conf; mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context ctr_drbg; mbedtls_x509_crt ca_chain; mbedtls_x509_crt own_cert; mbedtls_pk_context own_key; bool is_server = false; bool verify_client = false; bool has_verify_callback = false; MbedTlsContext(); ~MbedTlsContext(); MbedTlsContext(const MbedTlsContext &) = delete; MbedTlsContext &operator=(const MbedTlsContext &) = delete; }; } // namespace impl } // namespace tls #endif #endif // CPPHTTPLIB_SSL_ENABLED namespace stream { class Result { public: Result(); explicit Result(ClientImpl::StreamHandle &&handle, size_t chunk_size = 8192); Result(Result &&other) noexcept; Result &operator=(Result &&other) noexcept; Result(const Result &) = delete; Result &operator=(const Result &) = delete; // Response info bool is_valid() const; explicit operator bool() const; int status() const; const Headers &headers() const; std::string get_header_value(const std::string &key, const char *def = "") const; bool has_header(const std::string &key) const; Error error() const; Error read_error() const; bool has_read_error() const; // Stream reading bool next(); const char *data() const; size_t size() const; std::string read_all(); private: ClientImpl::StreamHandle handle_; std::string buffer_; size_t current_size_ = 0; size_t chunk_size_; bool finished_ = false; }; // GET template inline Result Get(ClientType &cli, const std::string &path, size_t chunk_size = 8192) { return Result{cli.open_stream("GET", path), chunk_size}; } template inline Result Get(ClientType &cli, const std::string &path, const Headers &headers, size_t chunk_size = 8192) { return Result{cli.open_stream("GET", path, {}, headers), chunk_size}; } template inline Result Get(ClientType &cli, const std::string &path, const Params ¶ms, size_t chunk_size = 8192) { return Result{cli.open_stream("GET", path, params), chunk_size}; } template inline Result Get(ClientType &cli, const std::string &path, const Params ¶ms, const Headers &headers, size_t chunk_size = 8192) { return Result{cli.open_stream("GET", path, params, headers), chunk_size}; } // POST template inline Result Post(ClientType &cli, const std::string &path, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{cli.open_stream("POST", path, {}, {}, body, content_type), chunk_size}; } template inline Result Post(ClientType &cli, const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{cli.open_stream("POST", path, {}, headers, body, content_type), chunk_size}; } template inline Result Post(ClientType &cli, const std::string &path, const Params ¶ms, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{cli.open_stream("POST", path, params, {}, body, content_type), chunk_size}; } template inline Result Post(ClientType &cli, const std::string &path, const Params ¶ms, const Headers &headers, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{ cli.open_stream("POST", path, params, headers, body, content_type), chunk_size}; } // PUT template inline Result Put(ClientType &cli, const std::string &path, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{cli.open_stream("PUT", path, {}, {}, body, content_type), chunk_size}; } template inline Result Put(ClientType &cli, const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{cli.open_stream("PUT", path, {}, headers, body, content_type), chunk_size}; } template inline Result Put(ClientType &cli, const std::string &path, const Params ¶ms, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{cli.open_stream("PUT", path, params, {}, body, content_type), chunk_size}; } template inline Result Put(ClientType &cli, const std::string &path, const Params ¶ms, const Headers &headers, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{ cli.open_stream("PUT", path, params, headers, body, content_type), chunk_size}; } // PATCH template inline Result Patch(ClientType &cli, const std::string &path, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{cli.open_stream("PATCH", path, {}, {}, body, content_type), chunk_size}; } template inline Result Patch(ClientType &cli, const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{cli.open_stream("PATCH", path, {}, headers, body, content_type), chunk_size}; } template inline Result Patch(ClientType &cli, const std::string &path, const Params ¶ms, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{cli.open_stream("PATCH", path, params, {}, body, content_type), chunk_size}; } template inline Result Patch(ClientType &cli, const std::string &path, const Params ¶ms, const Headers &headers, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{ cli.open_stream("PATCH", path, params, headers, body, content_type), chunk_size}; } // DELETE template inline Result Delete(ClientType &cli, const std::string &path, size_t chunk_size = 8192) { return Result{cli.open_stream("DELETE", path), chunk_size}; } template inline Result Delete(ClientType &cli, const std::string &path, const Headers &headers, size_t chunk_size = 8192) { return Result{cli.open_stream("DELETE", path, {}, headers), chunk_size}; } template inline Result Delete(ClientType &cli, const std::string &path, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{cli.open_stream("DELETE", path, {}, {}, body, content_type), chunk_size}; } template inline Result Delete(ClientType &cli, const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{ cli.open_stream("DELETE", path, {}, headers, body, content_type), chunk_size}; } template inline Result Delete(ClientType &cli, const std::string &path, const Params ¶ms, size_t chunk_size = 8192) { return Result{cli.open_stream("DELETE", path, params), chunk_size}; } template inline Result Delete(ClientType &cli, const std::string &path, const Params ¶ms, const Headers &headers, size_t chunk_size = 8192) { return Result{cli.open_stream("DELETE", path, params, headers), chunk_size}; } template inline Result Delete(ClientType &cli, const std::string &path, const Params ¶ms, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{cli.open_stream("DELETE", path, params, {}, body, content_type), chunk_size}; } template inline Result Delete(ClientType &cli, const std::string &path, const Params ¶ms, const Headers &headers, const std::string &body, const std::string &content_type, size_t chunk_size = 8192) { return Result{ cli.open_stream("DELETE", path, params, headers, body, content_type), chunk_size}; } // HEAD template inline Result Head(ClientType &cli, const std::string &path, size_t chunk_size = 8192) { return Result{cli.open_stream("HEAD", path), chunk_size}; } template inline Result Head(ClientType &cli, const std::string &path, const Headers &headers, size_t chunk_size = 8192) { return Result{cli.open_stream("HEAD", path, {}, headers), chunk_size}; } template inline Result Head(ClientType &cli, const std::string &path, const Params ¶ms, size_t chunk_size = 8192) { return Result{cli.open_stream("HEAD", path, params), chunk_size}; } template inline Result Head(ClientType &cli, const std::string &path, const Params ¶ms, const Headers &headers, size_t chunk_size = 8192) { return Result{cli.open_stream("HEAD", path, params, headers), chunk_size}; } // OPTIONS template inline Result Options(ClientType &cli, const std::string &path, size_t chunk_size = 8192) { return Result{cli.open_stream("OPTIONS", path), chunk_size}; } template inline Result Options(ClientType &cli, const std::string &path, const Headers &headers, size_t chunk_size = 8192) { return Result{cli.open_stream("OPTIONS", path, {}, headers), chunk_size}; } template inline Result Options(ClientType &cli, const std::string &path, const Params ¶ms, size_t chunk_size = 8192) { return Result{cli.open_stream("OPTIONS", path, params), chunk_size}; } template inline Result Options(ClientType &cli, const std::string &path, const Params ¶ms, const Headers &headers, size_t chunk_size = 8192) { return Result{cli.open_stream("OPTIONS", path, params, headers), chunk_size}; } } // namespace stream namespace sse { struct SSEMessage { std::string event; // Event type (default: "message") std::string data; // Event payload std::string id; // Event ID for Last-Event-ID header SSEMessage(); void clear(); }; class SSEClient { public: using MessageHandler = std::function; using ErrorHandler = std::function; using OpenHandler = std::function; SSEClient(Client &client, const std::string &path); SSEClient(Client &client, const std::string &path, const Headers &headers); ~SSEClient(); SSEClient(const SSEClient &) = delete; SSEClient &operator=(const SSEClient &) = delete; // Event handlers SSEClient &on_message(MessageHandler handler); SSEClient &on_event(const std::string &type, MessageHandler handler); SSEClient &on_open(OpenHandler handler); SSEClient &on_error(ErrorHandler handler); SSEClient &set_reconnect_interval(int ms); SSEClient &set_max_reconnect_attempts(int n); // State accessors bool is_connected() const; const std::string &last_event_id() const; // Blocking start - runs event loop with auto-reconnect void start(); // Non-blocking start - runs in background thread void start_async(); // Stop the client (thread-safe) void stop(); private: bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms); void run_event_loop(); void dispatch_event(const SSEMessage &msg); bool should_reconnect(int count) const; void wait_for_reconnect(); // Client and path Client &client_; std::string path_; Headers headers_; // Callbacks MessageHandler on_message_; std::map event_handlers_; OpenHandler on_open_; ErrorHandler on_error_; // Configuration int reconnect_interval_ms_ = 3000; int max_reconnect_attempts_ = 0; // 0 = unlimited // State std::atomic running_{false}; std::atomic connected_{false}; std::string last_event_id_; // Async support std::thread async_thread_; }; } // namespace sse // ---------------------------------------------------------------------------- /* * Implementation that will be part of the .cc file if split into .h + .cc. */ namespace stream { // stream::Result implementations inline Result::Result() : chunk_size_(8192) {} inline Result::Result(ClientImpl::StreamHandle &&handle, size_t chunk_size) : handle_(std::move(handle)), chunk_size_(chunk_size) {} inline Result::Result(Result &&other) noexcept : handle_(std::move(other.handle_)), buffer_(std::move(other.buffer_)), current_size_(other.current_size_), chunk_size_(other.chunk_size_), finished_(other.finished_) { other.current_size_ = 0; other.finished_ = true; } inline Result &Result::operator=(Result &&other) noexcept { if (this != &other) { handle_ = std::move(other.handle_); buffer_ = std::move(other.buffer_); current_size_ = other.current_size_; chunk_size_ = other.chunk_size_; finished_ = other.finished_; other.current_size_ = 0; other.finished_ = true; } return *this; } inline bool Result::is_valid() const { return handle_.is_valid(); } inline Result::operator bool() const { return is_valid(); } inline int Result::status() const { return handle_.response ? handle_.response->status : -1; } inline const Headers &Result::headers() const { static const Headers empty_headers; return handle_.response ? handle_.response->headers : empty_headers; } inline std::string Result::get_header_value(const std::string &key, const char *def) const { return handle_.response ? handle_.response->get_header_value(key, def) : def; } inline bool Result::has_header(const std::string &key) const { return handle_.response ? handle_.response->has_header(key) : false; } inline Error Result::error() const { return handle_.error; } inline Error Result::read_error() const { return handle_.get_read_error(); } inline bool Result::has_read_error() const { return handle_.has_read_error(); } inline bool Result::next() { if (!handle_.is_valid() || finished_) { return false; } if (buffer_.size() < chunk_size_) { buffer_.resize(chunk_size_); } ssize_t n = handle_.read(&buffer_[0], chunk_size_); if (n > 0) { current_size_ = static_cast(n); return true; } current_size_ = 0; finished_ = true; return false; } inline const char *Result::data() const { return buffer_.data(); } inline size_t Result::size() const { return current_size_; } inline std::string Result::read_all() { std::string result; while (next()) { result.append(data(), size()); } return result; } } // namespace stream namespace sse { // SSEMessage implementations inline SSEMessage::SSEMessage() : event("message") {} inline void SSEMessage::clear() { event = "message"; data.clear(); id.clear(); } // SSEClient implementations inline SSEClient::SSEClient(Client &client, const std::string &path) : client_(client), path_(path) {} inline SSEClient::SSEClient(Client &client, const std::string &path, const Headers &headers) : client_(client), path_(path), headers_(headers) {} inline SSEClient::~SSEClient() { stop(); } inline SSEClient &SSEClient::on_message(MessageHandler handler) { on_message_ = std::move(handler); return *this; } inline SSEClient &SSEClient::on_event(const std::string &type, MessageHandler handler) { event_handlers_[type] = std::move(handler); return *this; } inline SSEClient &SSEClient::on_open(OpenHandler handler) { on_open_ = std::move(handler); return *this; } inline SSEClient &SSEClient::on_error(ErrorHandler handler) { on_error_ = std::move(handler); return *this; } inline SSEClient &SSEClient::set_reconnect_interval(int ms) { reconnect_interval_ms_ = ms; return *this; } inline SSEClient &SSEClient::set_max_reconnect_attempts(int n) { max_reconnect_attempts_ = n; return *this; } inline bool SSEClient::is_connected() const { return connected_.load(); } inline const std::string &SSEClient::last_event_id() const { return last_event_id_; } inline void SSEClient::start() { running_.store(true); run_event_loop(); } inline void SSEClient::start_async() { running_.store(true); async_thread_ = std::thread([this]() { run_event_loop(); }); } inline void SSEClient::stop() { running_.store(false); client_.stop(); // Cancel any pending operations if (async_thread_.joinable()) { async_thread_.join(); } } inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms) { // Blank line signals end of event if (line.empty() || line == "\r") { return true; } // Lines starting with ':' are comments (ignored) if (!line.empty() && line[0] == ':') { return false; } // Find the colon separator auto colon_pos = line.find(':'); if (colon_pos == std::string::npos) { // Line with no colon is treated as field name with empty value return false; } auto field = line.substr(0, colon_pos); std::string value; // Value starts after colon, skip optional single space if (colon_pos + 1 < line.size()) { auto value_start = colon_pos + 1; if (line[value_start] == ' ') { value_start++; } value = line.substr(value_start); // Remove trailing \r if present if (!value.empty() && value.back() == '\r') { value.pop_back(); } } // Handle known fields if (field == "event") { msg.event = value; } else if (field == "data") { // Multiple data lines are concatenated with newlines if (!msg.data.empty()) { msg.data += "\n"; } msg.data += value; } else if (field == "id") { // Empty id is valid (clears the last event ID) msg.id = value; } else if (field == "retry") { // Parse retry interval in milliseconds { int v = 0; auto res = detail::from_chars(value.data(), value.data() + value.size(), v); if (res.ec == std::errc{}) { retry_ms = v; } } } // Unknown fields are ignored per SSE spec return false; } inline void SSEClient::run_event_loop() { auto reconnect_count = 0; while (running_.load()) { // Build headers, including Last-Event-ID if we have one auto request_headers = headers_; if (!last_event_id_.empty()) { request_headers.emplace("Last-Event-ID", last_event_id_); } // Open streaming connection auto result = stream::Get(client_, path_, request_headers); // Connection error handling if (!result) { connected_.store(false); if (on_error_) { on_error_(result.error()); } if (!should_reconnect(reconnect_count)) { break; } wait_for_reconnect(); reconnect_count++; continue; } if (result.status() != 200) { connected_.store(false); // For certain errors, don't reconnect if (result.status() == 204 || // No Content - server wants us to stop result.status() == 404 || // Not Found result.status() == 401 || // Unauthorized result.status() == 403) { // Forbidden if (on_error_) { on_error_(Error::Connection); } break; } if (on_error_) { on_error_(Error::Connection); } if (!should_reconnect(reconnect_count)) { break; } wait_for_reconnect(); reconnect_count++; continue; } // Connection successful connected_.store(true); reconnect_count = 0; if (on_open_) { on_open_(); } // Event receiving loop std::string buffer; SSEMessage current_msg; while (running_.load() && result.next()) { buffer.append(result.data(), result.size()); // Process complete lines in the buffer size_t line_start = 0; size_t newline_pos; while ((newline_pos = buffer.find('\n', line_start)) != std::string::npos) { auto line = buffer.substr(line_start, newline_pos - line_start); line_start = newline_pos + 1; // Parse the line and check if event is complete auto event_complete = parse_sse_line(line, current_msg, reconnect_interval_ms_); if (event_complete && !current_msg.data.empty()) { // Update last_event_id for reconnection if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; } // Dispatch event to appropriate handler dispatch_event(current_msg); current_msg.clear(); } } // Keep unprocessed data in buffer buffer.erase(0, line_start); } // Connection ended connected_.store(false); if (!running_.load()) { break; } // Check for read errors if (result.has_read_error()) { if (on_error_) { on_error_(result.read_error()); } } if (!should_reconnect(reconnect_count)) { break; } wait_for_reconnect(); reconnect_count++; } connected_.store(false); } inline void SSEClient::dispatch_event(const SSEMessage &msg) { // Check for specific event type handler first auto it = event_handlers_.find(msg.event); if (it != event_handlers_.end()) { it->second(msg); return; } // Fall back to generic message handler if (on_message_) { on_message_(msg); } } inline bool SSEClient::should_reconnect(int count) const { if (!running_.load()) { return false; } if (max_reconnect_attempts_ == 0) { return true; } // unlimited return count < max_reconnect_attempts_; } inline void SSEClient::wait_for_reconnect() { // Use small increments to check running_ flag frequently auto waited = 0; while (running_.load() && waited < reconnect_interval_ms_) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); waited += 100; } } } // namespace sse #ifdef CPPHTTPLIB_SSL_ENABLED /* * TLS abstraction layer - internal function declarations * These are implementation details and not part of the public API. */ namespace tls { // Client context ctx_t create_client_context(); void free_context(ctx_t ctx); bool set_min_version(ctx_t ctx, Version version); bool load_ca_pem(ctx_t ctx, const char *pem, size_t len); bool load_ca_file(ctx_t ctx, const char *file_path); bool load_ca_dir(ctx_t ctx, const char *dir_path); bool load_system_certs(ctx_t ctx); bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key, const char *password); bool set_client_cert_file(ctx_t ctx, const char *cert_path, const char *key_path, const char *password); // Server context ctx_t create_server_context(); bool set_server_cert_pem(ctx_t ctx, const char *cert, const char *key, const char *password); bool set_server_cert_file(ctx_t ctx, const char *cert_path, const char *key_path, const char *password); bool set_client_ca_file(ctx_t ctx, const char *ca_file, const char *ca_dir); void set_verify_client(ctx_t ctx, bool require); // Session management session_t create_session(ctx_t ctx, socket_t sock); void free_session(session_t session); bool set_sni(session_t session, const char *hostname); bool set_hostname(session_t session, const char *hostname); // Handshake (non-blocking capable) TlsError connect(session_t session); TlsError accept(session_t session); // Handshake with timeout (blocking until timeout) bool connect_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err); bool accept_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err); // I/O (non-blocking capable) ssize_t read(session_t session, void *buf, size_t len, TlsError &err); ssize_t write(session_t session, const void *buf, size_t len, TlsError &err); int pending(const_session_t session); void shutdown(session_t session, bool graceful); // Connection state bool is_peer_closed(session_t session, socket_t sock); // Certificate verification cert_t get_peer_cert(const_session_t session); void free_cert(cert_t cert); bool verify_hostname(cert_t cert, const char *hostname); uint64_t hostname_mismatch_code(); long get_verify_result(const_session_t session); // Certificate introspection std::string get_cert_subject_cn(cert_t cert); std::string get_cert_issuer_name(cert_t cert); bool get_cert_sans(cert_t cert, std::vector &sans); bool get_cert_validity(cert_t cert, time_t ¬_before, time_t ¬_after); std::string get_cert_serial(cert_t cert); bool get_cert_der(cert_t cert, std::vector &der); const char *get_sni(const_session_t session); // CA store management ca_store_t create_ca_store(const char *pem, size_t len); void free_ca_store(ca_store_t store); bool set_ca_store(ctx_t ctx, ca_store_t store); size_t get_ca_certs(ctx_t ctx, std::vector &certs); std::vector get_ca_names(ctx_t ctx); // Dynamic certificate update (for servers) bool update_server_cert(ctx_t ctx, const char *cert_pem, const char *key_pem, const char *password); bool update_server_client_ca(ctx_t ctx, const char *ca_pem); // Certificate verification callback bool set_verify_callback(ctx_t ctx, VerifyCallback callback); long get_verify_error(const_session_t session); std::string verify_error_string(long error_code); // TlsError information uint64_t peek_error(); uint64_t get_error(); std::string error_string(uint64_t code); } // namespace tls #endif // CPPHTTPLIB_SSL_ENABLED /* * Group 1: detail namespace - Non-SSL utilities */ namespace detail { inline bool set_socket_opt_impl(socket_t sock, int level, int optname, const void *optval, socklen_t optlen) { return setsockopt(sock, level, optname, #ifdef _WIN32 reinterpret_cast(optval), #else optval, #endif optlen) == 0; } inline bool set_socket_opt(socket_t sock, int level, int optname, int optval) { return set_socket_opt_impl(sock, level, optname, &optval, sizeof(optval)); } inline bool set_socket_opt_time(socket_t sock, int level, int optname, time_t sec, time_t usec) { #ifdef _WIN32 auto timeout = static_cast(sec * 1000 + usec / 1000); #else timeval timeout; timeout.tv_sec = static_cast(sec); timeout.tv_usec = static_cast(usec); #endif return set_socket_opt_impl(sock, level, optname, &timeout, sizeof(timeout)); } inline bool is_hex(char c, int &v) { if (isdigit(c)) { v = c - '0'; return true; } else if ('A' <= c && c <= 'F') { v = c - 'A' + 10; return true; } else if ('a' <= c && c <= 'f') { v = c - 'a' + 10; return true; } return false; } inline bool from_hex_to_i(const std::string &s, size_t i, size_t cnt, int &val) { if (i >= s.size()) { return false; } val = 0; for (; cnt; i++, cnt--) { if (!s[i]) { return false; } auto v = 0; if (is_hex(s[i], v)) { val = val * 16 + v; } else { return false; } } return true; } inline std::string from_i_to_hex(size_t n) { static const auto charset = "0123456789abcdef"; std::string ret; do { ret = charset[n & 15] + ret; n >>= 4; } while (n > 0); return ret; } inline std::string compute_etag(const FileStat &fs) { if (!fs.is_file()) { return std::string(); } // If mtime cannot be determined (negative value indicates an error // or sentinel), do not generate an ETag. Returning a neutral / fixed // value like 0 could collide with a real file that legitimately has // mtime == 0 (epoch) and lead to misleading validators. auto mtime_raw = fs.mtime(); if (mtime_raw < 0) { return std::string(); } auto mtime = static_cast(mtime_raw); auto size = fs.size(); return std::string("W/\"") + from_i_to_hex(mtime) + "-" + from_i_to_hex(size) + "\""; } // Format time_t as HTTP-date (RFC 9110 Section 5.6.7): "Sun, 06 Nov 1994 // 08:49:37 GMT" This implementation is defensive: it validates `mtime`, checks // return values from `gmtime_r`/`gmtime_s`, and ensures `strftime` succeeds. inline std::string file_mtime_to_http_date(time_t mtime) { if (mtime < 0) { return std::string(); } struct tm tm_buf; #ifdef _WIN32 if (gmtime_s(&tm_buf, &mtime) != 0) { return std::string(); } #else if (gmtime_r(&mtime, &tm_buf) == nullptr) { return std::string(); } #endif char buf[64]; if (strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", &tm_buf) == 0) { return std::string(); } return std::string(buf); } // Parse HTTP-date (RFC 9110 Section 5.6.7) to time_t. Returns -1 on failure. inline time_t parse_http_date(const std::string &date_str) { struct tm tm_buf; // Create a classic locale object once for all parsing attempts const std::locale classic_locale = std::locale::classic(); // Try to parse using std::get_time (C++11, cross-platform) auto try_parse = [&](const char *fmt) -> bool { std::istringstream ss(date_str); ss.imbue(classic_locale); memset(&tm_buf, 0, sizeof(tm_buf)); ss >> std::get_time(&tm_buf, fmt); return !ss.fail(); }; // RFC 9110 preferred format (HTTP-date): "Sun, 06 Nov 1994 08:49:37 GMT" if (!try_parse("%a, %d %b %Y %H:%M:%S")) { // RFC 850 format: "Sunday, 06-Nov-94 08:49:37 GMT" if (!try_parse("%A, %d-%b-%y %H:%M:%S")) { // asctime format: "Sun Nov 6 08:49:37 1994" if (!try_parse("%a %b %d %H:%M:%S %Y")) { return static_cast(-1); } } } #ifdef _WIN32 return _mkgmtime(&tm_buf); #elif defined _AIX return mktime(&tm_buf); #else return timegm(&tm_buf); #endif } inline bool is_weak_etag(const std::string &s) { // Check if the string is a weak ETag (starts with 'W/"') return s.size() > 3 && s[0] == 'W' && s[1] == '/' && s[2] == '"'; } inline bool is_strong_etag(const std::string &s) { // Check if the string is a strong ETag (starts and ends with '"', at least 2 // chars) return s.size() >= 2 && s[0] == '"' && s.back() == '"'; } inline size_t to_utf8(int code, char *buff) { if (code < 0x0080) { buff[0] = static_cast(code & 0x7F); return 1; } else if (code < 0x0800) { buff[0] = static_cast(0xC0 | ((code >> 6) & 0x1F)); buff[1] = static_cast(0x80 | (code & 0x3F)); return 2; } else if (code < 0xD800) { buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF)); buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); buff[2] = static_cast(0x80 | (code & 0x3F)); return 3; } else if (code < 0xE000) { // D800 - DFFF is invalid... return 0; } else if (code < 0x10000) { buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF)); buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); buff[2] = static_cast(0x80 | (code & 0x3F)); return 3; } else if (code < 0x110000) { buff[0] = static_cast(0xF0 | ((code >> 18) & 0x7)); buff[1] = static_cast(0x80 | ((code >> 12) & 0x3F)); buff[2] = static_cast(0x80 | ((code >> 6) & 0x3F)); buff[3] = static_cast(0x80 | (code & 0x3F)); return 4; } // NOTREACHED return 0; } // NOTE: This code came up with the following stackoverflow post: // https://stackoverflow.com/questions/180947/base64-decode-snippet-in-c inline std::string base64_encode(const std::string &in) { static const auto lookup = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; std::string out; out.reserve(in.size()); auto val = 0; auto valb = -6; for (auto c : in) { val = (val << 8) + static_cast(c); valb += 8; while (valb >= 0) { out.push_back(lookup[(val >> valb) & 0x3F]); valb -= 6; } } if (valb > -6) { out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]); } while (out.size() % 4) { out.push_back('='); } return out; } inline bool is_valid_path(const std::string &path) { size_t level = 0; size_t i = 0; // Skip slash while (i < path.size() && path[i] == '/') { i++; } while (i < path.size()) { // Read component auto beg = i; while (i < path.size() && path[i] != '/') { if (path[i] == '\0') { return false; } else if (path[i] == '\\') { return false; } i++; } auto len = i - beg; assert(len > 0); if (!path.compare(beg, len, ".")) { ; } else if (!path.compare(beg, len, "..")) { if (level == 0) { return false; } level--; } else { level++; } // Skip slash while (i < path.size() && path[i] == '/') { i++; } } return true; } inline FileStat::FileStat(const std::string &path) { #if defined(_WIN32) auto wpath = u8string_to_wstring(path.c_str()); ret_ = _wstat(wpath.c_str(), &st_); #else ret_ = stat(path.c_str(), &st_); #endif } inline bool FileStat::is_file() const { return ret_ >= 0 && S_ISREG(st_.st_mode); } inline bool FileStat::is_dir() const { return ret_ >= 0 && S_ISDIR(st_.st_mode); } inline time_t FileStat::mtime() const { return ret_ >= 0 ? static_cast(st_.st_mtime) : static_cast(-1); } inline size_t FileStat::size() const { return ret_ >= 0 ? static_cast(st_.st_size) : 0; } inline std::string encode_path(const std::string &s) { std::string result; result.reserve(s.size()); for (size_t i = 0; s[i]; i++) { switch (s[i]) { case ' ': result += "%20"; break; case '+': result += "%2B"; break; case '\r': result += "%0D"; break; case '\n': result += "%0A"; break; case '\'': result += "%27"; break; case ',': result += "%2C"; break; // case ':': result += "%3A"; break; // ok? probably... case ';': result += "%3B"; break; default: auto c = static_cast(s[i]); if (c >= 0x80) { result += '%'; char hex[4]; auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c); assert(len == 2); result.append(hex, static_cast(len)); } else { result += s[i]; } break; } } return result; } inline std::string file_extension(const std::string &path) { std::smatch m; thread_local auto re = std::regex("\\.([a-zA-Z0-9]+)$"); if (std::regex_search(path, m, re)) { return m[1].str(); } return std::string(); } inline bool is_space_or_tab(char c) { return c == ' ' || c == '\t'; } template inline bool parse_header(const char *beg, const char *end, T fn); template inline bool parse_header(const char *beg, const char *end, T fn) { // Skip trailing spaces and tabs. while (beg < end && is_space_or_tab(end[-1])) { end--; } auto p = beg; while (p < end && *p != ':') { p++; } auto name = std::string(beg, p); if (!detail::fields::is_field_name(name)) { return false; } if (p == end) { return false; } auto key_end = p; if (*p++ != ':') { return false; } while (p < end && is_space_or_tab(*p)) { p++; } if (p <= end) { auto key_len = key_end - beg; if (!key_len) { return false; } auto key = std::string(beg, key_end); auto val = std::string(p, end); if (!detail::fields::is_field_value(val)) { return false; } if (case_ignore::equal(key, "Location") || case_ignore::equal(key, "Referer")) { fn(key, val); } else { fn(key, decode_path_component(val)); } return true; } return false; } inline bool parse_trailers(stream_line_reader &line_reader, Headers &dest, const Headers &src_headers) { // NOTE: In RFC 9112, '7.1 Chunked Transfer Coding' mentions "The chunked // transfer coding is complete when a chunk with a chunk-size of zero is // received, possibly followed by a trailer section, and finally terminated by // an empty line". https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1 // // In '7.1.3. Decoding Chunked', however, the pseudo-code in the section // doesn't care for the existence of the final CRLF. In other words, it seems // to be ok whether the final CRLF exists or not in the chunked data. // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1.3 // // According to the reference code in RFC 9112, cpp-httplib now allows // chunked transfer coding data without the final CRLF. // RFC 7230 Section 4.1.2 - Headers prohibited in trailers thread_local case_ignore::unordered_set prohibited_trailers = { "transfer-encoding", "content-length", "host", "authorization", "www-authenticate", "proxy-authenticate", "proxy-authorization", "cookie", "set-cookie", "cache-control", "expect", "max-forwards", "pragma", "range", "te", "age", "expires", "date", "location", "retry-after", "vary", "warning", "content-encoding", "content-type", "content-range", "trailer"}; case_ignore::unordered_set declared_trailers; auto trailer_header = get_header_value(src_headers, "Trailer", "", 0); if (trailer_header && std::strlen(trailer_header)) { auto len = std::strlen(trailer_header); split(trailer_header, trailer_header + len, ',', [&](const char *b, const char *e) { const char *kbeg = b; const char *kend = e; while (kbeg < kend && (*kbeg == ' ' || *kbeg == '\t')) { ++kbeg; } while (kend > kbeg && (kend[-1] == ' ' || kend[-1] == '\t')) { --kend; } std::string key(kbeg, static_cast(kend - kbeg)); if (!key.empty() && prohibited_trailers.find(key) == prohibited_trailers.end()) { declared_trailers.insert(key); } }); } size_t trailer_header_count = 0; while (strcmp(line_reader.ptr(), "\r\n") != 0) { if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; } constexpr auto line_terminator_len = 2; auto line_beg = line_reader.ptr(); auto line_end = line_reader.ptr() + line_reader.size() - line_terminator_len; if (!parse_header(line_beg, line_end, [&](const std::string &key, const std::string &val) { if (declared_trailers.find(key) != declared_trailers.end()) { dest.emplace(key, val); trailer_header_count++; } })) { return false; } if (!line_reader.getline()) { return false; } } return true; } inline std::pair trim(const char *b, const char *e, size_t left, size_t right) { while (b + left < e && is_space_or_tab(b[left])) { left++; } while (right > 0 && is_space_or_tab(b[right - 1])) { right--; } return std::make_pair(left, right); } inline std::string trim_copy(const std::string &s) { auto r = trim(s.data(), s.data() + s.size(), 0, s.size()); return s.substr(r.first, r.second - r.first); } inline std::string trim_double_quotes_copy(const std::string &s) { if (s.length() >= 2 && s.front() == '"' && s.back() == '"') { return s.substr(1, s.size() - 2); } return s; } inline void divide(const char *data, std::size_t size, char d, std::function fn) { const auto it = std::find(data, data + size, d); const auto found = static_cast(it != data + size); const auto lhs_data = data; const auto lhs_size = static_cast(it - data); const auto rhs_data = it + found; const auto rhs_size = size - lhs_size - found; fn(lhs_data, lhs_size, rhs_data, rhs_size); } inline void divide(const std::string &str, char d, std::function fn) { divide(str.data(), str.size(), d, std::move(fn)); } inline void split(const char *b, const char *e, char d, std::function fn) { return split(b, e, d, (std::numeric_limits::max)(), std::move(fn)); } inline void split(const char *b, const char *e, char d, size_t m, std::function fn) { size_t i = 0; size_t beg = 0; size_t count = 1; while (e ? (b + i < e) : (b[i] != '\0')) { if (b[i] == d && count < m) { auto r = trim(b, e, beg, i); if (r.first < r.second) { fn(&b[r.first], &b[r.second]); } beg = i + 1; count++; } i++; } if (i) { auto r = trim(b, e, beg, i); if (r.first < r.second) { fn(&b[r.first], &b[r.second]); } } } inline bool split_find(const char *b, const char *e, char d, size_t m, std::function fn) { size_t i = 0; size_t beg = 0; size_t count = 1; while (e ? (b + i < e) : (b[i] != '\0')) { if (b[i] == d && count < m) { auto r = trim(b, e, beg, i); if (r.first < r.second) { auto found = fn(&b[r.first], &b[r.second]); if (found) { return true; } } beg = i + 1; count++; } i++; } if (i) { auto r = trim(b, e, beg, i); if (r.first < r.second) { auto found = fn(&b[r.first], &b[r.second]); if (found) { return true; } } } return false; } inline bool split_find(const char *b, const char *e, char d, std::function fn) { return split_find(b, e, d, (std::numeric_limits::max)(), std::move(fn)); } inline stream_line_reader::stream_line_reader(Stream &strm, char *fixed_buffer, size_t fixed_buffer_size) : strm_(strm), fixed_buffer_(fixed_buffer), fixed_buffer_size_(fixed_buffer_size) {} inline const char *stream_line_reader::ptr() const { if (growable_buffer_.empty()) { return fixed_buffer_; } else { return growable_buffer_.data(); } } inline size_t stream_line_reader::size() const { if (growable_buffer_.empty()) { return fixed_buffer_used_size_; } else { return growable_buffer_.size(); } } inline bool stream_line_reader::end_with_crlf() const { auto end = ptr() + size(); return size() >= 2 && end[-2] == '\r' && end[-1] == '\n'; } inline bool stream_line_reader::getline() { fixed_buffer_used_size_ = 0; growable_buffer_.clear(); #ifndef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR char prev_byte = 0; #endif for (size_t i = 0;; i++) { if (size() >= CPPHTTPLIB_MAX_LINE_LENGTH) { // Treat exceptionally long lines as an error to // prevent infinite loops/memory exhaustion return false; } char byte; auto n = strm_.read(&byte, 1); if (n < 0) { return false; } else if (n == 0) { if (i == 0) { return false; } else { break; } } append(byte); #ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR if (byte == '\n') { break; } #else if (prev_byte == '\r' && byte == '\n') { break; } prev_byte = byte; #endif } return true; } inline void stream_line_reader::append(char c) { if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) { fixed_buffer_[fixed_buffer_used_size_++] = c; fixed_buffer_[fixed_buffer_used_size_] = '\0'; } else { if (growable_buffer_.empty()) { assert(fixed_buffer_[fixed_buffer_used_size_] == '\0'); growable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_); } growable_buffer_ += c; } } inline mmap::mmap(const char *path) { open(path); } inline mmap::~mmap() { close(); } inline bool mmap::open(const char *path) { close(); #if defined(_WIN32) auto wpath = u8string_to_wstring(path); if (wpath.empty()) { return false; } hFile_ = ::CreateFile2(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ, OPEN_EXISTING, NULL); if (hFile_ == INVALID_HANDLE_VALUE) { return false; } LARGE_INTEGER size{}; if (!::GetFileSizeEx(hFile_, &size)) { return false; } // If the following line doesn't compile due to QuadPart, update Windows SDK. // See: // https://github.com/yhirose/cpp-httplib/issues/1903#issuecomment-2316520721 if (static_cast(size.QuadPart) > (std::numeric_limits::max)()) { // `size_t` might be 32-bits, on 32-bits Windows. return false; } size_ = static_cast(size.QuadPart); hMapping_ = ::CreateFileMappingFromApp(hFile_, NULL, PAGE_READONLY, size_, NULL); // Special treatment for an empty file... if (hMapping_ == NULL && size_ == 0) { close(); is_open_empty_file = true; return true; } if (hMapping_ == NULL) { close(); return false; } addr_ = ::MapViewOfFileFromApp(hMapping_, FILE_MAP_READ, 0, 0); if (addr_ == nullptr) { close(); return false; } #else fd_ = ::open(path, O_RDONLY); if (fd_ == -1) { return false; } struct stat sb; if (fstat(fd_, &sb) == -1) { close(); return false; } size_ = static_cast(sb.st_size); addr_ = ::mmap(NULL, size_, PROT_READ, MAP_PRIVATE, fd_, 0); // Special treatment for an empty file... if (addr_ == MAP_FAILED && size_ == 0) { close(); is_open_empty_file = true; return false; } #endif return true; } inline bool mmap::is_open() const { return is_open_empty_file ? true : addr_ != nullptr; } inline size_t mmap::size() const { return size_; } inline const char *mmap::data() const { return is_open_empty_file ? "" : static_cast(addr_); } inline void mmap::close() { #if defined(_WIN32) if (addr_) { ::UnmapViewOfFile(addr_); addr_ = nullptr; } if (hMapping_) { ::CloseHandle(hMapping_); hMapping_ = NULL; } if (hFile_ != INVALID_HANDLE_VALUE) { ::CloseHandle(hFile_); hFile_ = INVALID_HANDLE_VALUE; } is_open_empty_file = false; #else if (addr_ != nullptr) { munmap(addr_, size_); addr_ = nullptr; } if (fd_ != -1) { ::close(fd_); fd_ = -1; } #endif size_ = 0; } inline int close_socket(socket_t sock) { #ifdef _WIN32 return closesocket(sock); #else return close(sock); #endif } template inline ssize_t handle_EINTR(T fn) { ssize_t res = 0; while (true) { res = fn(); if (res < 0 && errno == EINTR) { std::this_thread::sleep_for(std::chrono::microseconds{1}); continue; } break; } return res; } inline ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags) { return handle_EINTR([&]() { return recv(sock, #ifdef _WIN32 static_cast(ptr), static_cast(size), #else ptr, size, #endif flags); }); } inline ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags) { return handle_EINTR([&]() { return send(sock, #ifdef _WIN32 static_cast(ptr), static_cast(size), #else ptr, size, #endif flags); }); } inline int poll_wrapper(struct pollfd *fds, nfds_t nfds, int timeout) { #ifdef _WIN32 return ::WSAPoll(fds, nfds, timeout); #else return ::poll(fds, nfds, timeout); #endif } template inline ssize_t select_impl(socket_t sock, time_t sec, time_t usec) { #ifdef __APPLE__ if (sock >= FD_SETSIZE) { return -1; } fd_set fds, *rfds, *wfds; FD_ZERO(&fds); FD_SET(sock, &fds); rfds = (Read ? &fds : nullptr); wfds = (Read ? nullptr : &fds); timeval tv; tv.tv_sec = static_cast(sec); tv.tv_usec = static_cast(usec); return handle_EINTR([&]() { return select(static_cast(sock + 1), rfds, wfds, nullptr, &tv); }); #else struct pollfd pfd; pfd.fd = sock; pfd.events = (Read ? POLLIN : POLLOUT); auto timeout = static_cast(sec * 1000 + usec / 1000); return handle_EINTR([&]() { return poll_wrapper(&pfd, 1, timeout); }); #endif } inline ssize_t select_read(socket_t sock, time_t sec, time_t usec) { return select_impl(sock, sec, usec); } inline ssize_t select_write(socket_t sock, time_t sec, time_t usec) { return select_impl(sock, sec, usec); } inline Error wait_until_socket_is_ready(socket_t sock, time_t sec, time_t usec) { #ifdef __APPLE__ if (sock >= FD_SETSIZE) { return Error::Connection; } fd_set fdsr, fdsw; FD_ZERO(&fdsr); FD_ZERO(&fdsw); FD_SET(sock, &fdsr); FD_SET(sock, &fdsw); timeval tv; tv.tv_sec = static_cast(sec); tv.tv_usec = static_cast(usec); auto ret = handle_EINTR([&]() { return select(static_cast(sock + 1), &fdsr, &fdsw, nullptr, &tv); }); if (ret == 0) { return Error::ConnectionTimeout; } if (ret > 0 && (FD_ISSET(sock, &fdsr) || FD_ISSET(sock, &fdsw))) { auto error = 0; socklen_t len = sizeof(error); auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR, reinterpret_cast(&error), &len); auto successful = res >= 0 && !error; return successful ? Error::Success : Error::Connection; } return Error::Connection; #else struct pollfd pfd_read; pfd_read.fd = sock; pfd_read.events = POLLIN | POLLOUT; auto timeout = static_cast(sec * 1000 + usec / 1000); auto poll_res = handle_EINTR([&]() { return poll_wrapper(&pfd_read, 1, timeout); }); if (poll_res == 0) { return Error::ConnectionTimeout; } if (poll_res > 0 && pfd_read.revents & (POLLIN | POLLOUT)) { auto error = 0; socklen_t len = sizeof(error); auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR, reinterpret_cast(&error), &len); auto successful = res >= 0 && !error; return successful ? Error::Success : Error::Connection; } return Error::Connection; #endif } inline bool is_socket_alive(socket_t sock) { const auto val = detail::select_read(sock, 0, 0); if (val == 0) { return true; } else if (val < 0 && errno == EBADF) { return false; } char buf[1]; return detail::read_socket(sock, &buf[0], sizeof(buf), MSG_PEEK) > 0; } class SocketStream final : public Stream { public: SocketStream(socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec = 0, std::chrono::time_point start_time = (std::chrono::steady_clock::time_point::min)()); ~SocketStream() override; bool is_readable() const override; bool wait_readable() const override; bool wait_writable() const override; ssize_t read(char *ptr, size_t size) override; ssize_t write(const char *ptr, size_t size) override; void get_remote_ip_and_port(std::string &ip, int &port) const override; void get_local_ip_and_port(std::string &ip, int &port) const override; socket_t socket() const override; time_t duration() const override; private: socket_t sock_; time_t read_timeout_sec_; time_t read_timeout_usec_; time_t write_timeout_sec_; time_t write_timeout_usec_; time_t max_timeout_msec_; const std::chrono::time_point start_time_; std::vector read_buff_; size_t read_buff_off_ = 0; size_t read_buff_content_size_ = 0; static const size_t read_buff_size_ = 1024l * 4; }; inline bool keep_alive(const std::atomic &svr_sock, socket_t sock, time_t keep_alive_timeout_sec) { using namespace std::chrono; const auto interval_usec = CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND; // Avoid expensive `steady_clock::now()` call for the first time if (select_read(sock, 0, interval_usec) > 0) { return true; } const auto start = steady_clock::now() - microseconds{interval_usec}; const auto timeout = seconds{keep_alive_timeout_sec}; while (true) { if (svr_sock == INVALID_SOCKET) { break; // Server socket is closed } auto val = select_read(sock, 0, interval_usec); if (val < 0) { break; // Ssocket error } else if (val == 0) { if (steady_clock::now() - start > timeout) { break; // Timeout } } else { return true; // Ready for read } } return false; } template inline bool process_server_socket_core(const std::atomic &svr_sock, socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec, T callback) { assert(keep_alive_max_count > 0); auto ret = false; auto count = keep_alive_max_count; while (count > 0 && keep_alive(svr_sock, sock, keep_alive_timeout_sec)) { auto close_connection = count == 1; auto connection_closed = false; ret = callback(close_connection, connection_closed); if (!ret || connection_closed) { break; } count--; } return ret; } template inline bool process_server_socket(const std::atomic &svr_sock, socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, T callback) { return process_server_socket_core( svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec, [&](bool close_connection, bool &connection_closed) { SocketStream strm(sock, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec); return callback(strm, close_connection, connection_closed); }); } inline bool process_client_socket( socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec, std::chrono::time_point start_time, std::function callback) { SocketStream strm(sock, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec, max_timeout_msec, start_time); return callback(strm); } inline int shutdown_socket(socket_t sock) { #ifdef _WIN32 return shutdown(sock, SD_BOTH); #else return shutdown(sock, SHUT_RDWR); #endif } inline std::string escape_abstract_namespace_unix_domain(const std::string &s) { if (s.size() > 1 && s[0] == '\0') { auto ret = s; ret[0] = '@'; return ret; } return s; } inline std::string unescape_abstract_namespace_unix_domain(const std::string &s) { if (s.size() > 1 && s[0] == '@') { auto ret = s; ret[0] = '\0'; return ret; } return s; } inline int getaddrinfo_with_timeout(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res, time_t timeout_sec) { #ifdef CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO if (timeout_sec <= 0) { // No timeout specified, use standard getaddrinfo return getaddrinfo(node, service, hints, res); } #ifdef _WIN32 // Windows-specific implementation using GetAddrInfoEx with overlapped I/O OVERLAPPED overlapped = {0}; HANDLE event = CreateEventW(nullptr, TRUE, FALSE, nullptr); if (!event) { return EAI_FAIL; } overlapped.hEvent = event; PADDRINFOEXW result_addrinfo = nullptr; HANDLE cancel_handle = nullptr; ADDRINFOEXW hints_ex = {0}; if (hints) { hints_ex.ai_flags = hints->ai_flags; hints_ex.ai_family = hints->ai_family; hints_ex.ai_socktype = hints->ai_socktype; hints_ex.ai_protocol = hints->ai_protocol; } auto wnode = u8string_to_wstring(node); auto wservice = u8string_to_wstring(service); auto ret = ::GetAddrInfoExW(wnode.data(), wservice.data(), NS_DNS, nullptr, hints ? &hints_ex : nullptr, &result_addrinfo, nullptr, &overlapped, nullptr, &cancel_handle); if (ret == WSA_IO_PENDING) { auto wait_result = ::WaitForSingleObject(event, static_cast(timeout_sec * 1000)); if (wait_result == WAIT_TIMEOUT) { if (cancel_handle) { ::GetAddrInfoExCancel(&cancel_handle); } ::CloseHandle(event); return EAI_AGAIN; } DWORD bytes_returned; if (!::GetOverlappedResult((HANDLE)INVALID_SOCKET, &overlapped, &bytes_returned, FALSE)) { ::CloseHandle(event); return ::WSAGetLastError(); } } ::CloseHandle(event); if (ret == NO_ERROR || ret == WSA_IO_PENDING) { *res = reinterpret_cast(result_addrinfo); return 0; } return ret; #elif TARGET_OS_MAC if (!node) { return EAI_NONAME; } // macOS implementation using CFHost API for asynchronous DNS resolution CFStringRef hostname_ref = CFStringCreateWithCString( kCFAllocatorDefault, node, kCFStringEncodingUTF8); if (!hostname_ref) { return EAI_MEMORY; } CFHostRef host_ref = CFHostCreateWithName(kCFAllocatorDefault, hostname_ref); CFRelease(hostname_ref); if (!host_ref) { return EAI_MEMORY; } // Set up context for callback struct CFHostContext { bool completed = false; bool success = false; CFArrayRef addresses = nullptr; std::mutex mutex; std::condition_variable cv; } context; CFHostClientContext client_context; memset(&client_context, 0, sizeof(client_context)); client_context.info = &context; // Set callback auto callback = [](CFHostRef theHost, CFHostInfoType /*typeInfo*/, const CFStreamError *error, void *info) { auto ctx = static_cast(info); std::lock_guard lock(ctx->mutex); if (error && error->error != 0) { ctx->success = false; } else { Boolean hasBeenResolved; ctx->addresses = CFHostGetAddressing(theHost, &hasBeenResolved); if (ctx->addresses && hasBeenResolved) { CFRetain(ctx->addresses); ctx->success = true; } else { ctx->success = false; } } ctx->completed = true; ctx->cv.notify_one(); }; if (!CFHostSetClient(host_ref, callback, &client_context)) { CFRelease(host_ref); return EAI_SYSTEM; } // Schedule on run loop CFRunLoopRef run_loop = CFRunLoopGetCurrent(); CFHostScheduleWithRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); // Start resolution CFStreamError stream_error; if (!CFHostStartInfoResolution(host_ref, kCFHostAddresses, &stream_error)) { CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); CFRelease(host_ref); return EAI_FAIL; } // Wait for completion with timeout auto timeout_time = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_sec); bool timed_out = false; { std::unique_lock lock(context.mutex); while (!context.completed) { auto now = std::chrono::steady_clock::now(); if (now >= timeout_time) { timed_out = true; break; } // Run the runloop for a short time lock.unlock(); CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, true); lock.lock(); } } // Clean up CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); CFHostSetClient(host_ref, nullptr, nullptr); if (timed_out || !context.completed) { CFHostCancelInfoResolution(host_ref, kCFHostAddresses); CFRelease(host_ref); return EAI_AGAIN; } if (!context.success || !context.addresses) { CFRelease(host_ref); return EAI_NODATA; } // Convert CFArray to addrinfo CFIndex count = CFArrayGetCount(context.addresses); if (count == 0) { CFRelease(context.addresses); CFRelease(host_ref); return EAI_NODATA; } struct addrinfo *result_addrinfo = nullptr; struct addrinfo **current = &result_addrinfo; for (CFIndex i = 0; i < count; i++) { CFDataRef addr_data = static_cast(CFArrayGetValueAtIndex(context.addresses, i)); if (!addr_data) continue; const struct sockaddr *sockaddr_ptr = reinterpret_cast(CFDataGetBytePtr(addr_data)); socklen_t sockaddr_len = static_cast(CFDataGetLength(addr_data)); // Allocate addrinfo structure *current = static_cast(malloc(sizeof(struct addrinfo))); if (!*current) { freeaddrinfo(result_addrinfo); CFRelease(context.addresses); CFRelease(host_ref); return EAI_MEMORY; } memset(*current, 0, sizeof(struct addrinfo)); // Set up addrinfo fields (*current)->ai_family = sockaddr_ptr->sa_family; (*current)->ai_socktype = hints ? hints->ai_socktype : SOCK_STREAM; (*current)->ai_protocol = hints ? hints->ai_protocol : IPPROTO_TCP; (*current)->ai_addrlen = sockaddr_len; // Copy sockaddr (*current)->ai_addr = static_cast(malloc(sockaddr_len)); if (!(*current)->ai_addr) { freeaddrinfo(result_addrinfo); CFRelease(context.addresses); CFRelease(host_ref); return EAI_MEMORY; } memcpy((*current)->ai_addr, sockaddr_ptr, sockaddr_len); // Set port if service is specified if (service && strlen(service) > 0) { int port = atoi(service); if (port > 0) { if (sockaddr_ptr->sa_family == AF_INET) { reinterpret_cast((*current)->ai_addr) ->sin_port = htons(static_cast(port)); } else if (sockaddr_ptr->sa_family == AF_INET6) { reinterpret_cast((*current)->ai_addr) ->sin6_port = htons(static_cast(port)); } } } current = &((*current)->ai_next); } CFRelease(context.addresses); CFRelease(host_ref); *res = result_addrinfo; return 0; #elif defined(_GNU_SOURCE) && defined(__GLIBC__) && \ (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2)) // Linux implementation using getaddrinfo_a for asynchronous DNS resolution struct gaicb request; struct gaicb *requests[1] = {&request}; struct sigevent sevp; struct timespec timeout; // Initialize the request structure memset(&request, 0, sizeof(request)); request.ar_name = node; request.ar_service = service; request.ar_request = hints; // Set up timeout timeout.tv_sec = timeout_sec; timeout.tv_nsec = 0; // Initialize sigevent structure (not used, but required) memset(&sevp, 0, sizeof(sevp)); sevp.sigev_notify = SIGEV_NONE; // Start asynchronous resolution int start_result = getaddrinfo_a(GAI_NOWAIT, requests, 1, &sevp); if (start_result != 0) { return start_result; } // Wait for completion with timeout int wait_result = gai_suspend((const struct gaicb *const *)requests, 1, &timeout); if (wait_result == 0 || wait_result == EAI_ALLDONE) { // Completed successfully, get the result int gai_result = gai_error(&request); if (gai_result == 0) { *res = request.ar_result; return 0; } else { // Clean up on error if (request.ar_result) { freeaddrinfo(request.ar_result); } return gai_result; } } else if (wait_result == EAI_AGAIN) { // Timeout occurred, cancel the request gai_cancel(&request); return EAI_AGAIN; } else { // Other error occurred gai_cancel(&request); return wait_result; } #else // Fallback implementation using thread-based timeout for other Unix systems struct GetAddrInfoState { ~GetAddrInfoState() { if (info) { freeaddrinfo(info); } } std::mutex mutex; std::condition_variable result_cv; bool completed = false; int result = EAI_SYSTEM; std::string node; std::string service; struct addrinfo hints; struct addrinfo *info = nullptr; }; // Allocate on the heap, so the resolver thread can keep using the data. auto state = std::make_shared(); if (node) { state->node = node; } state->service = service; state->hints = *hints; std::thread resolve_thread([state]() { auto thread_result = getaddrinfo(state->node.c_str(), state->service.c_str(), &state->hints, &state->info); std::lock_guard lock(state->mutex); state->result = thread_result; state->completed = true; state->result_cv.notify_one(); }); // Wait for completion or timeout std::unique_lock lock(state->mutex); auto finished = state->result_cv.wait_for(lock, std::chrono::seconds(timeout_sec), [&] { return state->completed; }); if (finished) { // Operation completed within timeout resolve_thread.join(); *res = state->info; state->info = nullptr; // Pass ownership to caller return state->result; } else { // Timeout occurred resolve_thread.detach(); // Let the thread finish in background return EAI_AGAIN; // Return timeout error } #endif #else (void)(timeout_sec); // Unused parameter for non-blocking getaddrinfo return getaddrinfo(node, service, hints, res); #endif } template socket_t create_socket(const std::string &host, const std::string &ip, int port, int address_family, int socket_flags, bool tcp_nodelay, bool ipv6_v6only, SocketOptions socket_options, BindOrConnect bind_or_connect, time_t timeout_sec = 0) { // Get address info const char *node = nullptr; struct addrinfo hints; struct addrinfo *result; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_IP; if (!ip.empty()) { node = ip.c_str(); // Ask getaddrinfo to convert IP in c-string to address hints.ai_family = AF_UNSPEC; hints.ai_flags = AI_NUMERICHOST; } else { if (!host.empty()) { node = host.c_str(); } hints.ai_family = address_family; hints.ai_flags = socket_flags; } #if !defined(_WIN32) || defined(CPPHTTPLIB_HAVE_AFUNIX_H) if (hints.ai_family == AF_UNIX) { const auto addrlen = host.length(); if (addrlen > sizeof(sockaddr_un::sun_path)) { return INVALID_SOCKET; } #ifdef SOCK_CLOEXEC auto sock = socket(hints.ai_family, hints.ai_socktype | SOCK_CLOEXEC, hints.ai_protocol); #else auto sock = socket(hints.ai_family, hints.ai_socktype, hints.ai_protocol); #endif if (sock != INVALID_SOCKET) { sockaddr_un addr{}; addr.sun_family = AF_UNIX; auto unescaped_host = unescape_abstract_namespace_unix_domain(host); std::copy(unescaped_host.begin(), unescaped_host.end(), addr.sun_path); hints.ai_addr = reinterpret_cast(&addr); hints.ai_addrlen = static_cast( sizeof(addr) - sizeof(addr.sun_path) + addrlen); #ifndef SOCK_CLOEXEC #ifndef _WIN32 fcntl(sock, F_SETFD, FD_CLOEXEC); #endif #endif if (socket_options) { socket_options(sock); } #ifdef _WIN32 // Setting SO_REUSEADDR seems not to work well with AF_UNIX on windows, so // remove the option. detail::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 0); #endif bool dummy; if (!bind_or_connect(sock, hints, dummy)) { close_socket(sock); sock = INVALID_SOCKET; } } return sock; } #endif auto service = std::to_string(port); if (getaddrinfo_with_timeout(node, service.c_str(), &hints, &result, timeout_sec)) { #if defined __linux__ && !defined __ANDROID__ res_init(); #endif return INVALID_SOCKET; } auto se = detail::scope_exit([&] { freeaddrinfo(result); }); for (auto rp = result; rp; rp = rp->ai_next) { // Create a socket #ifdef _WIN32 auto sock = WSASocketW(rp->ai_family, rp->ai_socktype, rp->ai_protocol, nullptr, 0, WSA_FLAG_NO_HANDLE_INHERIT | WSA_FLAG_OVERLAPPED); /** * Since the WSA_FLAG_NO_HANDLE_INHERIT is only supported on Windows 7 SP1 * and above the socket creation fails on older Windows Systems. * * Let's try to create a socket the old way in this case. * * Reference: * https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketa * * WSA_FLAG_NO_HANDLE_INHERIT: * This flag is supported on Windows 7 with SP1, Windows Server 2008 R2 with * SP1, and later * */ if (sock == INVALID_SOCKET) { sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); } #else #ifdef SOCK_CLOEXEC auto sock = socket(rp->ai_family, rp->ai_socktype | SOCK_CLOEXEC, rp->ai_protocol); #else auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); #endif #endif if (sock == INVALID_SOCKET) { continue; } #if !defined _WIN32 && !defined SOCK_CLOEXEC if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) { close_socket(sock); continue; } #endif if (tcp_nodelay) { set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1); } if (rp->ai_family == AF_INET6) { set_socket_opt(sock, IPPROTO_IPV6, IPV6_V6ONLY, ipv6_v6only ? 1 : 0); } if (socket_options) { socket_options(sock); } // bind or connect auto quit = false; if (bind_or_connect(sock, *rp, quit)) { return sock; } close_socket(sock); if (quit) { break; } } return INVALID_SOCKET; } inline void set_nonblocking(socket_t sock, bool nonblocking) { #ifdef _WIN32 auto flags = nonblocking ? 1UL : 0UL; ioctlsocket(sock, FIONBIO, &flags); #else auto flags = fcntl(sock, F_GETFL, 0); fcntl(sock, F_SETFL, nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK))); #endif } inline bool is_connection_error() { #ifdef _WIN32 return WSAGetLastError() != WSAEWOULDBLOCK; #else return errno != EINPROGRESS; #endif } inline bool bind_ip_address(socket_t sock, const std::string &host) { struct addrinfo hints; struct addrinfo *result; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; if (getaddrinfo_with_timeout(host.c_str(), "0", &hints, &result, 0)) { return false; } auto se = detail::scope_exit([&] { freeaddrinfo(result); }); auto ret = false; for (auto rp = result; rp; rp = rp->ai_next) { const auto &ai = *rp; if (!::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { ret = true; break; } } return ret; } #if !defined _WIN32 && !defined ANDROID && !defined _AIX && !defined __MVS__ #define USE_IF2IP #endif #ifdef USE_IF2IP inline std::string if2ip(int address_family, const std::string &ifn) { struct ifaddrs *ifap; getifaddrs(&ifap); auto se = detail::scope_exit([&] { freeifaddrs(ifap); }); std::string addr_candidate; for (auto ifa = ifap; ifa; ifa = ifa->ifa_next) { if (ifa->ifa_addr && ifn == ifa->ifa_name && (AF_UNSPEC == address_family || ifa->ifa_addr->sa_family == address_family)) { if (ifa->ifa_addr->sa_family == AF_INET) { auto sa = reinterpret_cast(ifa->ifa_addr); char buf[INET_ADDRSTRLEN]; if (inet_ntop(AF_INET, &sa->sin_addr, buf, INET_ADDRSTRLEN)) { return std::string(buf, INET_ADDRSTRLEN); } } else if (ifa->ifa_addr->sa_family == AF_INET6) { auto sa = reinterpret_cast(ifa->ifa_addr); if (!IN6_IS_ADDR_LINKLOCAL(&sa->sin6_addr)) { char buf[INET6_ADDRSTRLEN] = {}; if (inet_ntop(AF_INET6, &sa->sin6_addr, buf, INET6_ADDRSTRLEN)) { // equivalent to mac's IN6_IS_ADDR_UNIQUE_LOCAL auto s6_addr_head = sa->sin6_addr.s6_addr[0]; if (s6_addr_head == 0xfc || s6_addr_head == 0xfd) { addr_candidate = std::string(buf, INET6_ADDRSTRLEN); } else { return std::string(buf, INET6_ADDRSTRLEN); } } } } } } return addr_candidate; } #endif inline socket_t create_client_socket( const std::string &host, const std::string &ip, int port, int address_family, bool tcp_nodelay, bool ipv6_v6only, SocketOptions socket_options, time_t connection_timeout_sec, time_t connection_timeout_usec, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, const std::string &intf, Error &error) { auto sock = create_socket( host, ip, port, address_family, 0, tcp_nodelay, ipv6_v6only, std::move(socket_options), [&](socket_t sock2, struct addrinfo &ai, bool &quit) -> bool { if (!intf.empty()) { #ifdef USE_IF2IP auto ip_from_if = if2ip(address_family, intf); if (ip_from_if.empty()) { ip_from_if = intf; } if (!bind_ip_address(sock2, ip_from_if)) { error = Error::BindIPAddress; return false; } #endif } set_nonblocking(sock2, true); auto ret = ::connect(sock2, ai.ai_addr, static_cast(ai.ai_addrlen)); if (ret < 0) { if (is_connection_error()) { error = Error::Connection; return false; } error = wait_until_socket_is_ready(sock2, connection_timeout_sec, connection_timeout_usec); if (error != Error::Success) { if (error == Error::ConnectionTimeout) { quit = true; } return false; } } set_nonblocking(sock2, false); set_socket_opt_time(sock2, SOL_SOCKET, SO_RCVTIMEO, read_timeout_sec, read_timeout_usec); set_socket_opt_time(sock2, SOL_SOCKET, SO_SNDTIMEO, write_timeout_sec, write_timeout_usec); error = Error::Success; return true; }, connection_timeout_sec); // Pass DNS timeout if (sock != INVALID_SOCKET) { error = Error::Success; } else { if (error == Error::Success) { error = Error::Connection; } } return sock; } inline bool get_ip_and_port(const struct sockaddr_storage &addr, socklen_t addr_len, std::string &ip, int &port) { if (addr.ss_family == AF_INET) { port = ntohs(reinterpret_cast(&addr)->sin_port); } else if (addr.ss_family == AF_INET6) { port = ntohs(reinterpret_cast(&addr)->sin6_port); } else { return false; } std::array ipstr{}; if (getnameinfo(reinterpret_cast(&addr), addr_len, ipstr.data(), static_cast(ipstr.size()), nullptr, 0, NI_NUMERICHOST)) { return false; } ip = ipstr.data(); return true; } inline void get_local_ip_and_port(socket_t sock, std::string &ip, int &port) { struct sockaddr_storage addr; socklen_t addr_len = sizeof(addr); if (!getsockname(sock, reinterpret_cast(&addr), &addr_len)) { get_ip_and_port(addr, addr_len, ip, port); } } inline void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) { struct sockaddr_storage addr; socklen_t addr_len = sizeof(addr); if (!getpeername(sock, reinterpret_cast(&addr), &addr_len)) { #ifndef _WIN32 if (addr.ss_family == AF_UNIX) { #if defined(__linux__) struct ucred ucred; socklen_t len = sizeof(ucred); if (getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &ucred, &len) == 0) { port = ucred.pid; } #elif defined(SOL_LOCAL) && defined(SO_PEERPID) pid_t pid; socklen_t len = sizeof(pid); if (getsockopt(sock, SOL_LOCAL, SO_PEERPID, &pid, &len) == 0) { port = pid; } #endif return; } #endif get_ip_and_port(addr, addr_len, ip, port); } } inline constexpr unsigned int str2tag_core(const char *s, size_t l, unsigned int h) { return (l == 0) ? h : str2tag_core( s + 1, l - 1, // Unsets the 6 high bits of h, therefore no overflow happens (((std::numeric_limits::max)() >> 6) & h * 33) ^ static_cast(*s)); } inline unsigned int str2tag(const std::string &s) { return str2tag_core(s.data(), s.size(), 0); } namespace udl { inline constexpr unsigned int operator""_t(const char *s, size_t l) { return str2tag_core(s, l, 0); } } // namespace udl inline std::string find_content_type(const std::string &path, const std::map &user_data, const std::string &default_content_type) { auto ext = file_extension(path); auto it = user_data.find(ext); if (it != user_data.end()) { return it->second; } using udl::operator""_t; switch (str2tag(ext)) { default: return default_content_type; case "css"_t: return "text/css"; case "csv"_t: return "text/csv"; case "htm"_t: case "html"_t: return "text/html"; case "js"_t: case "mjs"_t: return "text/javascript"; case "txt"_t: return "text/plain"; case "vtt"_t: return "text/vtt"; case "apng"_t: return "image/apng"; case "avif"_t: return "image/avif"; case "bmp"_t: return "image/bmp"; case "gif"_t: return "image/gif"; case "png"_t: return "image/png"; case "svg"_t: return "image/svg+xml"; case "webp"_t: return "image/webp"; case "ico"_t: return "image/x-icon"; case "tif"_t: return "image/tiff"; case "tiff"_t: return "image/tiff"; case "jpg"_t: case "jpeg"_t: return "image/jpeg"; case "mp4"_t: return "video/mp4"; case "mpeg"_t: return "video/mpeg"; case "webm"_t: return "video/webm"; case "mp3"_t: return "audio/mp3"; case "mpga"_t: return "audio/mpeg"; case "weba"_t: return "audio/webm"; case "wav"_t: return "audio/wave"; case "otf"_t: return "font/otf"; case "ttf"_t: return "font/ttf"; case "woff"_t: return "font/woff"; case "woff2"_t: return "font/woff2"; case "7z"_t: return "application/x-7z-compressed"; case "atom"_t: return "application/atom+xml"; case "pdf"_t: return "application/pdf"; case "json"_t: return "application/json"; case "rss"_t: return "application/rss+xml"; case "tar"_t: return "application/x-tar"; case "xht"_t: case "xhtml"_t: return "application/xhtml+xml"; case "xslt"_t: return "application/xslt+xml"; case "xml"_t: return "application/xml"; case "gz"_t: return "application/gzip"; case "zip"_t: return "application/zip"; case "wasm"_t: return "application/wasm"; } } inline bool can_compress_content_type(const std::string &content_type) { using udl::operator""_t; auto tag = str2tag(content_type); switch (tag) { case "image/svg+xml"_t: case "application/javascript"_t: case "application/json"_t: case "application/xml"_t: case "application/protobuf"_t: case "application/xhtml+xml"_t: return true; case "text/event-stream"_t: return false; default: return !content_type.rfind("text/", 0); } } inline EncodingType encoding_type(const Request &req, const Response &res) { auto ret = detail::can_compress_content_type(res.get_header_value("Content-Type")); if (!ret) { return EncodingType::None; } const auto &s = req.get_header_value("Accept-Encoding"); (void)(s); #ifdef CPPHTTPLIB_BROTLI_SUPPORT // TODO: 'Accept-Encoding' has br, not br;q=0 ret = s.find("br") != std::string::npos; if (ret) { return EncodingType::Brotli; } #endif #ifdef CPPHTTPLIB_ZLIB_SUPPORT // TODO: 'Accept-Encoding' has gzip, not gzip;q=0 ret = s.find("gzip") != std::string::npos; if (ret) { return EncodingType::Gzip; } #endif #ifdef CPPHTTPLIB_ZSTD_SUPPORT // TODO: 'Accept-Encoding' has zstd, not zstd;q=0 ret = s.find("zstd") != std::string::npos; if (ret) { return EncodingType::Zstd; } #endif return EncodingType::None; } inline bool nocompressor::compress(const char *data, size_t data_length, bool /*last*/, Callback callback) { if (!data_length) { return true; } return callback(data, data_length); } #ifdef CPPHTTPLIB_ZLIB_SUPPORT inline gzip_compressor::gzip_compressor() { std::memset(&strm_, 0, sizeof(strm_)); strm_.zalloc = Z_NULL; strm_.zfree = Z_NULL; strm_.opaque = Z_NULL; is_valid_ = deflateInit2(&strm_, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) == Z_OK; } inline gzip_compressor::~gzip_compressor() { deflateEnd(&strm_); } inline bool gzip_compressor::compress(const char *data, size_t data_length, bool last, Callback callback) { assert(is_valid_); do { constexpr size_t max_avail_in = (std::numeric_limits::max)(); strm_.avail_in = static_cast( (std::min)(data_length, max_avail_in)); strm_.next_in = const_cast(reinterpret_cast(data)); data_length -= strm_.avail_in; data += strm_.avail_in; auto flush = (last && data_length == 0) ? Z_FINISH : Z_NO_FLUSH; auto ret = Z_OK; std::array buff{}; do { strm_.avail_out = static_cast(buff.size()); strm_.next_out = reinterpret_cast(buff.data()); ret = deflate(&strm_, flush); if (ret == Z_STREAM_ERROR) { return false; } if (!callback(buff.data(), buff.size() - strm_.avail_out)) { return false; } } while (strm_.avail_out == 0); assert((flush == Z_FINISH && ret == Z_STREAM_END) || (flush == Z_NO_FLUSH && ret == Z_OK)); assert(strm_.avail_in == 0); } while (data_length > 0); return true; } inline gzip_decompressor::gzip_decompressor() { std::memset(&strm_, 0, sizeof(strm_)); strm_.zalloc = Z_NULL; strm_.zfree = Z_NULL; strm_.opaque = Z_NULL; // 15 is the value of wbits, which should be at the maximum possible value // to ensure that any gzip stream can be decoded. The offset of 32 specifies // that the stream type should be automatically detected either gzip or // deflate. is_valid_ = inflateInit2(&strm_, 32 + 15) == Z_OK; } inline gzip_decompressor::~gzip_decompressor() { inflateEnd(&strm_); } inline bool gzip_decompressor::is_valid() const { return is_valid_; } inline bool gzip_decompressor::decompress(const char *data, size_t data_length, Callback callback) { assert(is_valid_); auto ret = Z_OK; do { constexpr size_t max_avail_in = (std::numeric_limits::max)(); strm_.avail_in = static_cast( (std::min)(data_length, max_avail_in)); strm_.next_in = const_cast(reinterpret_cast(data)); data_length -= strm_.avail_in; data += strm_.avail_in; std::array buff{}; while (strm_.avail_in > 0 && ret == Z_OK) { strm_.avail_out = static_cast(buff.size()); strm_.next_out = reinterpret_cast(buff.data()); ret = inflate(&strm_, Z_NO_FLUSH); assert(ret != Z_STREAM_ERROR); switch (ret) { case Z_NEED_DICT: case Z_DATA_ERROR: case Z_MEM_ERROR: inflateEnd(&strm_); return false; } if (!callback(buff.data(), buff.size() - strm_.avail_out)) { return false; } } if (ret != Z_OK && ret != Z_STREAM_END) { return false; } } while (data_length > 0); return true; } #endif #ifdef CPPHTTPLIB_BROTLI_SUPPORT inline brotli_compressor::brotli_compressor() { state_ = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr); } inline brotli_compressor::~brotli_compressor() { BrotliEncoderDestroyInstance(state_); } inline bool brotli_compressor::compress(const char *data, size_t data_length, bool last, Callback callback) { std::array buff{}; auto operation = last ? BROTLI_OPERATION_FINISH : BROTLI_OPERATION_PROCESS; auto available_in = data_length; auto next_in = reinterpret_cast(data); for (;;) { if (last) { if (BrotliEncoderIsFinished(state_)) { break; } } else { if (!available_in) { break; } } auto available_out = buff.size(); auto next_out = buff.data(); if (!BrotliEncoderCompressStream(state_, operation, &available_in, &next_in, &available_out, &next_out, nullptr)) { return false; } auto output_bytes = buff.size() - available_out; if (output_bytes) { callback(reinterpret_cast(buff.data()), output_bytes); } } return true; } inline brotli_decompressor::brotli_decompressor() { decoder_s = BrotliDecoderCreateInstance(0, 0, 0); decoder_r = decoder_s ? BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT : BROTLI_DECODER_RESULT_ERROR; } inline brotli_decompressor::~brotli_decompressor() { if (decoder_s) { BrotliDecoderDestroyInstance(decoder_s); } } inline bool brotli_decompressor::is_valid() const { return decoder_s; } inline bool brotli_decompressor::decompress(const char *data, size_t data_length, Callback callback) { if (decoder_r == BROTLI_DECODER_RESULT_SUCCESS || decoder_r == BROTLI_DECODER_RESULT_ERROR) { return 0; } auto next_in = reinterpret_cast(data); size_t avail_in = data_length; size_t total_out; decoder_r = BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT; std::array buff{}; while (decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) { char *next_out = buff.data(); size_t avail_out = buff.size(); decoder_r = BrotliDecoderDecompressStream( decoder_s, &avail_in, &next_in, &avail_out, reinterpret_cast(&next_out), &total_out); if (decoder_r == BROTLI_DECODER_RESULT_ERROR) { return false; } if (!callback(buff.data(), buff.size() - avail_out)) { return false; } } return decoder_r == BROTLI_DECODER_RESULT_SUCCESS || decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT; } #endif #ifdef CPPHTTPLIB_ZSTD_SUPPORT inline zstd_compressor::zstd_compressor() { ctx_ = ZSTD_createCCtx(); ZSTD_CCtx_setParameter(ctx_, ZSTD_c_compressionLevel, ZSTD_fast); } inline zstd_compressor::~zstd_compressor() { ZSTD_freeCCtx(ctx_); } inline bool zstd_compressor::compress(const char *data, size_t data_length, bool last, Callback callback) { std::array buff{}; ZSTD_EndDirective mode = last ? ZSTD_e_end : ZSTD_e_continue; ZSTD_inBuffer input = {data, data_length, 0}; bool finished; do { ZSTD_outBuffer output = {buff.data(), CPPHTTPLIB_COMPRESSION_BUFSIZ, 0}; size_t const remaining = ZSTD_compressStream2(ctx_, &output, &input, mode); if (ZSTD_isError(remaining)) { return false; } if (!callback(buff.data(), output.pos)) { return false; } finished = last ? (remaining == 0) : (input.pos == input.size); } while (!finished); return true; } inline zstd_decompressor::zstd_decompressor() { ctx_ = ZSTD_createDCtx(); } inline zstd_decompressor::~zstd_decompressor() { ZSTD_freeDCtx(ctx_); } inline bool zstd_decompressor::is_valid() const { return ctx_ != nullptr; } inline bool zstd_decompressor::decompress(const char *data, size_t data_length, Callback callback) { std::array buff{}; ZSTD_inBuffer input = {data, data_length, 0}; while (input.pos < input.size) { ZSTD_outBuffer output = {buff.data(), CPPHTTPLIB_COMPRESSION_BUFSIZ, 0}; size_t const remaining = ZSTD_decompressStream(ctx_, &output, &input); if (ZSTD_isError(remaining)) { return false; } if (!callback(buff.data(), output.pos)) { return false; } } return true; } #endif inline std::unique_ptr create_decompressor(const std::string &encoding) { std::unique_ptr decompressor; if (encoding == "gzip" || encoding == "deflate") { #ifdef CPPHTTPLIB_ZLIB_SUPPORT decompressor = detail::make_unique(); #endif } else if (encoding.find("br") != std::string::npos) { #ifdef CPPHTTPLIB_BROTLI_SUPPORT decompressor = detail::make_unique(); #endif } else if (encoding == "zstd" || encoding.find("zstd") != std::string::npos) { #ifdef CPPHTTPLIB_ZSTD_SUPPORT decompressor = detail::make_unique(); #endif } return decompressor; } inline bool is_prohibited_header_name(const std::string &name) { using udl::operator""_t; switch (str2tag(name)) { case "REMOTE_ADDR"_t: case "REMOTE_PORT"_t: case "LOCAL_ADDR"_t: case "LOCAL_PORT"_t: return true; default: return false; } } inline bool has_header(const Headers &headers, const std::string &key) { if (is_prohibited_header_name(key)) { return false; } return headers.find(key) != headers.end(); } inline const char *get_header_value(const Headers &headers, const std::string &key, const char *def, size_t id) { if (is_prohibited_header_name(key)) { #ifndef CPPHTTPLIB_NO_EXCEPTIONS std::string msg = "Prohibited header name '" + key + "' is specified."; throw std::invalid_argument(msg); #else return ""; #endif } auto rng = headers.equal_range(key); auto it = rng.first; std::advance(it, static_cast(id)); if (it != rng.second) { return it->second.c_str(); } return def; } inline bool read_headers(Stream &strm, Headers &headers) { const auto bufsiz = 2048; char buf[bufsiz]; stream_line_reader line_reader(strm, buf, bufsiz); size_t header_count = 0; for (;;) { if (!line_reader.getline()) { return false; } // Check if the line ends with CRLF. auto line_terminator_len = 2; if (line_reader.end_with_crlf()) { // Blank line indicates end of headers. if (line_reader.size() == 2) { break; } } else { #ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR // Blank line indicates end of headers. if (line_reader.size() == 1) { break; } line_terminator_len = 1; #else continue; // Skip invalid line. #endif } if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } // Check header count limit if (header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; } // Exclude line terminator auto end = line_reader.ptr() + line_reader.size() - line_terminator_len; if (!parse_header(line_reader.ptr(), end, [&](const std::string &key, const std::string &val) { headers.emplace(key, val); })) { return false; } header_count++; } return true; } enum class ReadContentResult { Success, // Successfully read the content PayloadTooLarge, // The content exceeds the specified payload limit Error // An error occurred while reading the content }; inline ReadContentResult read_content_with_length( Stream &strm, size_t len, DownloadProgress progress, ContentReceiverWithProgress out, size_t payload_max_length = (std::numeric_limits::max)()) { char buf[CPPHTTPLIB_RECV_BUFSIZ]; detail::BodyReader br; br.stream = &strm; br.has_content_length = true; br.content_length = len; br.payload_max_length = payload_max_length; br.chunked = false; br.bytes_read = 0; br.last_error = Error::Success; size_t r = 0; while (r < len) { auto read_len = static_cast(len - r); auto to_read = (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ); auto n = detail::read_body_content(&strm, br, buf, to_read); if (n <= 0) { // Check if it was a payload size error if (br.last_error == Error::ExceedMaxPayloadSize) { return ReadContentResult::PayloadTooLarge; } return ReadContentResult::Error; } if (!out(buf, static_cast(n), r, len)) { return ReadContentResult::Error; } r += static_cast(n); if (progress) { if (!progress(r, len)) { return ReadContentResult::Error; } } } return ReadContentResult::Success; } inline ReadContentResult read_content_without_length(Stream &strm, size_t payload_max_length, ContentReceiverWithProgress out) { char buf[CPPHTTPLIB_RECV_BUFSIZ]; size_t r = 0; for (;;) { auto n = strm.read(buf, CPPHTTPLIB_RECV_BUFSIZ); if (n == 0) { return ReadContentResult::Success; } if (n < 0) { return ReadContentResult::Error; } // Check if adding this data would exceed the payload limit if (r > payload_max_length || payload_max_length - r < static_cast(n)) { return ReadContentResult::PayloadTooLarge; } if (!out(buf, static_cast(n), r, 0)) { return ReadContentResult::Error; } r += static_cast(n); } return ReadContentResult::Success; } template inline ReadContentResult read_content_chunked(Stream &strm, T &x, size_t payload_max_length, ContentReceiverWithProgress out) { detail::ChunkedDecoder dec(strm); char buf[CPPHTTPLIB_RECV_BUFSIZ]; size_t total_len = 0; for (;;) { size_t chunk_offset = 0; size_t chunk_total = 0; auto n = dec.read_payload(buf, sizeof(buf), chunk_offset, chunk_total); if (n < 0) { return ReadContentResult::Error; } if (n == 0) { if (!dec.parse_trailers_into(x.trailers, x.headers)) { return ReadContentResult::Error; } return ReadContentResult::Success; } if (total_len > payload_max_length || payload_max_length - total_len < static_cast(n)) { return ReadContentResult::PayloadTooLarge; } if (!out(buf, static_cast(n), chunk_offset, chunk_total)) { return ReadContentResult::Error; } total_len += static_cast(n); } } inline bool is_chunked_transfer_encoding(const Headers &headers) { return case_ignore::equal( get_header_value(headers, "Transfer-Encoding", "", 0), "chunked"); } template bool prepare_content_receiver(T &x, int &status, ContentReceiverWithProgress receiver, bool decompress, U callback) { if (decompress) { std::string encoding = x.get_header_value("Content-Encoding"); std::unique_ptr decompressor; if (!encoding.empty()) { decompressor = detail::create_decompressor(encoding); if (!decompressor) { // Unsupported encoding or no support compiled in status = StatusCode::UnsupportedMediaType_415; return false; } } if (decompressor) { if (decompressor->is_valid()) { ContentReceiverWithProgress out = [&](const char *buf, size_t n, size_t off, size_t len) { return decompressor->decompress(buf, n, [&](const char *buf2, size_t n2) { return receiver(buf2, n2, off, len); }); }; return callback(std::move(out)); } else { status = StatusCode::InternalServerError_500; return false; } } } ContentReceiverWithProgress out = [&](const char *buf, size_t n, size_t off, size_t len) { return receiver(buf, n, off, len); }; return callback(std::move(out)); } template bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status, DownloadProgress progress, ContentReceiverWithProgress receiver, bool decompress) { return prepare_content_receiver( x, status, std::move(receiver), decompress, [&](const ContentReceiverWithProgress &out) { auto ret = true; auto exceed_payload_max_length = false; if (is_chunked_transfer_encoding(x.headers)) { auto result = read_content_chunked(strm, x, payload_max_length, out); if (result == ReadContentResult::Success) { ret = true; } else if (result == ReadContentResult::PayloadTooLarge) { exceed_payload_max_length = true; ret = false; } else { ret = false; } } else if (!has_header(x.headers, "Content-Length")) { auto result = read_content_without_length(strm, payload_max_length, out); if (result == ReadContentResult::Success) { ret = true; } else if (result == ReadContentResult::PayloadTooLarge) { exceed_payload_max_length = true; ret = false; } else { ret = false; } } else { auto is_invalid_value = false; auto len = get_header_value_u64(x.headers, "Content-Length", (std::numeric_limits::max)(), 0, is_invalid_value); if (is_invalid_value) { ret = false; } else if (len > 0) { auto result = read_content_with_length( strm, len, std::move(progress), out, payload_max_length); ret = (result == ReadContentResult::Success); if (result == ReadContentResult::PayloadTooLarge) { exceed_payload_max_length = true; } } } if (!ret) { status = exceed_payload_max_length ? StatusCode::PayloadTooLarge_413 : StatusCode::BadRequest_400; } return ret; }); } inline ssize_t write_request_line(Stream &strm, const std::string &method, const std::string &path) { std::string s = method; s += ' '; s += path; s += " HTTP/1.1\r\n"; return strm.write(s.data(), s.size()); } inline ssize_t write_response_line(Stream &strm, int status) { std::string s = "HTTP/1.1 "; s += std::to_string(status); s += ' '; s += httplib::status_message(status); s += "\r\n"; return strm.write(s.data(), s.size()); } inline ssize_t write_headers(Stream &strm, const Headers &headers) { ssize_t write_len = 0; for (const auto &x : headers) { std::string s; s = x.first; s += ": "; s += x.second; s += "\r\n"; auto len = strm.write(s.data(), s.size()); if (len < 0) { return len; } write_len += len; } auto len = strm.write("\r\n"); if (len < 0) { return len; } write_len += len; return write_len; } inline bool write_data(Stream &strm, const char *d, size_t l) { size_t offset = 0; while (offset < l) { auto length = strm.write(d + offset, l - offset); if (length < 0) { return false; } offset += static_cast(length); } return true; } template inline bool write_content_with_progress(Stream &strm, const ContentProvider &content_provider, size_t offset, size_t length, T is_shutting_down, const UploadProgress &upload_progress, Error &error) { size_t end_offset = offset + length; size_t start_offset = offset; auto ok = true; DataSink data_sink; data_sink.write = [&](const char *d, size_t l) -> bool { if (ok) { if (write_data(strm, d, l)) { offset += l; if (upload_progress && length > 0) { size_t current_written = offset - start_offset; if (!upload_progress(current_written, length)) { ok = false; return false; } } } else { ok = false; } } return ok; }; data_sink.is_writable = [&]() -> bool { return strm.wait_writable(); }; while (offset < end_offset && !is_shutting_down()) { if (!strm.wait_writable()) { error = Error::Write; return false; } else if (!content_provider(offset, end_offset - offset, data_sink)) { error = Error::Canceled; return false; } else if (!ok) { error = Error::Write; return false; } } error = Error::Success; return true; } template inline bool write_content(Stream &strm, const ContentProvider &content_provider, size_t offset, size_t length, T is_shutting_down, Error &error) { return write_content_with_progress(strm, content_provider, offset, length, is_shutting_down, nullptr, error); } template inline bool write_content(Stream &strm, const ContentProvider &content_provider, size_t offset, size_t length, const T &is_shutting_down) { auto error = Error::Success; return write_content(strm, content_provider, offset, length, is_shutting_down, error); } template inline bool write_content_without_length(Stream &strm, const ContentProvider &content_provider, const T &is_shutting_down) { size_t offset = 0; auto data_available = true; auto ok = true; DataSink data_sink; data_sink.write = [&](const char *d, size_t l) -> bool { if (ok) { offset += l; if (!write_data(strm, d, l)) { ok = false; } } return ok; }; data_sink.is_writable = [&]() -> bool { return strm.wait_writable(); }; data_sink.done = [&](void) { data_available = false; }; while (data_available && !is_shutting_down()) { if (!strm.wait_writable()) { return false; } else if (!content_provider(offset, 0, data_sink)) { return false; } else if (!ok) { return false; } } return true; } template inline bool write_content_chunked(Stream &strm, const ContentProvider &content_provider, const T &is_shutting_down, U &compressor, Error &error) { size_t offset = 0; auto data_available = true; auto ok = true; DataSink data_sink; data_sink.write = [&](const char *d, size_t l) -> bool { if (ok) { data_available = l > 0; offset += l; std::string payload; if (compressor.compress(d, l, false, [&](const char *data, size_t data_len) { payload.append(data, data_len); return true; })) { if (!payload.empty()) { // Emit chunked response header and footer for each chunk auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n"; if (!write_data(strm, chunk.data(), chunk.size())) { ok = false; } } } else { ok = false; } } return ok; }; data_sink.is_writable = [&]() -> bool { return strm.wait_writable(); }; auto done_with_trailer = [&](const Headers *trailer) { if (!ok) { return; } data_available = false; std::string payload; if (!compressor.compress(nullptr, 0, true, [&](const char *data, size_t data_len) { payload.append(data, data_len); return true; })) { ok = false; return; } if (!payload.empty()) { // Emit chunked response header and footer for each chunk auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n"; if (!write_data(strm, chunk.data(), chunk.size())) { ok = false; return; } } constexpr const char done_marker[] = "0\r\n"; if (!write_data(strm, done_marker, str_len(done_marker))) { ok = false; } // Trailer if (trailer) { for (const auto &kv : *trailer) { std::string field_line = kv.first + ": " + kv.second + "\r\n"; if (!write_data(strm, field_line.data(), field_line.size())) { ok = false; } } } constexpr const char crlf[] = "\r\n"; if (!write_data(strm, crlf, str_len(crlf))) { ok = false; } }; data_sink.done = [&](void) { done_with_trailer(nullptr); }; data_sink.done_with_trailer = [&](const Headers &trailer) { done_with_trailer(&trailer); }; while (data_available && !is_shutting_down()) { if (!strm.wait_writable()) { error = Error::Write; return false; } else if (!content_provider(offset, 0, data_sink)) { error = Error::Canceled; return false; } else if (!ok) { error = Error::Write; return false; } } error = Error::Success; return true; } template inline bool write_content_chunked(Stream &strm, const ContentProvider &content_provider, const T &is_shutting_down, U &compressor) { auto error = Error::Success; return write_content_chunked(strm, content_provider, is_shutting_down, compressor, error); } template inline bool redirect(T &cli, Request &req, Response &res, const std::string &path, const std::string &location, Error &error) { Request new_req = req; new_req.path = path; new_req.redirect_count_ -= 1; if (res.status == StatusCode::SeeOther_303 && (req.method != "GET" && req.method != "HEAD")) { new_req.method = "GET"; new_req.body.clear(); new_req.headers.clear(); } Response new_res; auto ret = cli.send(new_req, new_res, error); if (ret) { req = std::move(new_req); res = std::move(new_res); if (res.location.empty()) { res.location = location; } } return ret; } inline std::string params_to_query_str(const Params ¶ms) { std::string query; for (auto it = params.begin(); it != params.end(); ++it) { if (it != params.begin()) { query += '&'; } query += encode_query_component(it->first); query += '='; query += encode_query_component(it->second); } return query; } inline void parse_query_text(const char *data, std::size_t size, Params ¶ms) { std::set cache; split(data, data + size, '&', [&](const char *b, const char *e) { std::string kv(b, e); if (cache.find(kv) != cache.end()) { return; } cache.insert(std::move(kv)); std::string key; std::string val; divide(b, static_cast(e - b), '=', [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data, std::size_t rhs_size) { key.assign(lhs_data, lhs_size); val.assign(rhs_data, rhs_size); }); if (!key.empty()) { params.emplace(decode_query_component(key), decode_query_component(val)); } }); } inline void parse_query_text(const std::string &s, Params ¶ms) { parse_query_text(s.data(), s.size(), params); } // Normalize a query string by decoding and re-encoding each key/value pair // while preserving the original parameter order. This avoids double-encoding // and ensures consistent encoding without reordering (unlike Params which // uses std::multimap and sorts keys). inline std::string normalize_query_string(const std::string &query) { std::string result; split(query.data(), query.data() + query.size(), '&', [&](const char *b, const char *e) { std::string key; std::string val; divide(b, static_cast(e - b), '=', [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data, std::size_t rhs_size) { key.assign(lhs_data, lhs_size); val.assign(rhs_data, rhs_size); }); if (!key.empty()) { auto dec_key = decode_query_component(key); auto dec_val = decode_query_component(val); if (!result.empty()) { result += '&'; } result += encode_query_component(dec_key); if (!val.empty() || std::find(b, e, '=') != e) { result += '='; result += encode_query_component(dec_val); } } }); return result; } inline bool parse_multipart_boundary(const std::string &content_type, std::string &boundary) { auto boundary_keyword = "boundary="; auto pos = content_type.find(boundary_keyword); if (pos == std::string::npos) { return false; } auto end = content_type.find(';', pos); auto beg = pos + strlen(boundary_keyword); boundary = trim_double_quotes_copy(content_type.substr(beg, end - beg)); return !boundary.empty(); } inline void parse_disposition_params(const std::string &s, Params ¶ms) { std::set cache; split(s.data(), s.data() + s.size(), ';', [&](const char *b, const char *e) { std::string kv(b, e); if (cache.find(kv) != cache.end()) { return; } cache.insert(kv); std::string key; std::string val; split(b, e, '=', [&](const char *b2, const char *e2) { if (key.empty()) { key.assign(b2, e2); } else { val.assign(b2, e2); } }); if (!key.empty()) { params.emplace(trim_double_quotes_copy((key)), trim_double_quotes_copy((val))); } }); } #ifdef CPPHTTPLIB_NO_EXCEPTIONS inline bool parse_range_header(const std::string &s, Ranges &ranges) { #else inline bool parse_range_header(const std::string &s, Ranges &ranges) try { #endif auto is_valid = [](const std::string &str) { return std::all_of(str.cbegin(), str.cend(), [](unsigned char c) { return std::isdigit(c); }); }; if (s.size() > 7 && s.compare(0, 6, "bytes=") == 0) { const auto pos = static_cast(6); const auto len = static_cast(s.size() - 6); auto all_valid_ranges = true; split(&s[pos], &s[pos + len], ',', [&](const char *b, const char *e) { if (!all_valid_ranges) { return; } const auto it = std::find(b, e, '-'); if (it == e) { all_valid_ranges = false; return; } const auto lhs = std::string(b, it); const auto rhs = std::string(it + 1, e); if (!is_valid(lhs) || !is_valid(rhs)) { all_valid_ranges = false; return; } ssize_t first = -1; if (!lhs.empty()) { ssize_t v; auto res = detail::from_chars(lhs.data(), lhs.data() + lhs.size(), v); if (res.ec == std::errc{}) { first = v; } } ssize_t last = -1; if (!rhs.empty()) { ssize_t v; auto res = detail::from_chars(rhs.data(), rhs.data() + rhs.size(), v); if (res.ec == std::errc{}) { last = v; } } if ((first == -1 && last == -1) || (first != -1 && last != -1 && first > last)) { all_valid_ranges = false; return; } ranges.emplace_back(first, last); }); return all_valid_ranges && !ranges.empty(); } return false; #ifdef CPPHTTPLIB_NO_EXCEPTIONS } #else } catch (...) { return false; } #endif inline bool parse_accept_header(const std::string &s, std::vector &content_types) { content_types.clear(); // Empty string is considered valid (no preference) if (s.empty()) { return true; } // Check for invalid patterns: leading/trailing commas or consecutive commas if (s.front() == ',' || s.back() == ',' || s.find(",,") != std::string::npos) { return false; } struct AcceptEntry { std::string media_type; double quality; int order; // Original order in header }; std::vector entries; int order = 0; bool has_invalid_entry = false; // Split by comma and parse each entry split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) { std::string entry(b, e); entry = trim_copy(entry); if (entry.empty()) { has_invalid_entry = true; return; } AcceptEntry accept_entry; accept_entry.quality = 1.0; // Default quality accept_entry.order = order++; // Find q= parameter auto q_pos = entry.find(";q="); if (q_pos == std::string::npos) { q_pos = entry.find("; q="); } if (q_pos != std::string::npos) { // Extract media type (before q parameter) accept_entry.media_type = trim_copy(entry.substr(0, q_pos)); // Extract quality value auto q_start = entry.find('=', q_pos) + 1; auto q_end = entry.find(';', q_start); if (q_end == std::string::npos) { q_end = entry.length(); } std::string quality_str = trim_copy(entry.substr(q_start, q_end - q_start)); if (quality_str.empty()) { has_invalid_entry = true; return; } { double v = 0.0; auto res = detail::from_chars( quality_str.data(), quality_str.data() + quality_str.size(), v); if (res.ec == std::errc{}) { accept_entry.quality = v; } else { has_invalid_entry = true; return; } } // Check if quality is in valid range [0.0, 1.0] if (accept_entry.quality < 0.0 || accept_entry.quality > 1.0) { has_invalid_entry = true; return; } } else { // No quality parameter, use entire entry as media type accept_entry.media_type = entry; } // Remove additional parameters from media type auto param_pos = accept_entry.media_type.find(';'); if (param_pos != std::string::npos) { accept_entry.media_type = trim_copy(accept_entry.media_type.substr(0, param_pos)); } // Basic validation of media type format if (accept_entry.media_type.empty()) { has_invalid_entry = true; return; } // Check for basic media type format (should contain '/' or be '*') if (accept_entry.media_type != "*" && accept_entry.media_type.find('/') == std::string::npos) { has_invalid_entry = true; return; } entries.push_back(std::move(accept_entry)); }); // Return false if any invalid entry was found if (has_invalid_entry) { return false; } // Sort by quality (descending), then by original order (ascending) std::sort(entries.begin(), entries.end(), [](const AcceptEntry &a, const AcceptEntry &b) { if (a.quality != b.quality) { return a.quality > b.quality; // Higher quality first } return a.order < b.order; // Earlier order first for same quality }); // Extract sorted media types content_types.reserve(entries.size()); for (auto &entry : entries) { content_types.push_back(std::move(entry.media_type)); } return true; } class FormDataParser { public: FormDataParser() = default; void set_boundary(std::string &&boundary) { boundary_ = std::move(boundary); dash_boundary_crlf_ = dash_ + boundary_ + crlf_; crlf_dash_boundary_ = crlf_ + dash_ + boundary_; } bool is_valid() const { return is_valid_; } bool parse(const char *buf, size_t n, const FormDataHeader &header_callback, const ContentReceiver &content_callback) { buf_append(buf, n); while (buf_size() > 0) { switch (state_) { case 0: { // Initial boundary auto pos = buf_find(dash_boundary_crlf_); if (pos == buf_size()) { return true; } buf_erase(pos + dash_boundary_crlf_.size()); state_ = 1; break; } case 1: { // New entry clear_file_info(); state_ = 2; break; } case 2: { // Headers auto pos = buf_find(crlf_); if (pos > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } while (pos < buf_size()) { // Empty line if (pos == 0) { if (!header_callback(file_)) { is_valid_ = false; return false; } buf_erase(crlf_.size()); state_ = 3; break; } const auto header = buf_head(pos); if (!parse_header(header.data(), header.data() + header.size(), [&](const std::string &, const std::string &) {})) { is_valid_ = false; return false; } // Parse and emplace space trimmed headers into a map if (!parse_header( header.data(), header.data() + header.size(), [&](const std::string &key, const std::string &val) { file_.headers.emplace(key, val); })) { is_valid_ = false; return false; } constexpr const char header_content_type[] = "Content-Type:"; if (start_with_case_ignore(header, header_content_type)) { file_.content_type = trim_copy(header.substr(str_len(header_content_type))); } else { thread_local const std::regex re_content_disposition( R"~(^Content-Disposition:\s*form-data;\s*(.*)$)~", std::regex_constants::icase); std::smatch m; if (std::regex_match(header, m, re_content_disposition)) { Params params; parse_disposition_params(m[1], params); auto it = params.find("name"); if (it != params.end()) { file_.name = it->second; } else { is_valid_ = false; return false; } it = params.find("filename"); if (it != params.end()) { file_.filename = it->second; } it = params.find("filename*"); if (it != params.end()) { // Only allow UTF-8 encoding... thread_local const std::regex re_rfc5987_encoding( R"~(^UTF-8''(.+?)$)~", std::regex_constants::icase); std::smatch m2; if (std::regex_match(it->second, m2, re_rfc5987_encoding)) { file_.filename = decode_path_component(m2[1]); // override... } else { is_valid_ = false; return false; } } } } buf_erase(pos + crlf_.size()); pos = buf_find(crlf_); } if (state_ != 3) { return true; } break; } case 3: { // Body if (crlf_dash_boundary_.size() > buf_size()) { return true; } auto pos = buf_find(crlf_dash_boundary_); if (pos < buf_size()) { if (!content_callback(buf_data(), pos)) { is_valid_ = false; return false; } buf_erase(pos + crlf_dash_boundary_.size()); state_ = 4; } else { auto len = buf_size() - crlf_dash_boundary_.size(); if (len > 0) { if (!content_callback(buf_data(), len)) { is_valid_ = false; return false; } buf_erase(len); } return true; } break; } case 4: { // Boundary if (crlf_.size() > buf_size()) { return true; } if (buf_start_with(crlf_)) { buf_erase(crlf_.size()); state_ = 1; } else { if (dash_.size() > buf_size()) { return true; } if (buf_start_with(dash_)) { buf_erase(dash_.size()); is_valid_ = true; buf_erase(buf_size()); // Remove epilogue } else { return true; } } break; } } } return true; } private: void clear_file_info() { file_.name.clear(); file_.filename.clear(); file_.content_type.clear(); file_.headers.clear(); } bool start_with_case_ignore(const std::string &a, const char *b) const { const auto b_len = strlen(b); if (a.size() < b_len) { return false; } for (size_t i = 0; i < b_len; i++) { if (case_ignore::to_lower(a[i]) != case_ignore::to_lower(b[i])) { return false; } } return true; } const std::string dash_ = "--"; const std::string crlf_ = "\r\n"; std::string boundary_; std::string dash_boundary_crlf_; std::string crlf_dash_boundary_; size_t state_ = 0; bool is_valid_ = false; FormData file_; // Buffer bool start_with(const std::string &a, size_t spos, size_t epos, const std::string &b) const { if (epos - spos < b.size()) { return false; } for (size_t i = 0; i < b.size(); i++) { if (a[i + spos] != b[i]) { return false; } } return true; } size_t buf_size() const { return buf_epos_ - buf_spos_; } const char *buf_data() const { return &buf_[buf_spos_]; } std::string buf_head(size_t l) const { return buf_.substr(buf_spos_, l); } bool buf_start_with(const std::string &s) const { return start_with(buf_, buf_spos_, buf_epos_, s); } size_t buf_find(const std::string &s) const { auto c = s.front(); size_t off = buf_spos_; while (off < buf_epos_) { auto pos = off; while (true) { if (pos == buf_epos_) { return buf_size(); } if (buf_[pos] == c) { break; } pos++; } auto remaining_size = buf_epos_ - pos; if (s.size() > remaining_size) { return buf_size(); } if (start_with(buf_, pos, buf_epos_, s)) { return pos - buf_spos_; } off = pos + 1; } return buf_size(); } void buf_append(const char *data, size_t n) { auto remaining_size = buf_size(); if (remaining_size > 0 && buf_spos_ > 0) { for (size_t i = 0; i < remaining_size; i++) { buf_[i] = buf_[buf_spos_ + i]; } } buf_spos_ = 0; buf_epos_ = remaining_size; if (remaining_size + n > buf_.size()) { buf_.resize(remaining_size + n); } for (size_t i = 0; i < n; i++) { buf_[buf_epos_ + i] = data[i]; } buf_epos_ += n; } void buf_erase(size_t size) { buf_spos_ += size; } std::string buf_; size_t buf_spos_ = 0; size_t buf_epos_ = 0; }; inline std::string random_string(size_t length) { constexpr const char data[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; thread_local auto engine([]() { // std::random_device might actually be deterministic on some // platforms, but due to lack of support in the c++ standard library, // doing better requires either some ugly hacks or breaking portability. std::random_device seed_gen; // Request 128 bits of entropy for initialization std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(), seed_gen()}; return std::mt19937(seed_sequence); }()); std::string result; for (size_t i = 0; i < length; i++) { result += data[engine() % (sizeof(data) - 1)]; } return result; } inline std::string make_multipart_data_boundary() { return "--cpp-httplib-multipart-data-" + detail::random_string(16); } inline bool is_multipart_boundary_chars_valid(const std::string &boundary) { auto valid = true; for (size_t i = 0; i < boundary.size(); i++) { auto c = boundary[i]; if (!std::isalnum(c) && c != '-' && c != '_') { valid = false; break; } } return valid; } template inline std::string serialize_multipart_formdata_item_begin(const T &item, const std::string &boundary) { std::string body = "--" + boundary + "\r\n"; body += "Content-Disposition: form-data; name=\"" + item.name + "\""; if (!item.filename.empty()) { body += "; filename=\"" + item.filename + "\""; } body += "\r\n"; if (!item.content_type.empty()) { body += "Content-Type: " + item.content_type + "\r\n"; } body += "\r\n"; return body; } inline std::string serialize_multipart_formdata_item_end() { return "\r\n"; } inline std::string serialize_multipart_formdata_finish(const std::string &boundary) { return "--" + boundary + "--\r\n"; } inline std::string serialize_multipart_formdata_get_content_type(const std::string &boundary) { return "multipart/form-data; boundary=" + boundary; } inline std::string serialize_multipart_formdata(const UploadFormDataItems &items, const std::string &boundary, bool finish = true) { std::string body; for (const auto &item : items) { body += serialize_multipart_formdata_item_begin(item, boundary); body += item.content + serialize_multipart_formdata_item_end(); } if (finish) { body += serialize_multipart_formdata_finish(boundary); } return body; } inline void coalesce_ranges(Ranges &ranges, size_t content_length) { if (ranges.size() <= 1) return; // Sort ranges by start position std::sort(ranges.begin(), ranges.end(), [](const Range &a, const Range &b) { return a.first < b.first; }); Ranges coalesced; coalesced.reserve(ranges.size()); for (auto &r : ranges) { auto first_pos = r.first; auto last_pos = r.second; // Handle special cases like in range_error if (first_pos == -1 && last_pos == -1) { first_pos = 0; last_pos = static_cast(content_length); } if (first_pos == -1) { first_pos = static_cast(content_length) - last_pos; last_pos = static_cast(content_length) - 1; } if (last_pos == -1 || last_pos >= static_cast(content_length)) { last_pos = static_cast(content_length) - 1; } // Skip invalid ranges if (!(0 <= first_pos && first_pos <= last_pos && last_pos < static_cast(content_length))) { continue; } // Coalesce with previous range if overlapping or adjacent (but not // identical) if (!coalesced.empty()) { auto &prev = coalesced.back(); // Check if current range overlaps or is adjacent to previous range // but don't coalesce identical ranges (allow duplicates) if (first_pos <= prev.second + 1 && !(first_pos == prev.first && last_pos == prev.second)) { // Extend the previous range prev.second = (std::max)(prev.second, last_pos); continue; } } // Add new range coalesced.emplace_back(first_pos, last_pos); } ranges = std::move(coalesced); } inline bool range_error(Request &req, Response &res) { if (!req.ranges.empty() && 200 <= res.status && res.status < 300) { ssize_t content_len = static_cast( res.content_length_ ? res.content_length_ : res.body.size()); std::vector> processed_ranges; size_t overwrapping_count = 0; // NOTE: The following Range check is based on '14.2. Range' in RFC 9110 // 'HTTP Semantics' to avoid potential denial-of-service attacks. // https://www.rfc-editor.org/rfc/rfc9110#section-14.2 // Too many ranges if (req.ranges.size() > CPPHTTPLIB_RANGE_MAX_COUNT) { return true; } for (auto &r : req.ranges) { auto &first_pos = r.first; auto &last_pos = r.second; if (first_pos == -1 && last_pos == -1) { first_pos = 0; last_pos = content_len; } if (first_pos == -1) { first_pos = content_len - last_pos; last_pos = content_len - 1; } // NOTE: RFC-9110 '14.1.2. Byte Ranges': // A client can limit the number of bytes requested without knowing the // size of the selected representation. If the last-pos value is absent, // or if the value is greater than or equal to the current length of the // representation data, the byte range is interpreted as the remainder of // the representation (i.e., the server replaces the value of last-pos // with a value that is one less than the current length of the selected // representation). // https://www.rfc-editor.org/rfc/rfc9110.html#section-14.1.2-6 if (last_pos == -1 || last_pos >= content_len) { last_pos = content_len - 1; } // Range must be within content length if (!(0 <= first_pos && first_pos <= last_pos && last_pos <= content_len - 1)) { return true; } // Request must not have more than two overlapping ranges for (const auto &processed_range : processed_ranges) { if (!(last_pos < processed_range.first || first_pos > processed_range.second)) { overwrapping_count++; if (overwrapping_count > 2) { return true; } break; // Only count once per range } } processed_ranges.emplace_back(first_pos, last_pos); } // After validation, coalesce overlapping ranges as per RFC 9110 coalesce_ranges(req.ranges, static_cast(content_len)); } return false; } inline std::pair get_range_offset_and_length(Range r, size_t content_length) { assert(r.first != -1 && r.second != -1); assert(0 <= r.first && r.first < static_cast(content_length)); assert(r.first <= r.second && r.second < static_cast(content_length)); (void)(content_length); return std::make_pair(r.first, static_cast(r.second - r.first) + 1); } inline std::string make_content_range_header_field( const std::pair &offset_and_length, size_t content_length) { auto st = offset_and_length.first; auto ed = st + offset_and_length.second - 1; std::string field = "bytes "; field += std::to_string(st); field += '-'; field += std::to_string(ed); field += '/'; field += std::to_string(content_length); return field; } template bool process_multipart_ranges_data(const Request &req, const std::string &boundary, const std::string &content_type, size_t content_length, SToken stoken, CToken ctoken, Content content) { for (size_t i = 0; i < req.ranges.size(); i++) { ctoken("--"); stoken(boundary); ctoken("\r\n"); if (!content_type.empty()) { ctoken("Content-Type: "); stoken(content_type); ctoken("\r\n"); } auto offset_and_length = get_range_offset_and_length(req.ranges[i], content_length); ctoken("Content-Range: "); stoken(make_content_range_header_field(offset_and_length, content_length)); ctoken("\r\n"); ctoken("\r\n"); if (!content(offset_and_length.first, offset_and_length.second)) { return false; } ctoken("\r\n"); } ctoken("--"); stoken(boundary); ctoken("--"); return true; } inline void make_multipart_ranges_data(const Request &req, Response &res, const std::string &boundary, const std::string &content_type, size_t content_length, std::string &data) { process_multipart_ranges_data( req, boundary, content_type, content_length, [&](const std::string &token) { data += token; }, [&](const std::string &token) { data += token; }, [&](size_t offset, size_t length) { assert(offset + length <= content_length); data += res.body.substr(offset, length); return true; }); } inline size_t get_multipart_ranges_data_length(const Request &req, const std::string &boundary, const std::string &content_type, size_t content_length) { size_t data_length = 0; process_multipart_ranges_data( req, boundary, content_type, content_length, [&](const std::string &token) { data_length += token.size(); }, [&](const std::string &token) { data_length += token.size(); }, [&](size_t /*offset*/, size_t length) { data_length += length; return true; }); return data_length; } template inline bool write_multipart_ranges_data(Stream &strm, const Request &req, Response &res, const std::string &boundary, const std::string &content_type, size_t content_length, const T &is_shutting_down) { return process_multipart_ranges_data( req, boundary, content_type, content_length, [&](const std::string &token) { strm.write(token); }, [&](const std::string &token) { strm.write(token); }, [&](size_t offset, size_t length) { return write_content(strm, res.content_provider_, offset, length, is_shutting_down); }); } inline bool expect_content(const Request &req) { if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" || req.method == "DELETE") { return true; } if (req.has_header("Content-Length") && req.get_header_value_u64("Content-Length") > 0) { return true; } if (is_chunked_transfer_encoding(req.headers)) { return true; } return false; } inline bool has_crlf(const std::string &s) { auto p = s.c_str(); while (*p) { if (*p == '\r' || *p == '\n') { return true; } p++; } return false; } #ifdef _WIN32 class WSInit { public: WSInit() { WSADATA wsaData; if (WSAStartup(0x0002, &wsaData) == 0) is_valid_ = true; } ~WSInit() { if (is_valid_) WSACleanup(); } bool is_valid_ = false; }; static WSInit wsinit_; #endif inline bool parse_www_authenticate(const Response &res, std::map &auth, bool is_proxy) { auto auth_key = is_proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; if (res.has_header(auth_key)) { thread_local auto re = std::regex(R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~"); auto s = res.get_header_value(auth_key); auto pos = s.find(' '); if (pos != std::string::npos) { auto type = s.substr(0, pos); if (type == "Basic") { return false; } else if (type == "Digest") { s = s.substr(pos + 1); auto beg = std::sregex_iterator(s.begin(), s.end(), re); for (auto i = beg; i != std::sregex_iterator(); ++i) { const auto &m = *i; auto key = s.substr(static_cast(m.position(1)), static_cast(m.length(1))); auto val = m.length(2) > 0 ? s.substr(static_cast(m.position(2)), static_cast(m.length(2))) : s.substr(static_cast(m.position(3)), static_cast(m.length(3))); auth[std::move(key)] = std::move(val); } return true; } } } return false; } class ContentProviderAdapter { public: explicit ContentProviderAdapter( ContentProviderWithoutLength &&content_provider) : content_provider_(std::move(content_provider)) {} bool operator()(size_t offset, size_t, DataSink &sink) { return content_provider_(offset, sink); } private: ContentProviderWithoutLength content_provider_; }; // NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5 namespace fields { inline bool is_token_char(char c) { return std::isalnum(c) || c == '!' || c == '#' || c == '$' || c == '%' || c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' || c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~'; } inline bool is_token(const std::string &s) { if (s.empty()) { return false; } for (auto c : s) { if (!is_token_char(c)) { return false; } } return true; } inline bool is_field_name(const std::string &s) { return is_token(s); } inline bool is_vchar(char c) { return c >= 33 && c <= 126; } inline bool is_obs_text(char c) { return 128 <= static_cast(c); } inline bool is_field_vchar(char c) { return is_vchar(c) || is_obs_text(c); } inline bool is_field_content(const std::string &s) { if (s.empty()) { return true; } if (s.size() == 1) { return is_field_vchar(s[0]); } else if (s.size() == 2) { return is_field_vchar(s[0]) && is_field_vchar(s[1]); } else { size_t i = 0; if (!is_field_vchar(s[i])) { return false; } i++; while (i < s.size() - 1) { auto c = s[i++]; if (c == ' ' || c == '\t' || is_field_vchar(c)) { } else { return false; } } return is_field_vchar(s[i]); } } inline bool is_field_value(const std::string &s) { return is_field_content(s); } } // namespace fields } // namespace detail /* * Group 2: detail namespace - SSL common utilities */ #ifdef CPPHTTPLIB_SSL_ENABLED namespace detail { class SSLSocketStream final : public Stream { public: SSLSocketStream( socket_t sock, tls::session_t session, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec = 0, std::chrono::time_point start_time = (std::chrono::steady_clock::time_point::min)()); ~SSLSocketStream() override; bool is_readable() const override; bool wait_readable() const override; bool wait_writable() const override; ssize_t read(char *ptr, size_t size) override; ssize_t write(const char *ptr, size_t size) override; void get_remote_ip_and_port(std::string &ip, int &port) const override; void get_local_ip_and_port(std::string &ip, int &port) const override; socket_t socket() const override; time_t duration() const override; private: socket_t sock_; tls::session_t session_; time_t read_timeout_sec_; time_t read_timeout_usec_; time_t write_timeout_sec_; time_t write_timeout_usec_; time_t max_timeout_msec_; const std::chrono::time_point start_time_; }; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT inline std::string message_digest(const std::string &s, const EVP_MD *algo) { auto context = std::unique_ptr( EVP_MD_CTX_new(), EVP_MD_CTX_free); unsigned int hash_length = 0; unsigned char hash[EVP_MAX_MD_SIZE]; EVP_DigestInit_ex(context.get(), algo, nullptr); EVP_DigestUpdate(context.get(), s.c_str(), s.size()); EVP_DigestFinal_ex(context.get(), hash, &hash_length); std::stringstream ss; for (auto i = 0u; i < hash_length; ++i) { ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(hash[i]); } return ss.str(); } inline std::string MD5(const std::string &s) { return message_digest(s, EVP_md5()); } inline std::string SHA_256(const std::string &s) { return message_digest(s, EVP_sha256()); } inline std::string SHA_512(const std::string &s) { return message_digest(s, EVP_sha512()); } #elif defined(CPPHTTPLIB_MBEDTLS_SUPPORT) namespace { template inline std::string hash_to_hex(const unsigned char (&hash)[N]) { std::stringstream ss; for (size_t i = 0; i < N; ++i) { ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(hash[i]); } return ss.str(); } } // namespace inline std::string MD5(const std::string &s) { unsigned char hash[16]; #ifdef CPPHTTPLIB_MBEDTLS_V3 mbedtls_md5(reinterpret_cast(s.c_str()), s.size(), hash); #else mbedtls_md5_ret(reinterpret_cast(s.c_str()), s.size(), hash); #endif return hash_to_hex(hash); } inline std::string SHA_256(const std::string &s) { unsigned char hash[32]; #ifdef CPPHTTPLIB_MBEDTLS_V3 mbedtls_sha256(reinterpret_cast(s.c_str()), s.size(), hash, 0); #else mbedtls_sha256_ret(reinterpret_cast(s.c_str()), s.size(), hash, 0); #endif return hash_to_hex(hash); } inline std::string SHA_512(const std::string &s) { unsigned char hash[64]; #ifdef CPPHTTPLIB_MBEDTLS_V3 mbedtls_sha512(reinterpret_cast(s.c_str()), s.size(), hash, 0); #else mbedtls_sha512_ret(reinterpret_cast(s.c_str()), s.size(), hash, 0); #endif return hash_to_hex(hash); } #endif inline bool is_ip_address(const std::string &host) { struct in_addr addr4; struct in6_addr addr6; return inet_pton(AF_INET, host.c_str(), &addr4) == 1 || inet_pton(AF_INET6, host.c_str(), &addr6) == 1; } template inline bool process_server_socket_ssl( const std::atomic &svr_sock, tls::session_t session, socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, T callback) { return process_server_socket_core( svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec, [&](bool close_connection, bool &connection_closed) { SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec); return callback(strm, close_connection, connection_closed); }); } template inline bool process_client_socket_ssl( tls::session_t session, socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec, std::chrono::time_point start_time, T callback) { SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec, max_timeout_msec, start_time); return callback(strm); } inline std::pair make_digest_authentication_header( const Request &req, const std::map &auth, size_t cnonce_count, const std::string &cnonce, const std::string &username, const std::string &password, bool is_proxy = false) { std::string nc; { std::stringstream ss; ss << std::setfill('0') << std::setw(8) << std::hex << cnonce_count; nc = ss.str(); } std::string qop; if (auth.find("qop") != auth.end()) { qop = auth.at("qop"); if (qop.find("auth-int") != std::string::npos) { qop = "auth-int"; } else if (qop.find("auth") != std::string::npos) { qop = "auth"; } else { qop.clear(); } } std::string algo = "MD5"; if (auth.find("algorithm") != auth.end()) { algo = auth.at("algorithm"); } std::string response; { auto H = algo == "SHA-256" ? detail::SHA_256 : algo == "SHA-512" ? detail::SHA_512 : detail::MD5; auto A1 = username + ":" + auth.at("realm") + ":" + password; auto A2 = req.method + ":" + req.path; if (qop == "auth-int") { A2 += ":" + H(req.body); } if (qop.empty()) { response = H(H(A1) + ":" + auth.at("nonce") + ":" + H(A2)); } else { response = H(H(A1) + ":" + auth.at("nonce") + ":" + nc + ":" + cnonce + ":" + qop + ":" + H(A2)); } } auto opaque = (auth.find("opaque") != auth.end()) ? auth.at("opaque") : ""; auto field = "Digest username=\"" + username + "\", realm=\"" + auth.at("realm") + "\", nonce=\"" + auth.at("nonce") + "\", uri=\"" + req.path + "\", algorithm=" + algo + (qop.empty() ? ", response=\"" : ", qop=" + qop + ", nc=" + nc + ", cnonce=\"" + cnonce + "\", response=\"") + response + "\"" + (opaque.empty() ? "" : ", opaque=\"" + opaque + "\""); auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; return std::make_pair(key, field); } inline bool match_hostname(const std::string &pattern, const std::string &hostname) { // Exact match (case-insensitive) if (detail::case_ignore::equal(hostname, pattern)) { return true; } // Split both pattern and hostname into components by '.' std::vector pattern_components; if (!pattern.empty()) { split(pattern.data(), pattern.data() + pattern.size(), '.', [&](const char *b, const char *e) { pattern_components.emplace_back(b, e); }); } std::vector host_components; if (!hostname.empty()) { split(hostname.data(), hostname.data() + hostname.size(), '.', [&](const char *b, const char *e) { host_components.emplace_back(b, e); }); } // Component count must match if (host_components.size() != pattern_components.size()) { return false; } // Compare each component with wildcard support // Supports: "*" (full wildcard), "prefix*" (partial wildcard) // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484 auto itr = pattern_components.begin(); for (const auto &h : host_components) { auto &p = *itr; if (!detail::case_ignore::equal(p, h) && p != "*") { bool partial_match = false; if (!p.empty() && p[p.size() - 1] == '*') { const auto prefix_length = p.size() - 1; if (prefix_length == 0) { partial_match = true; } else if (h.size() >= prefix_length) { partial_match = std::equal(p.begin(), p.begin() + static_cast( prefix_length), h.begin(), [](const char ca, const char cb) { return detail::case_ignore::to_lower(ca) == detail::case_ignore::to_lower(cb); }); } } if (!partial_match) { return false; } } ++itr; } return true; } #ifdef _WIN32 // Verify certificate using Windows CertGetCertificateChain API. // This provides real-time certificate validation with Windows Update // integration, independent of the TLS backend (OpenSSL or MbedTLS). inline bool verify_cert_with_windows_schannel( const std::vector &der_cert, const std::string &hostname, bool verify_hostname, unsigned long &out_error) { if (der_cert.empty()) { return false; } out_error = 0; // Create Windows certificate context from DER data auto cert_context = CertCreateCertificateContext( X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, der_cert.data(), static_cast(der_cert.size())); if (!cert_context) { out_error = GetLastError(); return false; } auto cert_guard = scope_exit([&] { CertFreeCertificateContext(cert_context); }); // Setup chain parameters CERT_CHAIN_PARA chain_para = {}; chain_para.cbSize = sizeof(chain_para); // Build certificate chain with revocation checking PCCERT_CHAIN_CONTEXT chain_context = nullptr; auto chain_result = CertGetCertificateChain( nullptr, cert_context, nullptr, cert_context->hCertStore, &chain_para, CERT_CHAIN_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_END_CERT | CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT, nullptr, &chain_context); if (!chain_result || !chain_context) { out_error = GetLastError(); return false; } auto chain_guard = scope_exit([&] { CertFreeCertificateChain(chain_context); }); // Check if chain has errors if (chain_context->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) { out_error = chain_context->TrustStatus.dwErrorStatus; return false; } // Verify SSL policy SSL_EXTRA_CERT_CHAIN_POLICY_PARA extra_policy_para = {}; extra_policy_para.cbSize = sizeof(extra_policy_para); #ifdef AUTHTYPE_SERVER extra_policy_para.dwAuthType = AUTHTYPE_SERVER; #endif std::wstring whost; if (verify_hostname) { whost = u8string_to_wstring(hostname.c_str()); extra_policy_para.pwszServerName = const_cast(whost.c_str()); } CERT_CHAIN_POLICY_PARA policy_para = {}; policy_para.cbSize = sizeof(policy_para); #ifdef CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS policy_para.dwFlags = CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS; #else policy_para.dwFlags = 0; #endif policy_para.pvExtraPolicyPara = &extra_policy_para; CERT_CHAIN_POLICY_STATUS policy_status = {}; policy_status.cbSize = sizeof(policy_status); if (!CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, chain_context, &policy_para, &policy_status)) { out_error = GetLastError(); return false; } if (policy_status.dwError != 0) { out_error = policy_status.dwError; return false; } return true; } #endif // _WIN32 } // namespace detail #endif // CPPHTTPLIB_SSL_ENABLED /* * Group 3: httplib namespace - Non-SSL public API implementations */ inline void default_socket_options(socket_t sock) { detail::set_socket_opt(sock, SOL_SOCKET, #ifdef SO_REUSEPORT SO_REUSEPORT, #else SO_REUSEADDR, #endif 1); } inline std::string get_bearer_token_auth(const Request &req) { if (req.has_header("Authorization")) { constexpr auto bearer_header_prefix_len = detail::str_len("Bearer "); return req.get_header_value("Authorization") .substr(bearer_header_prefix_len); } return ""; } inline const char *status_message(int status) { switch (status) { case StatusCode::Continue_100: return "Continue"; case StatusCode::SwitchingProtocol_101: return "Switching Protocol"; case StatusCode::Processing_102: return "Processing"; case StatusCode::EarlyHints_103: return "Early Hints"; case StatusCode::OK_200: return "OK"; case StatusCode::Created_201: return "Created"; case StatusCode::Accepted_202: return "Accepted"; case StatusCode::NonAuthoritativeInformation_203: return "Non-Authoritative Information"; case StatusCode::NoContent_204: return "No Content"; case StatusCode::ResetContent_205: return "Reset Content"; case StatusCode::PartialContent_206: return "Partial Content"; case StatusCode::MultiStatus_207: return "Multi-Status"; case StatusCode::AlreadyReported_208: return "Already Reported"; case StatusCode::IMUsed_226: return "IM Used"; case StatusCode::MultipleChoices_300: return "Multiple Choices"; case StatusCode::MovedPermanently_301: return "Moved Permanently"; case StatusCode::Found_302: return "Found"; case StatusCode::SeeOther_303: return "See Other"; case StatusCode::NotModified_304: return "Not Modified"; case StatusCode::UseProxy_305: return "Use Proxy"; case StatusCode::unused_306: return "unused"; case StatusCode::TemporaryRedirect_307: return "Temporary Redirect"; case StatusCode::PermanentRedirect_308: return "Permanent Redirect"; case StatusCode::BadRequest_400: return "Bad Request"; case StatusCode::Unauthorized_401: return "Unauthorized"; case StatusCode::PaymentRequired_402: return "Payment Required"; case StatusCode::Forbidden_403: return "Forbidden"; case StatusCode::NotFound_404: return "Not Found"; case StatusCode::MethodNotAllowed_405: return "Method Not Allowed"; case StatusCode::NotAcceptable_406: return "Not Acceptable"; case StatusCode::ProxyAuthenticationRequired_407: return "Proxy Authentication Required"; case StatusCode::RequestTimeout_408: return "Request Timeout"; case StatusCode::Conflict_409: return "Conflict"; case StatusCode::Gone_410: return "Gone"; case StatusCode::LengthRequired_411: return "Length Required"; case StatusCode::PreconditionFailed_412: return "Precondition Failed"; case StatusCode::PayloadTooLarge_413: return "Payload Too Large"; case StatusCode::UriTooLong_414: return "URI Too Long"; case StatusCode::UnsupportedMediaType_415: return "Unsupported Media Type"; case StatusCode::RangeNotSatisfiable_416: return "Range Not Satisfiable"; case StatusCode::ExpectationFailed_417: return "Expectation Failed"; case StatusCode::ImATeapot_418: return "I'm a teapot"; case StatusCode::MisdirectedRequest_421: return "Misdirected Request"; case StatusCode::UnprocessableContent_422: return "Unprocessable Content"; case StatusCode::Locked_423: return "Locked"; case StatusCode::FailedDependency_424: return "Failed Dependency"; case StatusCode::TooEarly_425: return "Too Early"; case StatusCode::UpgradeRequired_426: return "Upgrade Required"; case StatusCode::PreconditionRequired_428: return "Precondition Required"; case StatusCode::TooManyRequests_429: return "Too Many Requests"; case StatusCode::RequestHeaderFieldsTooLarge_431: return "Request Header Fields Too Large"; case StatusCode::UnavailableForLegalReasons_451: return "Unavailable For Legal Reasons"; case StatusCode::NotImplemented_501: return "Not Implemented"; case StatusCode::BadGateway_502: return "Bad Gateway"; case StatusCode::ServiceUnavailable_503: return "Service Unavailable"; case StatusCode::GatewayTimeout_504: return "Gateway Timeout"; case StatusCode::HttpVersionNotSupported_505: return "HTTP Version Not Supported"; case StatusCode::VariantAlsoNegotiates_506: return "Variant Also Negotiates"; case StatusCode::InsufficientStorage_507: return "Insufficient Storage"; case StatusCode::LoopDetected_508: return "Loop Detected"; case StatusCode::NotExtended_510: return "Not Extended"; case StatusCode::NetworkAuthenticationRequired_511: return "Network Authentication Required"; default: case StatusCode::InternalServerError_500: return "Internal Server Error"; } } inline std::string to_string(const Error error) { switch (error) { case Error::Success: return "Success (no error)"; case Error::Unknown: return "Unknown"; case Error::Connection: return "Could not establish connection"; case Error::BindIPAddress: return "Failed to bind IP address"; case Error::Read: return "Failed to read connection"; case Error::Write: return "Failed to write connection"; case Error::ExceedRedirectCount: return "Maximum redirect count exceeded"; case Error::Canceled: return "Connection handling canceled"; case Error::SSLConnection: return "SSL connection failed"; case Error::SSLLoadingCerts: return "SSL certificate loading failed"; case Error::SSLServerVerification: return "SSL server verification failed"; case Error::SSLServerHostnameVerification: return "SSL server hostname verification failed"; case Error::UnsupportedMultipartBoundaryChars: return "Unsupported HTTP multipart boundary characters"; case Error::Compression: return "Compression failed"; case Error::ConnectionTimeout: return "Connection timed out"; case Error::ProxyConnection: return "Proxy connection failed"; case Error::ConnectionClosed: return "Connection closed by server"; case Error::Timeout: return "Read timeout"; case Error::ResourceExhaustion: return "Resource exhaustion"; case Error::TooManyFormDataFiles: return "Too many form data files"; case Error::ExceedMaxPayloadSize: return "Exceeded maximum payload size"; case Error::ExceedUriMaxLength: return "Exceeded maximum URI length"; case Error::ExceedMaxSocketDescriptorCount: return "Exceeded maximum socket descriptor count"; case Error::InvalidRequestLine: return "Invalid request line"; case Error::InvalidHTTPMethod: return "Invalid HTTP method"; case Error::InvalidHTTPVersion: return "Invalid HTTP version"; case Error::InvalidHeaders: return "Invalid headers"; case Error::MultipartParsing: return "Multipart parsing failed"; case Error::OpenFile: return "Failed to open file"; case Error::Listen: return "Failed to listen on socket"; case Error::GetSockName: return "Failed to get socket name"; case Error::UnsupportedAddressFamily: return "Unsupported address family"; case Error::HTTPParsing: return "HTTP parsing failed"; case Error::InvalidRangeHeader: return "Invalid Range header"; default: break; } return "Invalid"; } inline std::ostream &operator<<(std::ostream &os, const Error &obj) { os << to_string(obj); os << " (" << static_cast::type>(obj) << ')'; return os; } inline std::string hosted_at(const std::string &hostname) { std::vector addrs; hosted_at(hostname, addrs); if (addrs.empty()) { return std::string(); } return addrs[0]; } inline void hosted_at(const std::string &hostname, std::vector &addrs) { struct addrinfo hints; struct addrinfo *result; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; if (detail::getaddrinfo_with_timeout(hostname.c_str(), nullptr, &hints, &result, 0)) { #if defined __linux__ && !defined __ANDROID__ res_init(); #endif return; } auto se = detail::scope_exit([&] { freeaddrinfo(result); }); for (auto rp = result; rp; rp = rp->ai_next) { const auto &addr = *reinterpret_cast(rp->ai_addr); std::string ip; auto dummy = -1; if (detail::get_ip_and_port(addr, sizeof(struct sockaddr_storage), ip, dummy)) { addrs.emplace_back(std::move(ip)); } } } inline std::string encode_uri_component(const std::string &value) { std::ostringstream escaped; escaped.fill('0'); escaped << std::hex; for (auto c : value) { if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || c == ')') { escaped << c; } else { escaped << std::uppercase; escaped << '%' << std::setw(2) << static_cast(static_cast(c)); escaped << std::nouppercase; } } return escaped.str(); } inline std::string encode_uri(const std::string &value) { std::ostringstream escaped; escaped.fill('0'); escaped << std::hex; for (auto c : value) { if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || c == ')' || c == ';' || c == '/' || c == '?' || c == ':' || c == '@' || c == '&' || c == '=' || c == '+' || c == '$' || c == ',' || c == '#') { escaped << c; } else { escaped << std::uppercase; escaped << '%' << std::setw(2) << static_cast(static_cast(c)); escaped << std::nouppercase; } } return escaped.str(); } inline std::string decode_uri_component(const std::string &value) { std::string result; for (size_t i = 0; i < value.size(); i++) { if (value[i] == '%' && i + 2 < value.size()) { auto val = 0; if (detail::from_hex_to_i(value, i + 1, 2, val)) { result += static_cast(val); i += 2; } else { result += value[i]; } } else { result += value[i]; } } return result; } inline std::string decode_uri(const std::string &value) { std::string result; for (size_t i = 0; i < value.size(); i++) { if (value[i] == '%' && i + 2 < value.size()) { auto val = 0; if (detail::from_hex_to_i(value, i + 1, 2, val)) { result += static_cast(val); i += 2; } else { result += value[i]; } } else { result += value[i]; } } return result; } inline std::string encode_path_component(const std::string &component) { std::string result; result.reserve(component.size() * 3); for (size_t i = 0; i < component.size(); i++) { auto c = static_cast(component[i]); // Unreserved characters per RFC 3986: ALPHA / DIGIT / "-" / "." / "_" / "~" if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') { result += static_cast(c); } // Path-safe sub-delimiters: "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / // "," / ";" / "=" else if (c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || c == ')' || c == '*' || c == '+' || c == ',' || c == ';' || c == '=') { result += static_cast(c); } // Colon is allowed in path segments except first segment else if (c == ':') { result += static_cast(c); } // @ is allowed in path else if (c == '@') { result += static_cast(c); } else { result += '%'; char hex[3]; snprintf(hex, sizeof(hex), "%02X", c); result.append(hex, 2); } } return result; } inline std::string decode_path_component(const std::string &component) { std::string result; result.reserve(component.size()); for (size_t i = 0; i < component.size(); i++) { if (component[i] == '%' && i + 1 < component.size()) { if (component[i + 1] == 'u') { // Unicode %uXXXX encoding auto val = 0; if (detail::from_hex_to_i(component, i + 2, 4, val)) { // 4 digits Unicode codes char buff[4]; size_t len = detail::to_utf8(val, buff); if (len > 0) { result.append(buff, len); } i += 5; // 'u0000' } else { result += component[i]; } } else { // Standard %XX encoding auto val = 0; if (detail::from_hex_to_i(component, i + 1, 2, val)) { // 2 digits hex codes result += static_cast(val); i += 2; // 'XX' } else { result += component[i]; } } } else { result += component[i]; } } return result; } inline std::string encode_query_component(const std::string &component, bool space_as_plus) { std::string result; result.reserve(component.size() * 3); for (size_t i = 0; i < component.size(); i++) { auto c = static_cast(component[i]); // Unreserved characters per RFC 3986 if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') { result += static_cast(c); } // Space handling else if (c == ' ') { if (space_as_plus) { result += '+'; } else { result += "%20"; } } // Plus sign handling else if (c == '+') { if (space_as_plus) { result += "%2B"; } else { result += static_cast(c); } } // Query-safe sub-delimiters (excluding & and = which are query delimiters) else if (c == '!' || c == '$' || c == '\'' || c == '(' || c == ')' || c == '*' || c == ',' || c == ';') { result += static_cast(c); } // Colon and @ are allowed in query else if (c == ':' || c == '@') { result += static_cast(c); } // Forward slash is allowed in query values else if (c == '/') { result += static_cast(c); } // Question mark is allowed in query values (after first ?) else if (c == '?') { result += static_cast(c); } else { result += '%'; char hex[3]; snprintf(hex, sizeof(hex), "%02X", c); result.append(hex, 2); } } return result; } inline std::string decode_query_component(const std::string &component, bool plus_as_space) { std::string result; result.reserve(component.size()); for (size_t i = 0; i < component.size(); i++) { if (component[i] == '%' && i + 2 < component.size()) { std::string hex = component.substr(i + 1, 2); char *end; unsigned long value = std::strtoul(hex.c_str(), &end, 16); if (end == hex.c_str() + 2) { result += static_cast(value); i += 2; } else { result += component[i]; } } else if (component[i] == '+' && plus_as_space) { result += ' '; // + becomes space in form-urlencoded } else { result += component[i]; } } return result; } inline std::string append_query_params(const std::string &path, const Params ¶ms) { std::string path_with_query = path; thread_local const std::regex re("[^?]+\\?.*"); auto delm = std::regex_match(path, re) ? '&' : '?'; path_with_query += delm + detail::params_to_query_str(params); return path_with_query; } // Header utilities inline std::pair make_range_header(const Ranges &ranges) { std::string field = "bytes="; auto i = 0; for (const auto &r : ranges) { if (i != 0) { field += ", "; } if (r.first != -1) { field += std::to_string(r.first); } field += '-'; if (r.second != -1) { field += std::to_string(r.second); } i++; } return std::make_pair("Range", std::move(field)); } inline std::pair make_basic_authentication_header(const std::string &username, const std::string &password, bool is_proxy) { auto field = "Basic " + detail::base64_encode(username + ":" + password); auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; return std::make_pair(key, std::move(field)); } inline std::pair make_bearer_token_authentication_header(const std::string &token, bool is_proxy = false) { auto field = "Bearer " + token; auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; return std::make_pair(key, std::move(field)); } // Request implementation inline size_t Request::get_header_value_u64(const std::string &key, size_t def, size_t id) const { return detail::get_header_value_u64(headers, key, def, id); } inline bool Request::has_header(const std::string &key) const { return detail::has_header(headers, key); } inline std::string Request::get_header_value(const std::string &key, const char *def, size_t id) const { return detail::get_header_value(headers, key, def, id); } inline size_t Request::get_header_value_count(const std::string &key) const { auto r = headers.equal_range(key); return static_cast(std::distance(r.first, r.second)); } inline void Request::set_header(const std::string &key, const std::string &val) { if (detail::fields::is_field_name(key) && detail::fields::is_field_value(val)) { headers.emplace(key, val); } } inline bool Request::has_trailer(const std::string &key) const { return trailers.find(key) != trailers.end(); } inline std::string Request::get_trailer_value(const std::string &key, size_t id) const { auto rng = trailers.equal_range(key); auto it = rng.first; std::advance(it, static_cast(id)); if (it != rng.second) { return it->second; } return std::string(); } inline size_t Request::get_trailer_value_count(const std::string &key) const { auto r = trailers.equal_range(key); return static_cast(std::distance(r.first, r.second)); } inline bool Request::has_param(const std::string &key) const { return params.find(key) != params.end(); } inline std::string Request::get_param_value(const std::string &key, size_t id) const { auto rng = params.equal_range(key); auto it = rng.first; std::advance(it, static_cast(id)); if (it != rng.second) { return it->second; } return std::string(); } inline size_t Request::get_param_value_count(const std::string &key) const { auto r = params.equal_range(key); return static_cast(std::distance(r.first, r.second)); } inline bool Request::is_multipart_form_data() const { const auto &content_type = get_header_value("Content-Type"); return !content_type.rfind("multipart/form-data", 0); } // Multipart FormData implementation inline std::string MultipartFormData::get_field(const std::string &key, size_t id) const { auto rng = fields.equal_range(key); auto it = rng.first; std::advance(it, static_cast(id)); if (it != rng.second) { return it->second.content; } return std::string(); } inline std::vector MultipartFormData::get_fields(const std::string &key) const { std::vector values; auto rng = fields.equal_range(key); for (auto it = rng.first; it != rng.second; it++) { values.push_back(it->second.content); } return values; } inline bool MultipartFormData::has_field(const std::string &key) const { return fields.find(key) != fields.end(); } inline size_t MultipartFormData::get_field_count(const std::string &key) const { auto r = fields.equal_range(key); return static_cast(std::distance(r.first, r.second)); } inline FormData MultipartFormData::get_file(const std::string &key, size_t id) const { auto rng = files.equal_range(key); auto it = rng.first; std::advance(it, static_cast(id)); if (it != rng.second) { return it->second; } return FormData(); } inline std::vector MultipartFormData::get_files(const std::string &key) const { std::vector values; auto rng = files.equal_range(key); for (auto it = rng.first; it != rng.second; it++) { values.push_back(it->second); } return values; } inline bool MultipartFormData::has_file(const std::string &key) const { return files.find(key) != files.end(); } inline size_t MultipartFormData::get_file_count(const std::string &key) const { auto r = files.equal_range(key); return static_cast(std::distance(r.first, r.second)); } // Response implementation inline size_t Response::get_header_value_u64(const std::string &key, size_t def, size_t id) const { return detail::get_header_value_u64(headers, key, def, id); } inline bool Response::has_header(const std::string &key) const { return headers.find(key) != headers.end(); } inline std::string Response::get_header_value(const std::string &key, const char *def, size_t id) const { return detail::get_header_value(headers, key, def, id); } inline size_t Response::get_header_value_count(const std::string &key) const { auto r = headers.equal_range(key); return static_cast(std::distance(r.first, r.second)); } inline void Response::set_header(const std::string &key, const std::string &val) { if (detail::fields::is_field_name(key) && detail::fields::is_field_value(val)) { headers.emplace(key, val); } } inline bool Response::has_trailer(const std::string &key) const { return trailers.find(key) != trailers.end(); } inline std::string Response::get_trailer_value(const std::string &key, size_t id) const { auto rng = trailers.equal_range(key); auto it = rng.first; std::advance(it, static_cast(id)); if (it != rng.second) { return it->second; } return std::string(); } inline size_t Response::get_trailer_value_count(const std::string &key) const { auto r = trailers.equal_range(key); return static_cast(std::distance(r.first, r.second)); } inline void Response::set_redirect(const std::string &url, int stat) { if (detail::fields::is_field_value(url)) { set_header("Location", url); if (300 <= stat && stat < 400) { this->status = stat; } else { this->status = StatusCode::Found_302; } } } inline void Response::set_content(const char *s, size_t n, const std::string &content_type) { body.assign(s, n); auto rng = headers.equal_range("Content-Type"); headers.erase(rng.first, rng.second); set_header("Content-Type", content_type); } inline void Response::set_content(const std::string &s, const std::string &content_type) { set_content(s.data(), s.size(), content_type); } inline void Response::set_content(std::string &&s, const std::string &content_type) { body = std::move(s); auto rng = headers.equal_range("Content-Type"); headers.erase(rng.first, rng.second); set_header("Content-Type", content_type); } inline void Response::set_content_provider( size_t in_length, const std::string &content_type, ContentProvider provider, ContentProviderResourceReleaser resource_releaser) { set_header("Content-Type", content_type); content_length_ = in_length; if (in_length > 0) { content_provider_ = std::move(provider); } content_provider_resource_releaser_ = std::move(resource_releaser); is_chunked_content_provider_ = false; } inline void Response::set_content_provider( const std::string &content_type, ContentProviderWithoutLength provider, ContentProviderResourceReleaser resource_releaser) { set_header("Content-Type", content_type); content_length_ = 0; content_provider_ = detail::ContentProviderAdapter(std::move(provider)); content_provider_resource_releaser_ = std::move(resource_releaser); is_chunked_content_provider_ = false; } inline void Response::set_chunked_content_provider( const std::string &content_type, ContentProviderWithoutLength provider, ContentProviderResourceReleaser resource_releaser) { set_header("Content-Type", content_type); content_length_ = 0; content_provider_ = detail::ContentProviderAdapter(std::move(provider)); content_provider_resource_releaser_ = std::move(resource_releaser); is_chunked_content_provider_ = true; } inline void Response::set_file_content(const std::string &path, const std::string &content_type) { file_content_path_ = path; file_content_content_type_ = content_type; } inline void Response::set_file_content(const std::string &path) { file_content_path_ = path; } // Result implementation inline size_t Result::get_request_header_value_u64(const std::string &key, size_t def, size_t id) const { return detail::get_header_value_u64(request_headers_, key, def, id); } inline bool Result::has_request_header(const std::string &key) const { return request_headers_.find(key) != request_headers_.end(); } inline std::string Result::get_request_header_value(const std::string &key, const char *def, size_t id) const { return detail::get_header_value(request_headers_, key, def, id); } inline size_t Result::get_request_header_value_count(const std::string &key) const { auto r = request_headers_.equal_range(key); return static_cast(std::distance(r.first, r.second)); } // Stream implementation inline ssize_t Stream::write(const char *ptr) { return write(ptr, strlen(ptr)); } inline ssize_t Stream::write(const std::string &s) { return write(s.data(), s.size()); } // BodyReader implementation inline ssize_t detail::BodyReader::read(char *buf, size_t len) { if (!stream) { last_error = Error::Connection; return -1; } if (eof) { return 0; } if (!chunked) { // Content-Length based reading if (has_content_length && bytes_read >= content_length) { eof = true; return 0; } auto to_read = len; if (has_content_length) { auto remaining = content_length - bytes_read; to_read = (std::min)(len, remaining); } auto n = stream->read(buf, to_read); if (n < 0) { last_error = stream->get_error(); if (last_error == Error::Success) { last_error = Error::Read; } eof = true; return n; } if (n == 0) { // Unexpected EOF before content_length last_error = stream->get_error(); if (last_error == Error::Success) { last_error = Error::Read; } eof = true; return 0; } bytes_read += static_cast(n); if (has_content_length && bytes_read >= content_length) { eof = true; } if (payload_max_length > 0 && bytes_read > payload_max_length) { last_error = Error::ExceedMaxPayloadSize; eof = true; return -1; } return n; } // Chunked transfer encoding: delegate to shared decoder instance. if (!chunked_decoder) { chunked_decoder.reset(new ChunkedDecoder(*stream)); } size_t chunk_offset = 0; size_t chunk_total = 0; auto n = chunked_decoder->read_payload(buf, len, chunk_offset, chunk_total); if (n < 0) { last_error = stream->get_error(); if (last_error == Error::Success) { last_error = Error::Read; } eof = true; return n; } if (n == 0) { // Final chunk observed. Leave trailer parsing to the caller (StreamHandle). eof = true; return 0; } bytes_read += static_cast(n); if (payload_max_length > 0 && bytes_read > payload_max_length) { last_error = Error::ExceedMaxPayloadSize; eof = true; return -1; } return n; } // ThreadPool implementation inline ThreadPool::ThreadPool(size_t n, size_t mqr) : shutdown_(false), max_queued_requests_(mqr) { threads_.reserve(n); while (n) { threads_.emplace_back(worker(*this)); n--; } } inline bool ThreadPool::enqueue(std::function fn) { { std::unique_lock lock(mutex_); if (max_queued_requests_ > 0 && jobs_.size() >= max_queued_requests_) { return false; } jobs_.push_back(std::move(fn)); } cond_.notify_one(); return true; } inline void ThreadPool::shutdown() { // Stop all worker threads... { std::unique_lock lock(mutex_); shutdown_ = true; } cond_.notify_all(); // Join... for (auto &t : threads_) { t.join(); } } inline ThreadPool::worker::worker(ThreadPool &pool) : pool_(pool) {} inline void ThreadPool::worker::operator()() { for (;;) { std::function fn; { std::unique_lock lock(pool_.mutex_); pool_.cond_.wait(lock, [&] { return !pool_.jobs_.empty() || pool_.shutdown_; }); if (pool_.shutdown_ && pool_.jobs_.empty()) { break; } fn = pool_.jobs_.front(); pool_.jobs_.pop_front(); } assert(true == static_cast(fn)); fn(); } #if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && !defined(OPENSSL_IS_BORINGSSL) && \ !defined(LIBRESSL_VERSION_NUMBER) OPENSSL_thread_stop(); #endif } /* * Group 1 (continued): detail namespace - Stream implementations */ namespace detail { inline void calc_actual_timeout(time_t max_timeout_msec, time_t duration_msec, time_t timeout_sec, time_t timeout_usec, time_t &actual_timeout_sec, time_t &actual_timeout_usec) { auto timeout_msec = (timeout_sec * 1000) + (timeout_usec / 1000); auto actual_timeout_msec = (std::min)(max_timeout_msec - duration_msec, timeout_msec); if (actual_timeout_msec < 0) { actual_timeout_msec = 0; } actual_timeout_sec = actual_timeout_msec / 1000; actual_timeout_usec = (actual_timeout_msec % 1000) * 1000; } // Socket stream implementation inline SocketStream::SocketStream( socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec, std::chrono::time_point start_time) : sock_(sock), read_timeout_sec_(read_timeout_sec), read_timeout_usec_(read_timeout_usec), write_timeout_sec_(write_timeout_sec), write_timeout_usec_(write_timeout_usec), max_timeout_msec_(max_timeout_msec), start_time_(start_time), read_buff_(read_buff_size_, 0) {} inline SocketStream::~SocketStream() = default; inline bool SocketStream::is_readable() const { return read_buff_off_ < read_buff_content_size_; } inline bool SocketStream::wait_readable() const { if (max_timeout_msec_ <= 0) { return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; } time_t read_timeout_sec; time_t read_timeout_usec; calc_actual_timeout(max_timeout_msec_, duration(), read_timeout_sec_, read_timeout_usec_, read_timeout_sec, read_timeout_usec); return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0; } inline bool SocketStream::wait_writable() const { return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 && is_socket_alive(sock_); } inline ssize_t SocketStream::read(char *ptr, size_t size) { #ifdef _WIN32 size = (std::min)(size, static_cast((std::numeric_limits::max)())); #else size = (std::min)(size, static_cast((std::numeric_limits::max)())); #endif if (read_buff_off_ < read_buff_content_size_) { auto remaining_size = read_buff_content_size_ - read_buff_off_; if (size <= remaining_size) { memcpy(ptr, read_buff_.data() + read_buff_off_, size); read_buff_off_ += size; return static_cast(size); } else { memcpy(ptr, read_buff_.data() + read_buff_off_, remaining_size); read_buff_off_ += remaining_size; return static_cast(remaining_size); } } if (!wait_readable()) { error_ = Error::Timeout; return -1; } read_buff_off_ = 0; read_buff_content_size_ = 0; if (size < read_buff_size_) { auto n = read_socket(sock_, read_buff_.data(), read_buff_size_, CPPHTTPLIB_RECV_FLAGS); if (n <= 0) { if (n == 0) { error_ = Error::ConnectionClosed; } else { error_ = Error::Read; } return n; } else if (n <= static_cast(size)) { memcpy(ptr, read_buff_.data(), static_cast(n)); return n; } else { memcpy(ptr, read_buff_.data(), size); read_buff_off_ = size; read_buff_content_size_ = static_cast(n); return static_cast(size); } } else { auto n = read_socket(sock_, ptr, size, CPPHTTPLIB_RECV_FLAGS); if (n <= 0) { if (n == 0) { error_ = Error::ConnectionClosed; } else { error_ = Error::Read; } } return n; } } inline ssize_t SocketStream::write(const char *ptr, size_t size) { if (!wait_writable()) { return -1; } #if defined(_WIN32) && !defined(_WIN64) size = (std::min)(size, static_cast((std::numeric_limits::max)())); #endif return send_socket(sock_, ptr, size, CPPHTTPLIB_SEND_FLAGS); } inline void SocketStream::get_remote_ip_and_port(std::string &ip, int &port) const { return detail::get_remote_ip_and_port(sock_, ip, port); } inline void SocketStream::get_local_ip_and_port(std::string &ip, int &port) const { return detail::get_local_ip_and_port(sock_, ip, port); } inline socket_t SocketStream::socket() const { return sock_; } inline time_t SocketStream::duration() const { return std::chrono::duration_cast( std::chrono::steady_clock::now() - start_time_) .count(); } // Buffer stream implementation inline bool BufferStream::is_readable() const { return true; } inline bool BufferStream::wait_readable() const { return true; } inline bool BufferStream::wait_writable() const { return true; } inline ssize_t BufferStream::read(char *ptr, size_t size) { #if defined(_MSC_VER) && _MSC_VER < 1910 auto len_read = buffer._Copy_s(ptr, size, size, position); #else auto len_read = buffer.copy(ptr, size, position); #endif position += static_cast(len_read); return static_cast(len_read); } inline ssize_t BufferStream::write(const char *ptr, size_t size) { buffer.append(ptr, size); return static_cast(size); } inline void BufferStream::get_remote_ip_and_port(std::string & /*ip*/, int & /*port*/) const {} inline void BufferStream::get_local_ip_and_port(std::string & /*ip*/, int & /*port*/) const {} inline socket_t BufferStream::socket() const { return 0; } inline time_t BufferStream::duration() const { return 0; } inline const std::string &BufferStream::get_buffer() const { return buffer; } inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern) : MatcherBase(pattern) { constexpr const char marker[] = "/:"; // One past the last ending position of a path param substring std::size_t last_param_end = 0; #ifndef CPPHTTPLIB_NO_EXCEPTIONS // Needed to ensure that parameter names are unique during matcher // construction // If exceptions are disabled, only last duplicate path // parameter will be set std::unordered_set param_name_set; #endif while (true) { const auto marker_pos = pattern.find( marker, last_param_end == 0 ? last_param_end : last_param_end - 1); if (marker_pos == std::string::npos) { break; } static_fragments_.push_back( pattern.substr(last_param_end, marker_pos - last_param_end + 1)); const auto param_name_start = marker_pos + str_len(marker); auto sep_pos = pattern.find(separator, param_name_start); if (sep_pos == std::string::npos) { sep_pos = pattern.length(); } auto param_name = pattern.substr(param_name_start, sep_pos - param_name_start); #ifndef CPPHTTPLIB_NO_EXCEPTIONS if (param_name_set.find(param_name) != param_name_set.cend()) { std::string msg = "Encountered path parameter '" + param_name + "' multiple times in route pattern '" + pattern + "'."; throw std::invalid_argument(msg); } #endif param_names_.push_back(std::move(param_name)); last_param_end = sep_pos + 1; } if (last_param_end < pattern.length()) { static_fragments_.push_back(pattern.substr(last_param_end)); } } inline bool PathParamsMatcher::match(Request &request) const { request.matches = std::smatch(); request.path_params.clear(); request.path_params.reserve(param_names_.size()); // One past the position at which the path matched the pattern last time std::size_t starting_pos = 0; for (size_t i = 0; i < static_fragments_.size(); ++i) { const auto &fragment = static_fragments_[i]; if (starting_pos + fragment.length() > request.path.length()) { return false; } // Avoid unnecessary allocation by using strncmp instead of substr + // comparison if (std::strncmp(request.path.c_str() + starting_pos, fragment.c_str(), fragment.length()) != 0) { return false; } starting_pos += fragment.length(); // Should only happen when we have a static fragment after a param // Example: '/users/:id/subscriptions' // The 'subscriptions' fragment here does not have a corresponding param if (i >= param_names_.size()) { continue; } auto sep_pos = request.path.find(separator, starting_pos); if (sep_pos == std::string::npos) { sep_pos = request.path.length(); } const auto ¶m_name = param_names_[i]; request.path_params.emplace( param_name, request.path.substr(starting_pos, sep_pos - starting_pos)); // Mark everything up to '/' as matched starting_pos = sep_pos + 1; } // Returns false if the path is longer than the pattern return starting_pos >= request.path.length(); } inline bool RegexMatcher::match(Request &request) const { request.path_params.clear(); return std::regex_match(request.path, request.matches, regex_); } // Enclose IPv6 address in brackets if needed inline std::string prepare_host_string(const std::string &host) { // Enclose IPv6 address in brackets (but not if already enclosed) if (host.find(':') == std::string::npos || (!host.empty() && host[0] == '[')) { // IPv4, hostname, or already bracketed IPv6 return host; } else { // IPv6 address without brackets return "[" + host + "]"; } } inline std::string make_host_and_port_string(const std::string &host, int port, bool is_ssl) { auto result = prepare_host_string(host); // Append port if not default if ((!is_ssl && port == 80) || (is_ssl && port == 443)) { ; // do nothing } else { result += ":" + std::to_string(port); } return result; } // Create "host:port" string always including port number (for CONNECT method) inline std::string make_host_and_port_string_always_port(const std::string &host, int port) { return prepare_host_string(host) + ":" + std::to_string(port); } template inline bool check_and_write_headers(Stream &strm, Headers &headers, T header_writer, Error &error) { for (const auto &h : headers) { if (!detail::fields::is_field_name(h.first) || !detail::fields::is_field_value(h.second)) { error = Error::InvalidHeaders; return false; } } if (header_writer(strm, headers) <= 0) { error = Error::Write; return false; } return true; } } // namespace detail /* * Group 2 (continued): detail namespace - SSLSocketStream implementation */ #ifdef CPPHTTPLIB_SSL_ENABLED namespace detail { // SSL socket stream implementation inline SSLSocketStream::SSLSocketStream( socket_t sock, tls::session_t session, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec, std::chrono::time_point start_time) : sock_(sock), session_(session), read_timeout_sec_(read_timeout_sec), read_timeout_usec_(read_timeout_usec), write_timeout_sec_(write_timeout_sec), write_timeout_usec_(write_timeout_usec), max_timeout_msec_(max_timeout_msec), start_time_(start_time) { #ifdef CPPHTTPLIB_OPENSSL_SUPPORT // Clear AUTO_RETRY for proper non-blocking I/O timeout handling // Note: create_session() also clears this, but SSLClient currently // uses ssl_new() which does not. Until full TLS API migration is complete, // we need to ensure AUTO_RETRY is cleared here regardless of how the // SSL session was created. SSL_clear_mode(static_cast(session), SSL_MODE_AUTO_RETRY); #endif } inline SSLSocketStream::~SSLSocketStream() = default; inline bool SSLSocketStream::is_readable() const { return tls::pending(session_) > 0; } inline bool SSLSocketStream::wait_readable() const { if (max_timeout_msec_ <= 0) { return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; } time_t read_timeout_sec; time_t read_timeout_usec; calc_actual_timeout(max_timeout_msec_, duration(), read_timeout_sec_, read_timeout_usec_, read_timeout_sec, read_timeout_usec); return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0; } inline bool SSLSocketStream::wait_writable() const { return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 && is_socket_alive(sock_) && !tls::is_peer_closed(session_, sock_); } inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { if (tls::pending(session_) > 0) { tls::TlsError err; auto ret = tls::read(session_, ptr, size, err); if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) { error_ = Error::ConnectionClosed; } return ret; } else if (wait_readable()) { tls::TlsError err; auto ret = tls::read(session_, ptr, size, err); if (ret < 0) { auto n = 1000; #ifdef _WIN32 while (--n >= 0 && (err.code == tls::ErrorCode::WantRead || (err.code == tls::ErrorCode::SyscallError && WSAGetLastError() == WSAETIMEDOUT))) { #else while (--n >= 0 && err.code == tls::ErrorCode::WantRead) { #endif if (tls::pending(session_) > 0) { return tls::read(session_, ptr, size, err); } else if (wait_readable()) { std::this_thread::sleep_for(std::chrono::microseconds{10}); ret = tls::read(session_, ptr, size, err); if (ret >= 0) { return ret; } } else { break; } } assert(ret < 0); } else if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) { error_ = Error::ConnectionClosed; } return ret; } else { error_ = Error::Timeout; return -1; } } inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) { if (wait_writable()) { auto handle_size = std::min(size, (std::numeric_limits::max)()); tls::TlsError err; auto ret = tls::write(session_, ptr, handle_size, err); if (ret < 0) { auto n = 1000; #ifdef _WIN32 while (--n >= 0 && (err.code == tls::ErrorCode::WantWrite || (err.code == tls::ErrorCode::SyscallError && WSAGetLastError() == WSAETIMEDOUT))) { #else while (--n >= 0 && err.code == tls::ErrorCode::WantWrite) { #endif if (wait_writable()) { std::this_thread::sleep_for(std::chrono::microseconds{10}); ret = tls::write(session_, ptr, handle_size, err); if (ret >= 0) { return ret; } } else { break; } } assert(ret < 0); } return ret; } return -1; } inline void SSLSocketStream::get_remote_ip_and_port(std::string &ip, int &port) const { detail::get_remote_ip_and_port(sock_, ip, port); } inline void SSLSocketStream::get_local_ip_and_port(std::string &ip, int &port) const { detail::get_local_ip_and_port(sock_, ip, port); } inline socket_t SSLSocketStream::socket() const { return sock_; } inline time_t SSLSocketStream::duration() const { return std::chrono::duration_cast( std::chrono::steady_clock::now() - start_time_) .count(); } } // namespace detail #endif // CPPHTTPLIB_SSL_ENABLED /* * Group 4: Server implementation */ // HTTP server implementation inline Server::Server() : new_task_queue( [] { return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT); }) { #ifndef _WIN32 signal(SIGPIPE, SIG_IGN); #endif } inline Server::~Server() = default; inline std::unique_ptr Server::make_matcher(const std::string &pattern) { if (pattern.find("/:") != std::string::npos) { return detail::make_unique(pattern); } else { return detail::make_unique(pattern); } } inline Server &Server::Get(const std::string &pattern, Handler handler) { get_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); return *this; } inline Server &Server::Post(const std::string &pattern, Handler handler) { post_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); return *this; } inline Server &Server::Post(const std::string &pattern, HandlerWithContentReader handler) { post_handlers_for_content_reader_.emplace_back(make_matcher(pattern), std::move(handler)); return *this; } inline Server &Server::Put(const std::string &pattern, Handler handler) { put_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); return *this; } inline Server &Server::Put(const std::string &pattern, HandlerWithContentReader handler) { put_handlers_for_content_reader_.emplace_back(make_matcher(pattern), std::move(handler)); return *this; } inline Server &Server::Patch(const std::string &pattern, Handler handler) { patch_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); return *this; } inline Server &Server::Patch(const std::string &pattern, HandlerWithContentReader handler) { patch_handlers_for_content_reader_.emplace_back(make_matcher(pattern), std::move(handler)); return *this; } inline Server &Server::Delete(const std::string &pattern, Handler handler) { delete_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); return *this; } inline Server &Server::Delete(const std::string &pattern, HandlerWithContentReader handler) { delete_handlers_for_content_reader_.emplace_back(make_matcher(pattern), std::move(handler)); return *this; } inline Server &Server::Options(const std::string &pattern, Handler handler) { options_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); return *this; } inline bool Server::set_base_dir(const std::string &dir, const std::string &mount_point) { return set_mount_point(mount_point, dir); } inline bool Server::set_mount_point(const std::string &mount_point, const std::string &dir, Headers headers) { detail::FileStat stat(dir); if (stat.is_dir()) { std::string mnt = !mount_point.empty() ? mount_point : "/"; if (!mnt.empty() && mnt[0] == '/') { base_dirs_.push_back({std::move(mnt), dir, std::move(headers)}); return true; } } return false; } inline bool Server::remove_mount_point(const std::string &mount_point) { for (auto it = base_dirs_.begin(); it != base_dirs_.end(); ++it) { if (it->mount_point == mount_point) { base_dirs_.erase(it); return true; } } return false; } inline Server & Server::set_file_extension_and_mimetype_mapping(const std::string &ext, const std::string &mime) { file_extension_and_mimetype_map_[ext] = mime; return *this; } inline Server &Server::set_default_file_mimetype(const std::string &mime) { default_file_mimetype_ = mime; return *this; } inline Server &Server::set_file_request_handler(Handler handler) { file_request_handler_ = std::move(handler); return *this; } inline Server &Server::set_error_handler_core(HandlerWithResponse handler, std::true_type) { error_handler_ = std::move(handler); return *this; } inline Server &Server::set_error_handler_core(Handler handler, std::false_type) { error_handler_ = [handler](const Request &req, Response &res) { handler(req, res); return HandlerResponse::Handled; }; return *this; } inline Server &Server::set_exception_handler(ExceptionHandler handler) { exception_handler_ = std::move(handler); return *this; } inline Server &Server::set_pre_routing_handler(HandlerWithResponse handler) { pre_routing_handler_ = std::move(handler); return *this; } inline Server &Server::set_post_routing_handler(Handler handler) { post_routing_handler_ = std::move(handler); return *this; } inline Server &Server::set_pre_request_handler(HandlerWithResponse handler) { pre_request_handler_ = std::move(handler); return *this; } inline Server &Server::set_logger(Logger logger) { logger_ = std::move(logger); return *this; } inline Server &Server::set_error_logger(ErrorLogger error_logger) { error_logger_ = std::move(error_logger); return *this; } inline Server &Server::set_pre_compression_logger(Logger logger) { pre_compression_logger_ = std::move(logger); return *this; } inline Server & Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) { expect_100_continue_handler_ = std::move(handler); return *this; } inline Server &Server::set_address_family(int family) { address_family_ = family; return *this; } inline Server &Server::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; return *this; } inline Server &Server::set_ipv6_v6only(bool on) { ipv6_v6only_ = on; return *this; } inline Server &Server::set_socket_options(SocketOptions socket_options) { socket_options_ = std::move(socket_options); return *this; } inline Server &Server::set_default_headers(Headers headers) { default_headers_ = std::move(headers); return *this; } inline Server &Server::set_header_writer( std::function const &writer) { header_writer_ = writer; return *this; } inline Server & Server::set_trusted_proxies(const std::vector &proxies) { trusted_proxies_ = proxies; return *this; } inline Server &Server::set_keep_alive_max_count(size_t count) { keep_alive_max_count_ = count; return *this; } inline Server &Server::set_keep_alive_timeout(time_t sec) { keep_alive_timeout_sec_ = sec; return *this; } inline Server &Server::set_read_timeout(time_t sec, time_t usec) { read_timeout_sec_ = sec; read_timeout_usec_ = usec; return *this; } inline Server &Server::set_write_timeout(time_t sec, time_t usec) { write_timeout_sec_ = sec; write_timeout_usec_ = usec; return *this; } inline Server &Server::set_idle_interval(time_t sec, time_t usec) { idle_interval_sec_ = sec; idle_interval_usec_ = usec; return *this; } inline Server &Server::set_payload_max_length(size_t length) { payload_max_length_ = length; return *this; } inline bool Server::bind_to_port(const std::string &host, int port, int socket_flags) { auto ret = bind_internal(host, port, socket_flags); if (ret == -1) { is_decommissioned = true; } return ret >= 0; } inline int Server::bind_to_any_port(const std::string &host, int socket_flags) { auto ret = bind_internal(host, 0, socket_flags); if (ret == -1) { is_decommissioned = true; } return ret; } inline bool Server::listen_after_bind() { return listen_internal(); } inline bool Server::listen(const std::string &host, int port, int socket_flags) { return bind_to_port(host, port, socket_flags) && listen_internal(); } inline bool Server::is_running() const { return is_running_; } inline void Server::wait_until_ready() const { while (!is_running_ && !is_decommissioned) { std::this_thread::sleep_for(std::chrono::milliseconds{1}); } } inline void Server::stop() { if (is_running_) { assert(svr_sock_ != INVALID_SOCKET); std::atomic sock(svr_sock_.exchange(INVALID_SOCKET)); detail::shutdown_socket(sock); detail::close_socket(sock); } is_decommissioned = false; } inline void Server::decommission() { is_decommissioned = true; } inline bool Server::parse_request_line(const char *s, Request &req) const { auto len = strlen(s); if (len < 2 || s[len - 2] != '\r' || s[len - 1] != '\n') { return false; } len -= 2; { size_t count = 0; detail::split(s, s + len, ' ', [&](const char *b, const char *e) { switch (count) { case 0: req.method = std::string(b, e); break; case 1: req.target = std::string(b, e); break; case 2: req.version = std::string(b, e); break; default: break; } count++; }); if (count != 3) { return false; } } thread_local const std::set methods{ "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"}; if (methods.find(req.method) == methods.end()) { output_error_log(Error::InvalidHTTPMethod, &req); return false; } if (req.version != "HTTP/1.1" && req.version != "HTTP/1.0") { output_error_log(Error::InvalidHTTPVersion, &req); return false; } { // Skip URL fragment for (size_t i = 0; i < req.target.size(); i++) { if (req.target[i] == '#') { req.target.erase(i); break; } } detail::divide(req.target, '?', [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data, std::size_t rhs_size) { req.path = decode_path_component(std::string(lhs_data, lhs_size)); detail::parse_query_text(rhs_data, rhs_size, req.params); }); } return true; } inline bool Server::write_response(Stream &strm, bool close_connection, Request &req, Response &res) { // NOTE: `req.ranges` should be empty, otherwise it will be applied // incorrectly to the error content. req.ranges.clear(); return write_response_core(strm, close_connection, req, res, false); } inline bool Server::write_response_with_content(Stream &strm, bool close_connection, const Request &req, Response &res) { return write_response_core(strm, close_connection, req, res, true); } inline bool Server::write_response_core(Stream &strm, bool close_connection, const Request &req, Response &res, bool need_apply_ranges) { assert(res.status != -1); if (400 <= res.status && error_handler_ && error_handler_(req, res) == HandlerResponse::Handled) { need_apply_ranges = true; } std::string content_type; std::string boundary; if (need_apply_ranges) { apply_ranges(req, res, content_type, boundary); } // Prepare additional headers if (close_connection || req.get_header_value("Connection") == "close" || 400 <= res.status) { // Don't leave connections open after errors res.set_header("Connection", "close"); } else { std::string s = "timeout="; s += std::to_string(keep_alive_timeout_sec_); s += ", max="; s += std::to_string(keep_alive_max_count_); res.set_header("Keep-Alive", s); } if ((!res.body.empty() || res.content_length_ > 0 || res.content_provider_) && !res.has_header("Content-Type")) { res.set_header("Content-Type", "text/plain"); } if (res.body.empty() && !res.content_length_ && !res.content_provider_ && !res.has_header("Content-Length")) { res.set_header("Content-Length", "0"); } if (req.method == "HEAD" && !res.has_header("Accept-Ranges")) { res.set_header("Accept-Ranges", "bytes"); } if (post_routing_handler_) { post_routing_handler_(req, res); } // Response line and headers { detail::BufferStream bstrm; if (!detail::write_response_line(bstrm, res.status)) { return false; } if (header_writer_(bstrm, res.headers) <= 0) { return false; } // Flush buffer auto &data = bstrm.get_buffer(); detail::write_data(strm, data.data(), data.size()); } // Body auto ret = true; if (req.method != "HEAD") { if (!res.body.empty()) { if (!detail::write_data(strm, res.body.data(), res.body.size())) { ret = false; } } else if (res.content_provider_) { if (write_content_with_provider(strm, req, res, boundary, content_type)) { res.content_provider_success_ = true; } else { ret = false; } } } // Log output_log(req, res); return ret; } inline bool Server::write_content_with_provider(Stream &strm, const Request &req, Response &res, const std::string &boundary, const std::string &content_type) { auto is_shutting_down = [this]() { return this->svr_sock_ == INVALID_SOCKET; }; if (res.content_length_ > 0) { if (req.ranges.empty()) { return detail::write_content(strm, res.content_provider_, 0, res.content_length_, is_shutting_down); } else if (req.ranges.size() == 1) { auto offset_and_length = detail::get_range_offset_and_length( req.ranges[0], res.content_length_); return detail::write_content(strm, res.content_provider_, offset_and_length.first, offset_and_length.second, is_shutting_down); } else { return detail::write_multipart_ranges_data( strm, req, res, boundary, content_type, res.content_length_, is_shutting_down); } } else { if (res.is_chunked_content_provider_) { auto type = detail::encoding_type(req, res); std::unique_ptr compressor; if (type == detail::EncodingType::Gzip) { #ifdef CPPHTTPLIB_ZLIB_SUPPORT compressor = detail::make_unique(); #endif } else if (type == detail::EncodingType::Brotli) { #ifdef CPPHTTPLIB_BROTLI_SUPPORT compressor = detail::make_unique(); #endif } else if (type == detail::EncodingType::Zstd) { #ifdef CPPHTTPLIB_ZSTD_SUPPORT compressor = detail::make_unique(); #endif } else { compressor = detail::make_unique(); } assert(compressor != nullptr); return detail::write_content_chunked(strm, res.content_provider_, is_shutting_down, *compressor); } else { return detail::write_content_without_length(strm, res.content_provider_, is_shutting_down); } } } inline bool Server::read_content(Stream &strm, Request &req, Response &res) { FormFields::iterator cur_field; FormFiles::iterator cur_file; auto is_text_field = false; size_t count = 0; if (read_content_core( strm, req, res, // Regular [&](const char *buf, size_t n) { // Prevent arithmetic overflow when checking sizes. // Avoid computing (req.body.size() + n) directly because // adding two unsigned `size_t` values can wrap around and // produce a small result instead of indicating overflow. // Instead, check using subtraction: ensure `n` does not // exceed the remaining capacity `max_size() - size()`. if (req.body.size() >= req.body.max_size() || n > req.body.max_size() - req.body.size()) { return false; } // Limit decompressed body size to payload_max_length_ to protect // against "zip bomb" attacks where a small compressed payload // decompresses to a massive size. if (payload_max_length_ > 0 && (req.body.size() >= payload_max_length_ || n > payload_max_length_ - req.body.size())) { return false; } req.body.append(buf, n); return true; }, // Multipart FormData [&](const FormData &file) { if (count++ == CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT) { output_error_log(Error::TooManyFormDataFiles, &req); return false; } if (file.filename.empty()) { cur_field = req.form.fields.emplace( file.name, FormField{file.name, file.content, file.headers}); is_text_field = true; } else { cur_file = req.form.files.emplace(file.name, file); is_text_field = false; } return true; }, [&](const char *buf, size_t n) { if (is_text_field) { auto &content = cur_field->second.content; if (content.size() + n > content.max_size()) { return false; } content.append(buf, n); } else { auto &content = cur_file->second.content; if (content.size() + n > content.max_size()) { return false; } content.append(buf, n); } return true; })) { const auto &content_type = req.get_header_value("Content-Type"); if (!content_type.find("application/x-www-form-urlencoded")) { if (req.body.size() > CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH) { res.status = StatusCode::PayloadTooLarge_413; // NOTE: should be 414? output_error_log(Error::ExceedMaxPayloadSize, &req); return false; } detail::parse_query_text(req.body, req.params); } return true; } return false; } inline bool Server::read_content_with_content_receiver( Stream &strm, Request &req, Response &res, ContentReceiver receiver, FormDataHeader multipart_header, ContentReceiver multipart_receiver) { return read_content_core(strm, req, res, std::move(receiver), std::move(multipart_header), std::move(multipart_receiver)); } inline bool Server::read_content_core( Stream &strm, Request &req, Response &res, ContentReceiver receiver, FormDataHeader multipart_header, ContentReceiver multipart_receiver) const { detail::FormDataParser multipart_form_data_parser; ContentReceiverWithProgress out; if (req.is_multipart_form_data()) { const auto &content_type = req.get_header_value("Content-Type"); std::string boundary; if (!detail::parse_multipart_boundary(content_type, boundary)) { res.status = StatusCode::BadRequest_400; output_error_log(Error::MultipartParsing, &req); return false; } multipart_form_data_parser.set_boundary(std::move(boundary)); out = [&](const char *buf, size_t n, size_t /*off*/, size_t /*len*/) { return multipart_form_data_parser.parse(buf, n, multipart_header, multipart_receiver); }; } else { out = [receiver](const char *buf, size_t n, size_t /*off*/, size_t /*len*/) { return receiver(buf, n); }; } // RFC 7230 Section 3.3.3: If this is a request message and none of the above // are true (no Transfer-Encoding and no Content-Length), then the message // body length is zero (no message body is present). // // For non-SSL builds, detect clients that send a body without a // Content-Length header (raw HTTP over TCP). Check both the stream's // internal read buffer (data already read from the socket during header // parsing) and the socket itself for pending data. If data is found and // exceeds the configured payload limit, reject with 413. // For SSL builds we cannot reliably peek the decrypted application bytes, // so keep the original behaviour. #if !defined(CPPHTTPLIB_SSL_ENABLED) if (!req.has_header("Content-Length") && !detail::is_chunked_transfer_encoding(req.headers)) { // Only check if payload_max_length is set to a finite value if (payload_max_length_ > 0 && payload_max_length_ < (std::numeric_limits::max)()) { // Check if there is data already buffered in the stream (read during // header parsing) or pending on the socket. Use a non-blocking socket // check to avoid deadlock when the client sends no body. bool has_data = strm.is_readable(); if (!has_data) { socket_t s = strm.socket(); if (s != INVALID_SOCKET) { has_data = detail::select_read(s, 0, 0) > 0; } } if (has_data) { auto result = detail::read_content_without_length(strm, payload_max_length_, out); if (result == detail::ReadContentResult::PayloadTooLarge) { res.status = StatusCode::PayloadTooLarge_413; return false; } else if (result != detail::ReadContentResult::Success) { return false; } return true; } } return true; } #else if (!req.has_header("Content-Length") && !detail::is_chunked_transfer_encoding(req.headers)) { return true; } #endif if (!detail::read_content(strm, req, payload_max_length_, res.status, nullptr, out, true)) { return false; } if (req.is_multipart_form_data()) { if (!multipart_form_data_parser.is_valid()) { res.status = StatusCode::BadRequest_400; output_error_log(Error::MultipartParsing, &req); return false; } } return true; } inline bool Server::handle_file_request(Request &req, Response &res) { for (const auto &entry : base_dirs_) { // Prefix match if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) { std::string sub_path = "/" + req.path.substr(entry.mount_point.size()); if (detail::is_valid_path(sub_path)) { auto path = entry.base_dir + sub_path; if (path.back() == '/') { path += "index.html"; } detail::FileStat stat(path); if (stat.is_dir()) { res.set_redirect(sub_path + "/", StatusCode::MovedPermanently_301); return true; } if (stat.is_file()) { for (const auto &kv : entry.headers) { res.set_header(kv.first, kv.second); } auto etag = detail::compute_etag(stat); if (!etag.empty()) { res.set_header("ETag", etag); } auto mtime = stat.mtime(); auto last_modified = detail::file_mtime_to_http_date(mtime); if (!last_modified.empty()) { res.set_header("Last-Modified", last_modified); } if (check_if_not_modified(req, res, etag, mtime)) { return true; } check_if_range(req, etag, mtime); auto mm = std::make_shared(path.c_str()); if (!mm->is_open()) { output_error_log(Error::OpenFile, &req); return false; } res.set_content_provider( mm->size(), detail::find_content_type(path, file_extension_and_mimetype_map_, default_file_mimetype_), [mm](size_t offset, size_t length, DataSink &sink) -> bool { sink.write(mm->data() + offset, length); return true; }); if (req.method != "HEAD" && file_request_handler_) { file_request_handler_(req, res); } return true; } else { output_error_log(Error::OpenFile, &req); } } } } return false; } inline bool Server::check_if_not_modified(const Request &req, Response &res, const std::string &etag, time_t mtime) const { // Handle conditional GET: // 1. If-None-Match takes precedence (RFC 9110 Section 13.1.2) // 2. If-Modified-Since is checked only when If-None-Match is absent if (req.has_header("If-None-Match")) { if (!etag.empty()) { auto val = req.get_header_value("If-None-Match"); // NOTE: We use exact string matching here. This works correctly // because our server always generates weak ETags (W/"..."), and // clients typically send back the same ETag they received. // RFC 9110 Section 8.8.3.2 allows weak comparison for // If-None-Match, where W/"x" and "x" would match, but this // simplified implementation requires exact matches. auto ret = detail::split_find(val.data(), val.data() + val.size(), ',', [&](const char *b, const char *e) { auto seg_len = static_cast(e - b); return (seg_len == 1 && *b == '*') || (seg_len == etag.size() && std::equal(b, e, etag.begin())); }); if (ret) { res.status = StatusCode::NotModified_304; return true; } } } else if (req.has_header("If-Modified-Since")) { auto val = req.get_header_value("If-Modified-Since"); auto t = detail::parse_http_date(val); if (t != static_cast(-1) && mtime <= t) { res.status = StatusCode::NotModified_304; return true; } } return false; } inline bool Server::check_if_range(Request &req, const std::string &etag, time_t mtime) const { // Handle If-Range for partial content requests (RFC 9110 // Section 13.1.5). If-Range is only evaluated when Range header is // present. If the validator matches, serve partial content; otherwise // serve full content. if (!req.ranges.empty() && req.has_header("If-Range")) { auto val = req.get_header_value("If-Range"); auto is_valid_range = [&]() { if (detail::is_strong_etag(val)) { // RFC 9110 Section 13.1.5: If-Range requires strong ETag // comparison. return (!etag.empty() && val == etag); } else if (detail::is_weak_etag(val)) { // Weak ETags are not valid for If-Range (RFC 9110 Section 13.1.5) return false; } else { // HTTP-date comparison auto t = detail::parse_http_date(val); return (t != static_cast(-1) && mtime <= t); } }; if (!is_valid_range()) { // Validator doesn't match: ignore Range and serve full content req.ranges.clear(); return false; } } return true; } inline socket_t Server::create_server_socket(const std::string &host, int port, int socket_flags, SocketOptions socket_options) const { return detail::create_socket( host, std::string(), port, address_family_, socket_flags, tcp_nodelay_, ipv6_v6only_, std::move(socket_options), [&](socket_t sock, struct addrinfo &ai, bool & /*quit*/) -> bool { if (::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { output_error_log(Error::BindIPAddress, nullptr); return false; } if (::listen(sock, CPPHTTPLIB_LISTEN_BACKLOG)) { output_error_log(Error::Listen, nullptr); return false; } return true; }); } inline int Server::bind_internal(const std::string &host, int port, int socket_flags) { if (is_decommissioned) { return -1; } if (!is_valid()) { return -1; } svr_sock_ = create_server_socket(host, port, socket_flags, socket_options_); if (svr_sock_ == INVALID_SOCKET) { return -1; } if (port == 0) { struct sockaddr_storage addr; socklen_t addr_len = sizeof(addr); if (getsockname(svr_sock_, reinterpret_cast(&addr), &addr_len) == -1) { output_error_log(Error::GetSockName, nullptr); return -1; } if (addr.ss_family == AF_INET) { return ntohs(reinterpret_cast(&addr)->sin_port); } else if (addr.ss_family == AF_INET6) { return ntohs(reinterpret_cast(&addr)->sin6_port); } else { output_error_log(Error::UnsupportedAddressFamily, nullptr); return -1; } } else { return port; } } inline bool Server::listen_internal() { if (is_decommissioned) { return false; } auto ret = true; is_running_ = true; auto se = detail::scope_exit([&]() { is_running_ = false; }); { std::unique_ptr task_queue(new_task_queue()); while (svr_sock_ != INVALID_SOCKET) { #ifndef _WIN32 if (idle_interval_sec_ > 0 || idle_interval_usec_ > 0) { #endif auto val = detail::select_read(svr_sock_, idle_interval_sec_, idle_interval_usec_); if (val == 0) { // Timeout task_queue->on_idle(); continue; } #ifndef _WIN32 } #endif #if defined _WIN32 // sockets connected via WASAccept inherit flags NO_HANDLE_INHERIT, // OVERLAPPED socket_t sock = WSAAccept(svr_sock_, nullptr, nullptr, nullptr, 0); #elif defined SOCK_CLOEXEC socket_t sock = accept4(svr_sock_, nullptr, nullptr, SOCK_CLOEXEC); #else socket_t sock = accept(svr_sock_, nullptr, nullptr); #endif if (sock == INVALID_SOCKET) { if (errno == EMFILE) { // The per-process limit of open file descriptors has been reached. // Try to accept new connections after a short sleep. std::this_thread::sleep_for(std::chrono::microseconds{1}); continue; } else if (errno == EINTR || errno == EAGAIN) { continue; } if (svr_sock_ != INVALID_SOCKET) { detail::close_socket(svr_sock_); ret = false; output_error_log(Error::Connection, nullptr); } else { ; // The server socket was closed by user. } break; } detail::set_socket_opt_time(sock, SOL_SOCKET, SO_RCVTIMEO, read_timeout_sec_, read_timeout_usec_); detail::set_socket_opt_time(sock, SOL_SOCKET, SO_SNDTIMEO, write_timeout_sec_, write_timeout_usec_); if (!task_queue->enqueue( [this, sock]() { process_and_close_socket(sock); })) { output_error_log(Error::ResourceExhaustion, nullptr); detail::shutdown_socket(sock); detail::close_socket(sock); } } task_queue->shutdown(); } is_decommissioned = !ret; return ret; } inline bool Server::routing(Request &req, Response &res, Stream &strm) { if (pre_routing_handler_ && pre_routing_handler_(req, res) == HandlerResponse::Handled) { return true; } // File handler if ((req.method == "GET" || req.method == "HEAD") && handle_file_request(req, res)) { return true; } if (detail::expect_content(req)) { // Content reader handler { ContentReader reader( [&](ContentReceiver receiver) { auto result = read_content_with_content_receiver( strm, req, res, std::move(receiver), nullptr, nullptr); if (!result) { output_error_log(Error::Read, &req); } return result; }, [&](FormDataHeader header, ContentReceiver receiver) { auto result = read_content_with_content_receiver( strm, req, res, nullptr, std::move(header), std::move(receiver)); if (!result) { output_error_log(Error::Read, &req); } return result; }); if (req.method == "POST") { if (dispatch_request_for_content_reader( req, res, std::move(reader), post_handlers_for_content_reader_)) { return true; } } else if (req.method == "PUT") { if (dispatch_request_for_content_reader( req, res, std::move(reader), put_handlers_for_content_reader_)) { return true; } } else if (req.method == "PATCH") { if (dispatch_request_for_content_reader( req, res, std::move(reader), patch_handlers_for_content_reader_)) { return true; } } else if (req.method == "DELETE") { if (dispatch_request_for_content_reader( req, res, std::move(reader), delete_handlers_for_content_reader_)) { return true; } } } // Read content into `req.body` if (!read_content(strm, req, res)) { output_error_log(Error::Read, &req); return false; } } // Regular handler if (req.method == "GET" || req.method == "HEAD") { return dispatch_request(req, res, get_handlers_); } else if (req.method == "POST") { return dispatch_request(req, res, post_handlers_); } else if (req.method == "PUT") { return dispatch_request(req, res, put_handlers_); } else if (req.method == "DELETE") { return dispatch_request(req, res, delete_handlers_); } else if (req.method == "OPTIONS") { return dispatch_request(req, res, options_handlers_); } else if (req.method == "PATCH") { return dispatch_request(req, res, patch_handlers_); } res.status = StatusCode::BadRequest_400; return false; } inline bool Server::dispatch_request(Request &req, Response &res, const Handlers &handlers) const { for (const auto &x : handlers) { const auto &matcher = x.first; const auto &handler = x.second; if (matcher->match(req)) { req.matched_route = matcher->pattern(); if (!pre_request_handler_ || pre_request_handler_(req, res) != HandlerResponse::Handled) { handler(req, res); } return true; } } return false; } inline void Server::apply_ranges(const Request &req, Response &res, std::string &content_type, std::string &boundary) const { if (req.ranges.size() > 1 && res.status == StatusCode::PartialContent_206) { auto it = res.headers.find("Content-Type"); if (it != res.headers.end()) { content_type = it->second; res.headers.erase(it); } boundary = detail::make_multipart_data_boundary(); res.set_header("Content-Type", "multipart/byteranges; boundary=" + boundary); } auto type = detail::encoding_type(req, res); if (res.body.empty()) { if (res.content_length_ > 0) { size_t length = 0; if (req.ranges.empty() || res.status != StatusCode::PartialContent_206) { length = res.content_length_; } else if (req.ranges.size() == 1) { auto offset_and_length = detail::get_range_offset_and_length( req.ranges[0], res.content_length_); length = offset_and_length.second; auto content_range = detail::make_content_range_header_field( offset_and_length, res.content_length_); res.set_header("Content-Range", content_range); } else { length = detail::get_multipart_ranges_data_length( req, boundary, content_type, res.content_length_); } res.set_header("Content-Length", std::to_string(length)); } else { if (res.content_provider_) { if (res.is_chunked_content_provider_) { res.set_header("Transfer-Encoding", "chunked"); if (type == detail::EncodingType::Gzip) { res.set_header("Content-Encoding", "gzip"); res.set_header("Vary", "Accept-Encoding"); } else if (type == detail::EncodingType::Brotli) { res.set_header("Content-Encoding", "br"); res.set_header("Vary", "Accept-Encoding"); } else if (type == detail::EncodingType::Zstd) { res.set_header("Content-Encoding", "zstd"); res.set_header("Vary", "Accept-Encoding"); } } } } } else { if (req.ranges.empty() || res.status != StatusCode::PartialContent_206) { ; } else if (req.ranges.size() == 1) { auto offset_and_length = detail::get_range_offset_and_length(req.ranges[0], res.body.size()); auto offset = offset_and_length.first; auto length = offset_and_length.second; auto content_range = detail::make_content_range_header_field( offset_and_length, res.body.size()); res.set_header("Content-Range", content_range); assert(offset + length <= res.body.size()); res.body = res.body.substr(offset, length); } else { std::string data; detail::make_multipart_ranges_data(req, res, boundary, content_type, res.body.size(), data); res.body.swap(data); } if (type != detail::EncodingType::None) { output_pre_compression_log(req, res); std::unique_ptr compressor; std::string content_encoding; if (type == detail::EncodingType::Gzip) { #ifdef CPPHTTPLIB_ZLIB_SUPPORT compressor = detail::make_unique(); content_encoding = "gzip"; #endif } else if (type == detail::EncodingType::Brotli) { #ifdef CPPHTTPLIB_BROTLI_SUPPORT compressor = detail::make_unique(); content_encoding = "br"; #endif } else if (type == detail::EncodingType::Zstd) { #ifdef CPPHTTPLIB_ZSTD_SUPPORT compressor = detail::make_unique(); content_encoding = "zstd"; #endif } if (compressor) { std::string compressed; if (compressor->compress(res.body.data(), res.body.size(), true, [&](const char *data, size_t data_len) { compressed.append(data, data_len); return true; })) { res.body.swap(compressed); res.set_header("Content-Encoding", content_encoding); res.set_header("Vary", "Accept-Encoding"); } } } auto length = std::to_string(res.body.size()); res.set_header("Content-Length", length); } } inline bool Server::dispatch_request_for_content_reader( Request &req, Response &res, ContentReader content_reader, const HandlersForContentReader &handlers) const { for (const auto &x : handlers) { const auto &matcher = x.first; const auto &handler = x.second; if (matcher->match(req)) { req.matched_route = matcher->pattern(); if (!pre_request_handler_ || pre_request_handler_(req, res) != HandlerResponse::Handled) { handler(req, res, content_reader); } return true; } } return false; } inline std::string get_client_ip(const std::string &x_forwarded_for, const std::vector &trusted_proxies) { // X-Forwarded-For is a comma-separated list per RFC 7239 std::vector ip_list; detail::split(x_forwarded_for.data(), x_forwarded_for.data() + x_forwarded_for.size(), ',', [&](const char *b, const char *e) { auto r = detail::trim(b, e, 0, static_cast(e - b)); ip_list.emplace_back(std::string(b + r.first, b + r.second)); }); for (size_t i = 0; i < ip_list.size(); ++i) { auto ip = ip_list[i]; auto is_trusted_proxy = std::any_of(trusted_proxies.begin(), trusted_proxies.end(), [&](const std::string &proxy) { return ip == proxy; }); if (is_trusted_proxy) { if (i == 0) { // If the trusted proxy is the first IP, there's no preceding client IP return ip; } else { // Return the IP immediately before the trusted proxy return ip_list[i - 1]; } } } // If no trusted proxy is found, return the first IP in the list return ip_list.front(); } inline bool Server::process_request(Stream &strm, const std::string &remote_addr, int remote_port, const std::string &local_addr, int local_port, bool close_connection, bool &connection_closed, const std::function &setup_request) { std::array buf{}; detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); // Connection has been closed on client if (!line_reader.getline()) { return false; } Request req; req.start_time_ = std::chrono::steady_clock::now(); req.remote_addr = remote_addr; req.remote_port = remote_port; req.local_addr = local_addr; req.local_port = local_port; Response res; res.version = "HTTP/1.1"; res.headers = default_headers_; #ifdef __APPLE__ // Socket file descriptor exceeded FD_SETSIZE... if (strm.socket() >= FD_SETSIZE) { Headers dummy; detail::read_headers(strm, dummy); res.status = StatusCode::InternalServerError_500; output_error_log(Error::ExceedMaxSocketDescriptorCount, &req); return write_response(strm, close_connection, req, res); } #endif // Request line and headers if (!parse_request_line(line_reader.ptr(), req)) { res.status = StatusCode::BadRequest_400; output_error_log(Error::InvalidRequestLine, &req); return write_response(strm, close_connection, req, res); } // Request headers if (!detail::read_headers(strm, req.headers)) { res.status = StatusCode::BadRequest_400; output_error_log(Error::InvalidHeaders, &req); return write_response(strm, close_connection, req, res); } // Check if the request URI doesn't exceed the limit if (req.target.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) { res.status = StatusCode::UriTooLong_414; output_error_log(Error::ExceedUriMaxLength, &req); return write_response(strm, close_connection, req, res); } if (req.get_header_value("Connection") == "close") { connection_closed = true; } if (req.version == "HTTP/1.0" && req.get_header_value("Connection") != "Keep-Alive") { connection_closed = true; } if (!trusted_proxies_.empty() && req.has_header("X-Forwarded-For")) { auto x_forwarded_for = req.get_header_value("X-Forwarded-For"); req.remote_addr = get_client_ip(x_forwarded_for, trusted_proxies_); } else { req.remote_addr = remote_addr; } req.remote_port = remote_port; req.local_addr = local_addr; req.local_port = local_port; if (req.has_header("Accept")) { const auto &accept_header = req.get_header_value("Accept"); if (!detail::parse_accept_header(accept_header, req.accept_content_types)) { res.status = StatusCode::BadRequest_400; output_error_log(Error::HTTPParsing, &req); return write_response(strm, close_connection, req, res); } } if (req.has_header("Range")) { const auto &range_header_value = req.get_header_value("Range"); if (!detail::parse_range_header(range_header_value, req.ranges)) { res.status = StatusCode::RangeNotSatisfiable_416; output_error_log(Error::InvalidRangeHeader, &req); return write_response(strm, close_connection, req, res); } } if (setup_request) { setup_request(req); } if (req.get_header_value("Expect") == "100-continue") { int status = StatusCode::Continue_100; if (expect_100_continue_handler_) { status = expect_100_continue_handler_(req, res); } switch (status) { case StatusCode::Continue_100: case StatusCode::ExpectationFailed_417: detail::write_response_line(strm, status); strm.write("\r\n"); break; default: connection_closed = true; return write_response(strm, true, req, res); } } // Setup `is_connection_closed` method auto sock = strm.socket(); req.is_connection_closed = [sock]() { return !detail::is_socket_alive(sock); }; // Routing auto routed = false; #ifdef CPPHTTPLIB_NO_EXCEPTIONS routed = routing(req, res, strm); #else try { routed = routing(req, res, strm); } catch (std::exception &e) { if (exception_handler_) { auto ep = std::current_exception(); exception_handler_(req, res, ep); routed = true; } else { res.status = StatusCode::InternalServerError_500; std::string val; auto s = e.what(); for (size_t i = 0; s[i]; i++) { switch (s[i]) { case '\r': val += "\\r"; break; case '\n': val += "\\n"; break; default: val += s[i]; break; } } res.set_header("EXCEPTION_WHAT", val); } } catch (...) { if (exception_handler_) { auto ep = std::current_exception(); exception_handler_(req, res, ep); routed = true; } else { res.status = StatusCode::InternalServerError_500; res.set_header("EXCEPTION_WHAT", "UNKNOWN"); } } #endif if (routed) { if (res.status == -1) { res.status = req.ranges.empty() ? StatusCode::OK_200 : StatusCode::PartialContent_206; } // Serve file content by using a content provider if (!res.file_content_path_.empty()) { const auto &path = res.file_content_path_; auto mm = std::make_shared(path.c_str()); if (!mm->is_open()) { res.body.clear(); res.content_length_ = 0; res.content_provider_ = nullptr; res.status = StatusCode::NotFound_404; output_error_log(Error::OpenFile, &req); return write_response(strm, close_connection, req, res); } auto content_type = res.file_content_content_type_; if (content_type.empty()) { content_type = detail::find_content_type( path, file_extension_and_mimetype_map_, default_file_mimetype_); } res.set_content_provider( mm->size(), content_type, [mm](size_t offset, size_t length, DataSink &sink) -> bool { sink.write(mm->data() + offset, length); return true; }); } if (detail::range_error(req, res)) { res.body.clear(); res.content_length_ = 0; res.content_provider_ = nullptr; res.status = StatusCode::RangeNotSatisfiable_416; return write_response(strm, close_connection, req, res); } return write_response_with_content(strm, close_connection, req, res); } else { if (res.status == -1) { res.status = StatusCode::NotFound_404; } return write_response(strm, close_connection, req, res); } } inline bool Server::is_valid() const { return true; } inline bool Server::process_and_close_socket(socket_t sock) { std::string remote_addr; int remote_port = 0; detail::get_remote_ip_and_port(sock, remote_addr, remote_port); std::string local_addr; int local_port = 0; detail::get_local_ip_and_port(sock, local_addr, local_port); auto ret = detail::process_server_socket( svr_sock_, sock, keep_alive_max_count_, keep_alive_timeout_sec_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, [&](Stream &strm, bool close_connection, bool &connection_closed) { return process_request(strm, remote_addr, remote_port, local_addr, local_port, close_connection, connection_closed, nullptr); }); detail::shutdown_socket(sock); detail::close_socket(sock); return ret; } inline void Server::output_log(const Request &req, const Response &res) const { if (logger_) { std::lock_guard guard(logger_mutex_); logger_(req, res); } } inline void Server::output_pre_compression_log(const Request &req, const Response &res) const { if (pre_compression_logger_) { std::lock_guard guard(logger_mutex_); pre_compression_logger_(req, res); } } inline void Server::output_error_log(const Error &err, const Request *req) const { if (error_logger_) { std::lock_guard guard(logger_mutex_); error_logger_(err, req); } } /* * Group 5: ClientImpl and Client (Universal) implementation */ // HTTP client implementation inline ClientImpl::ClientImpl(const std::string &host) : ClientImpl(host, 80, std::string(), std::string()) {} inline ClientImpl::ClientImpl(const std::string &host, int port) : ClientImpl(host, port, std::string(), std::string()) {} inline ClientImpl::ClientImpl(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path) : host_(detail::escape_abstract_namespace_unix_domain(host)), port_(port), client_cert_path_(client_cert_path), client_key_path_(client_key_path) {} inline ClientImpl::~ClientImpl() { // Wait until all the requests in flight are handled. size_t retry_count = 10; while (retry_count-- > 0) { { std::lock_guard guard(socket_mutex_); if (socket_requests_in_flight_ == 0) { break; } } std::this_thread::sleep_for(std::chrono::milliseconds{1}); } std::lock_guard guard(socket_mutex_); shutdown_socket(socket_); close_socket(socket_); } inline bool ClientImpl::is_valid() const { return true; } inline void ClientImpl::copy_settings(const ClientImpl &rhs) { client_cert_path_ = rhs.client_cert_path_; client_key_path_ = rhs.client_key_path_; connection_timeout_sec_ = rhs.connection_timeout_sec_; read_timeout_sec_ = rhs.read_timeout_sec_; read_timeout_usec_ = rhs.read_timeout_usec_; write_timeout_sec_ = rhs.write_timeout_sec_; write_timeout_usec_ = rhs.write_timeout_usec_; max_timeout_msec_ = rhs.max_timeout_msec_; basic_auth_username_ = rhs.basic_auth_username_; basic_auth_password_ = rhs.basic_auth_password_; bearer_token_auth_token_ = rhs.bearer_token_auth_token_; keep_alive_ = rhs.keep_alive_; follow_location_ = rhs.follow_location_; path_encode_ = rhs.path_encode_; address_family_ = rhs.address_family_; tcp_nodelay_ = rhs.tcp_nodelay_; ipv6_v6only_ = rhs.ipv6_v6only_; socket_options_ = rhs.socket_options_; compress_ = rhs.compress_; decompress_ = rhs.decompress_; payload_max_length_ = rhs.payload_max_length_; has_payload_max_length_ = rhs.has_payload_max_length_; interface_ = rhs.interface_; proxy_host_ = rhs.proxy_host_; proxy_port_ = rhs.proxy_port_; proxy_basic_auth_username_ = rhs.proxy_basic_auth_username_; proxy_basic_auth_password_ = rhs.proxy_basic_auth_password_; proxy_bearer_token_auth_token_ = rhs.proxy_bearer_token_auth_token_; logger_ = rhs.logger_; error_logger_ = rhs.error_logger_; #ifdef CPPHTTPLIB_SSL_ENABLED digest_auth_username_ = rhs.digest_auth_username_; digest_auth_password_ = rhs.digest_auth_password_; proxy_digest_auth_username_ = rhs.proxy_digest_auth_username_; proxy_digest_auth_password_ = rhs.proxy_digest_auth_password_; ca_cert_file_path_ = rhs.ca_cert_file_path_; ca_cert_dir_path_ = rhs.ca_cert_dir_path_; server_certificate_verification_ = rhs.server_certificate_verification_; server_hostname_verification_ = rhs.server_hostname_verification_; #endif } inline socket_t ClientImpl::create_client_socket(Error &error) const { if (!proxy_host_.empty() && proxy_port_ != -1) { return detail::create_client_socket( proxy_host_, std::string(), proxy_port_, address_family_, tcp_nodelay_, ipv6_v6only_, socket_options_, connection_timeout_sec_, connection_timeout_usec_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, interface_, error); } // Check is custom IP specified for host_ std::string ip; auto it = addr_map_.find(host_); if (it != addr_map_.end()) { ip = it->second; } return detail::create_client_socket( host_, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_, socket_options_, connection_timeout_sec_, connection_timeout_usec_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, interface_, error); } inline bool ClientImpl::create_and_connect_socket(Socket &socket, Error &error) { auto sock = create_client_socket(error); if (sock == INVALID_SOCKET) { return false; } socket.sock = sock; return true; } inline bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) { return create_and_connect_socket(socket, error); } inline void ClientImpl::shutdown_ssl(Socket & /*socket*/, bool /*shutdown_gracefully*/) { // If there are any requests in flight from threads other than us, then it's // a thread-unsafe race because individual ssl* objects are not thread-safe. assert(socket_requests_in_flight_ == 0 || socket_requests_are_from_thread_ == std::this_thread::get_id()); } inline void ClientImpl::shutdown_socket(Socket &socket) const { if (socket.sock == INVALID_SOCKET) { return; } detail::shutdown_socket(socket.sock); } inline void ClientImpl::close_socket(Socket &socket) { // If there are requests in flight in another thread, usually closing // the socket will be fine and they will simply receive an error when // using the closed socket, but it is still a bug since rarely the OS // may reassign the socket id to be used for a new socket, and then // suddenly they will be operating on a live socket that is different // than the one they intended! assert(socket_requests_in_flight_ == 0 || socket_requests_are_from_thread_ == std::this_thread::get_id()); // It is also a bug if this happens while SSL is still active #ifdef CPPHTTPLIB_SSL_ENABLED assert(socket.ssl == nullptr); #endif if (socket.sock == INVALID_SOCKET) { return; } detail::close_socket(socket.sock); socket.sock = INVALID_SOCKET; } inline bool ClientImpl::read_response_line(Stream &strm, const Request &req, Response &res, bool skip_100_continue) const { std::array buf{}; detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); if (!line_reader.getline()) { return false; } #ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR thread_local const std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r?\n"); #else thread_local const std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n"); #endif std::cmatch m; if (!std::regex_match(line_reader.ptr(), m, re)) { return req.method == "CONNECT"; } res.version = std::string(m[1]); res.status = std::stoi(std::string(m[2])); res.reason = std::string(m[3]); // Ignore '100 Continue' (only when not using Expect: 100-continue explicitly) while (skip_100_continue && res.status == StatusCode::Continue_100) { if (!line_reader.getline()) { return false; } // CRLF if (!line_reader.getline()) { return false; } // next response line if (!std::regex_match(line_reader.ptr(), m, re)) { return false; } res.version = std::string(m[1]); res.status = std::stoi(std::string(m[2])); res.reason = std::string(m[3]); } return true; } inline bool ClientImpl::send(Request &req, Response &res, Error &error) { std::lock_guard request_mutex_guard(request_mutex_); auto ret = send_(req, res, error); if (error == Error::SSLPeerCouldBeClosed_) { assert(!ret); ret = send_(req, res, error); // If still failing with SSLPeerCouldBeClosed_, convert to Read error if (error == Error::SSLPeerCouldBeClosed_) { error = Error::Read; } } return ret; } inline bool ClientImpl::send_(Request &req, Response &res, Error &error) { { std::lock_guard guard(socket_mutex_); // Set this to false immediately - if it ever gets set to true by the end // of the request, we know another thread instructed us to close the // socket. socket_should_be_closed_when_request_is_done_ = false; auto is_alive = false; if (socket_.is_open()) { is_alive = detail::is_socket_alive(socket_.sock); #ifdef CPPHTTPLIB_SSL_ENABLED if (is_alive && is_ssl()) { if (tls::is_peer_closed(socket_.ssl, socket_.sock)) { is_alive = false; } } #endif if (!is_alive) { // Attempt to avoid sigpipe by shutting down non-gracefully if it // seems like the other side has already closed the connection Also, // there cannot be any requests in flight from other threads since we // locked request_mutex_, so safe to close everything immediately const bool shutdown_gracefully = false; shutdown_ssl(socket_, shutdown_gracefully); shutdown_socket(socket_); close_socket(socket_); } } if (!is_alive) { if (!ensure_socket_connection(socket_, error)) { output_error_log(error, &req); return false; } #ifdef CPPHTTPLIB_SSL_ENABLED // TODO: refactoring if (is_ssl()) { auto &scli = static_cast(*this); if (!proxy_host_.empty() && proxy_port_ != -1) { auto success = false; if (!scli.connect_with_proxy(socket_, req.start_time_, res, success, error)) { if (!success) { output_error_log(error, &req); } return success; } } if (!proxy_host_.empty() && proxy_port_ != -1) { if (!scli.initialize_ssl(socket_, error)) { output_error_log(error, &req); return false; } } } #endif } // Mark the current socket as being in use so that it cannot be closed by // anyone else while this request is ongoing, even though we will be // releasing the mutex. if (socket_requests_in_flight_ > 1) { assert(socket_requests_are_from_thread_ == std::this_thread::get_id()); } socket_requests_in_flight_ += 1; socket_requests_are_from_thread_ = std::this_thread::get_id(); } for (const auto &header : default_headers_) { if (req.headers.find(header.first) == req.headers.end()) { req.headers.insert(header); } } auto ret = false; auto close_connection = !keep_alive_; auto se = detail::scope_exit([&]() { // Briefly lock mutex in order to mark that a request is no longer ongoing std::lock_guard guard(socket_mutex_); socket_requests_in_flight_ -= 1; if (socket_requests_in_flight_ <= 0) { assert(socket_requests_in_flight_ == 0); socket_requests_are_from_thread_ = std::thread::id(); } if (socket_should_be_closed_when_request_is_done_ || close_connection || !ret) { shutdown_ssl(socket_, true); shutdown_socket(socket_); close_socket(socket_); } }); ret = process_socket(socket_, req.start_time_, [&](Stream &strm) { return handle_request(strm, req, res, close_connection, error); }); if (!ret) { if (error == Error::Success) { error = Error::Unknown; output_error_log(error, &req); } } return ret; } inline Result ClientImpl::send(const Request &req) { auto req2 = req; return send_(std::move(req2)); } inline Result ClientImpl::send_(Request &&req) { auto res = detail::make_unique(); auto error = Error::Success; auto ret = send(req, *res, error); #ifdef CPPHTTPLIB_SSL_ENABLED return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers), last_ssl_error_, last_backend_error_}; #else return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)}; #endif } inline void ClientImpl::prepare_default_headers(Request &r, bool for_stream, const std::string &ct) { (void)for_stream; for (const auto &header : default_headers_) { if (!r.has_header(header.first)) { r.headers.insert(header); } } if (!r.has_header("Host")) { if (address_family_ == AF_UNIX) { r.headers.emplace("Host", "localhost"); } else { r.headers.emplace( "Host", detail::make_host_and_port_string(host_, port_, is_ssl())); } } if (!r.has_header("Accept")) { r.headers.emplace("Accept", "*/*"); } if (!r.content_receiver) { if (!r.has_header("Accept-Encoding")) { std::string accept_encoding; #ifdef CPPHTTPLIB_BROTLI_SUPPORT accept_encoding = "br"; #endif #ifdef CPPHTTPLIB_ZLIB_SUPPORT if (!accept_encoding.empty()) { accept_encoding += ", "; } accept_encoding += "gzip, deflate"; #endif #ifdef CPPHTTPLIB_ZSTD_SUPPORT if (!accept_encoding.empty()) { accept_encoding += ", "; } accept_encoding += "zstd"; #endif r.set_header("Accept-Encoding", accept_encoding); } #ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT if (!r.has_header("User-Agent")) { auto agent = std::string("cpp-httplib/") + CPPHTTPLIB_VERSION; r.set_header("User-Agent", agent); } #endif } if (!r.body.empty()) { if (!ct.empty() && !r.has_header("Content-Type")) { r.headers.emplace("Content-Type", ct); } if (!r.has_header("Content-Length")) { r.headers.emplace("Content-Length", std::to_string(r.body.size())); } } } inline ClientImpl::StreamHandle ClientImpl::open_stream(const std::string &method, const std::string &path, const Params ¶ms, const Headers &headers, const std::string &body, const std::string &content_type) { StreamHandle handle; handle.response = detail::make_unique(); handle.error = Error::Success; auto query_path = params.empty() ? path : append_query_params(path, params); handle.connection_ = detail::make_unique(); { std::lock_guard guard(socket_mutex_); auto is_alive = false; if (socket_.is_open()) { is_alive = detail::is_socket_alive(socket_.sock); #ifdef CPPHTTPLIB_SSL_ENABLED if (is_alive && is_ssl()) { if (tls::is_peer_closed(socket_.ssl, socket_.sock)) { is_alive = false; } } #endif if (!is_alive) { shutdown_ssl(socket_, false); shutdown_socket(socket_); close_socket(socket_); } } if (!is_alive) { if (!ensure_socket_connection(socket_, handle.error)) { handle.response.reset(); return handle; } #ifdef CPPHTTPLIB_SSL_ENABLED if (is_ssl()) { auto &scli = static_cast(*this); if (!proxy_host_.empty() && proxy_port_ != -1) { if (!scli.initialize_ssl(socket_, handle.error)) { handle.response.reset(); return handle; } } } #endif } transfer_socket_ownership_to_handle(handle); } #ifdef CPPHTTPLIB_SSL_ENABLED if (is_ssl() && handle.connection_->session) { handle.socket_stream_ = detail::make_unique( handle.connection_->sock, handle.connection_->session, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_); } else { handle.socket_stream_ = detail::make_unique( handle.connection_->sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_); } #else handle.socket_stream_ = detail::make_unique( handle.connection_->sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_); #endif handle.stream_ = handle.socket_stream_.get(); Request req; req.method = method; req.path = query_path; req.headers = headers; req.body = body; prepare_default_headers(req, true, content_type); auto &strm = *handle.stream_; if (detail::write_request_line(strm, req.method, req.path) < 0) { handle.error = Error::Write; handle.response.reset(); return handle; } if (!detail::check_and_write_headers(strm, req.headers, header_writer_, handle.error)) { handle.response.reset(); return handle; } if (!body.empty()) { if (strm.write(body.data(), body.size()) < 0) { handle.error = Error::Write; handle.response.reset(); return handle; } } if (!read_response_line(strm, req, *handle.response) || !detail::read_headers(strm, handle.response->headers)) { handle.error = Error::Read; handle.response.reset(); return handle; } handle.body_reader_.stream = handle.stream_; handle.body_reader_.payload_max_length = payload_max_length_; auto content_length_str = handle.response->get_header_value("Content-Length"); if (!content_length_str.empty()) { handle.body_reader_.has_content_length = true; handle.body_reader_.content_length = static_cast(std::stoull(content_length_str)); } auto transfer_encoding = handle.response->get_header_value("Transfer-Encoding"); handle.body_reader_.chunked = (transfer_encoding == "chunked"); auto content_encoding = handle.response->get_header_value("Content-Encoding"); if (!content_encoding.empty()) { handle.decompressor_ = detail::create_decompressor(content_encoding); } return handle; } inline ssize_t ClientImpl::StreamHandle::read(char *buf, size_t len) { if (!is_valid() || !response) { return -1; } if (decompressor_) { return read_with_decompression(buf, len); } auto n = detail::read_body_content(stream_, body_reader_, buf, len); if (n <= 0 && body_reader_.chunked && !trailers_parsed_ && stream_) { trailers_parsed_ = true; if (body_reader_.chunked_decoder) { if (!body_reader_.chunked_decoder->parse_trailers_into( response->trailers, response->headers)) { return n; } } else { detail::ChunkedDecoder dec(*stream_); if (!dec.parse_trailers_into(response->trailers, response->headers)) { return n; } } } return n; } inline ssize_t ClientImpl::StreamHandle::read_with_decompression(char *buf, size_t len) { if (decompress_offset_ < decompress_buffer_.size()) { auto available = decompress_buffer_.size() - decompress_offset_; auto to_copy = (std::min)(len, available); std::memcpy(buf, decompress_buffer_.data() + decompress_offset_, to_copy); decompress_offset_ += to_copy; decompressed_bytes_read_ += to_copy; return static_cast(to_copy); } decompress_buffer_.clear(); decompress_offset_ = 0; constexpr size_t kDecompressionBufferSize = 8192; char compressed_buf[kDecompressionBufferSize]; while (true) { auto n = detail::read_body_content(stream_, body_reader_, compressed_buf, sizeof(compressed_buf)); if (n <= 0) { return n; } bool decompress_ok = decompressor_->decompress( compressed_buf, static_cast(n), [this](const char *data, size_t data_len) { decompress_buffer_.append(data, data_len); auto limit = body_reader_.payload_max_length; if (decompressed_bytes_read_ + decompress_buffer_.size() > limit) { return false; } return true; }); if (!decompress_ok) { body_reader_.last_error = Error::Read; return -1; } if (!decompress_buffer_.empty()) { break; } } auto to_copy = (std::min)(len, decompress_buffer_.size()); std::memcpy(buf, decompress_buffer_.data(), to_copy); decompress_offset_ = to_copy; decompressed_bytes_read_ += to_copy; return static_cast(to_copy); } inline void ClientImpl::StreamHandle::parse_trailers_if_needed() { if (!response || !stream_ || !body_reader_.chunked || trailers_parsed_) { return; } trailers_parsed_ = true; const auto bufsiz = 128; char line_buf[bufsiz]; detail::stream_line_reader line_reader(*stream_, line_buf, bufsiz); if (!line_reader.getline()) { return; } if (!detail::parse_trailers(line_reader, response->trailers, response->headers)) { return; } } namespace detail { inline ChunkedDecoder::ChunkedDecoder(Stream &s) : strm(s) {} inline ssize_t ChunkedDecoder::read_payload(char *buf, size_t len, size_t &out_chunk_offset, size_t &out_chunk_total) { if (finished) { return 0; } if (chunk_remaining == 0) { stream_line_reader lr(strm, line_buf, sizeof(line_buf)); if (!lr.getline()) { return -1; } char *endptr = nullptr; unsigned long chunk_len = std::strtoul(lr.ptr(), &endptr, 16); if (endptr == lr.ptr()) { return -1; } if (chunk_len == ULONG_MAX) { return -1; } if (chunk_len == 0) { chunk_remaining = 0; finished = true; out_chunk_offset = 0; out_chunk_total = 0; return 0; } chunk_remaining = static_cast(chunk_len); last_chunk_total = chunk_remaining; last_chunk_offset = 0; } auto to_read = (std::min)(chunk_remaining, len); auto n = strm.read(buf, to_read); if (n <= 0) { return -1; } auto offset_before = last_chunk_offset; last_chunk_offset += static_cast(n); chunk_remaining -= static_cast(n); out_chunk_offset = offset_before; out_chunk_total = last_chunk_total; if (chunk_remaining == 0) { stream_line_reader lr(strm, line_buf, sizeof(line_buf)); if (!lr.getline()) { return -1; } if (std::strcmp(lr.ptr(), "\r\n") != 0) { return -1; } } return n; } inline bool ChunkedDecoder::parse_trailers_into(Headers &dest, const Headers &src_headers) { stream_line_reader lr(strm, line_buf, sizeof(line_buf)); if (!lr.getline()) { return false; } return parse_trailers(lr, dest, src_headers); } } // namespace detail inline void ClientImpl::transfer_socket_ownership_to_handle(StreamHandle &handle) { handle.connection_->sock = socket_.sock; #ifdef CPPHTTPLIB_SSL_ENABLED handle.connection_->session = socket_.ssl; socket_.ssl = nullptr; #endif socket_.sock = INVALID_SOCKET; } inline bool ClientImpl::handle_request(Stream &strm, Request &req, Response &res, bool close_connection, Error &error) { if (req.path.empty()) { error = Error::Connection; output_error_log(error, &req); return false; } auto req_save = req; bool ret; if (!is_ssl() && !proxy_host_.empty() && proxy_port_ != -1) { auto req2 = req; req2.path = "http://" + detail::make_host_and_port_string(host_, port_, false) + req.path; ret = process_request(strm, req2, res, close_connection, error); req = std::move(req2); req.path = req_save.path; } else { ret = process_request(strm, req, res, close_connection, error); } if (!ret) { return false; } if (res.get_header_value("Connection") == "close" || (res.version == "HTTP/1.0" && res.reason != "Connection established")) { // TODO this requires a not-entirely-obvious chain of calls to be correct // for this to be safe. // This is safe to call because handle_request is only called by send_ // which locks the request mutex during the process. It would be a bug // to call it from a different thread since it's a thread-safety issue // to do these things to the socket if another thread is using the socket. std::lock_guard guard(socket_mutex_); shutdown_ssl(socket_, true); shutdown_socket(socket_); close_socket(socket_); } if (300 < res.status && res.status < 400 && follow_location_) { req = std::move(req_save); ret = redirect(req, res, error); } #ifdef CPPHTTPLIB_SSL_ENABLED if ((res.status == StatusCode::Unauthorized_401 || res.status == StatusCode::ProxyAuthenticationRequired_407) && req.authorization_count_ < 5) { auto is_proxy = res.status == StatusCode::ProxyAuthenticationRequired_407; const auto &username = is_proxy ? proxy_digest_auth_username_ : digest_auth_username_; const auto &password = is_proxy ? proxy_digest_auth_password_ : digest_auth_password_; if (!username.empty() && !password.empty()) { std::map auth; if (detail::parse_www_authenticate(res, auth, is_proxy)) { Request new_req = req; new_req.authorization_count_ += 1; new_req.headers.erase(is_proxy ? "Proxy-Authorization" : "Authorization"); new_req.headers.insert(detail::make_digest_authentication_header( req, auth, new_req.authorization_count_, detail::random_string(10), username, password, is_proxy)); Response new_res; ret = send(new_req, new_res, error); if (ret) { res = std::move(new_res); } } } } #endif return ret; } inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) { if (req.redirect_count_ == 0) { error = Error::ExceedRedirectCount; output_error_log(error, &req); return false; } auto location = res.get_header_value("location"); if (location.empty()) { return false; } thread_local const std::regex re( R"((?:(https?):)?(?://(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*)(\?[^#]*)?(?:#.*)?)"); std::smatch m; if (!std::regex_match(location, m, re)) { return false; } auto scheme = is_ssl() ? "https" : "http"; auto next_scheme = m[1].str(); auto next_host = m[2].str(); if (next_host.empty()) { next_host = m[3].str(); } auto port_str = m[4].str(); auto next_path = m[5].str(); auto next_query = m[6].str(); auto next_port = port_; if (!port_str.empty()) { next_port = std::stoi(port_str); } else if (!next_scheme.empty()) { next_port = next_scheme == "https" ? 443 : 80; } if (next_scheme.empty()) { next_scheme = scheme; } if (next_host.empty()) { next_host = host_; } if (next_path.empty()) { next_path = "/"; } auto path = decode_query_component(next_path, true) + next_query; // Same host redirect - use current client if (next_scheme == scheme && next_host == host_ && next_port == port_) { return detail::redirect(*this, req, res, path, location, error); } // Cross-host/scheme redirect - create new client with robust setup return create_redirect_client(next_scheme, next_host, next_port, req, res, path, location, error); } // New method for robust redirect client creation inline bool ClientImpl::create_redirect_client( const std::string &scheme, const std::string &host, int port, Request &req, Response &res, const std::string &path, const std::string &location, Error &error) { // Determine if we need SSL auto need_ssl = (scheme == "https"); // Clean up request headers that are host/client specific // Remove headers that should not be carried over to new host auto headers_to_remove = std::vector{"Host", "Proxy-Authorization", "Authorization"}; for (const auto &header_name : headers_to_remove) { auto it = req.headers.find(header_name); while (it != req.headers.end()) { it = req.headers.erase(it); it = req.headers.find(header_name); } } // Create appropriate client type and handle redirect if (need_ssl) { #ifdef CPPHTTPLIB_SSL_ENABLED // Create SSL client for HTTPS redirect SSLClient redirect_client(host, port); // Setup basic client configuration first setup_redirect_client(redirect_client); // SSL-specific configuration for proxy environments if (!proxy_host_.empty() && proxy_port_ != -1) { // Critical: Disable SSL verification for proxy environments redirect_client.enable_server_certificate_verification(false); redirect_client.enable_server_hostname_verification(false); } else { // For direct SSL connections, copy SSL verification settings redirect_client.enable_server_certificate_verification( server_certificate_verification_); redirect_client.enable_server_hostname_verification( server_hostname_verification_); } // Transfer CA certificate to redirect client if (!ca_cert_pem_.empty()) { redirect_client.load_ca_cert_store(ca_cert_pem_.c_str(), ca_cert_pem_.size()); } if (!ca_cert_file_path_.empty()) { redirect_client.set_ca_cert_path(ca_cert_file_path_, ca_cert_dir_path_); } // Client certificates are set through constructor for SSLClient // NOTE: SSLClient constructor already takes client_cert_path and // client_key_path so we need to create it properly if client certs are // needed // Execute the redirect return detail::redirect(redirect_client, req, res, path, location, error); #else // SSL not supported - set appropriate error error = Error::SSLConnection; output_error_log(error, &req); return false; #endif } else { // HTTP redirect ClientImpl redirect_client(host, port); // Setup client with robust configuration setup_redirect_client(redirect_client); // Execute the redirect return detail::redirect(redirect_client, req, res, path, location, error); } } // New method for robust client setup (based on basic_manual_redirect.cpp // logic) template inline void ClientImpl::setup_redirect_client(ClientType &client) { // Copy basic settings first client.set_connection_timeout(connection_timeout_sec_); client.set_read_timeout(read_timeout_sec_, read_timeout_usec_); client.set_write_timeout(write_timeout_sec_, write_timeout_usec_); client.set_keep_alive(keep_alive_); client.set_follow_location( true); // Enable redirects to handle multi-step redirects client.set_path_encode(path_encode_); client.set_compress(compress_); client.set_decompress(decompress_); // Copy authentication settings BEFORE proxy setup if (!basic_auth_username_.empty()) { client.set_basic_auth(basic_auth_username_, basic_auth_password_); } if (!bearer_token_auth_token_.empty()) { client.set_bearer_token_auth(bearer_token_auth_token_); } #ifdef CPPHTTPLIB_SSL_ENABLED if (!digest_auth_username_.empty()) { client.set_digest_auth(digest_auth_username_, digest_auth_password_); } #endif // Setup proxy configuration (CRITICAL ORDER - proxy must be set // before proxy auth) if (!proxy_host_.empty() && proxy_port_ != -1) { // First set proxy host and port client.set_proxy(proxy_host_, proxy_port_); // Then set proxy authentication (order matters!) if (!proxy_basic_auth_username_.empty()) { client.set_proxy_basic_auth(proxy_basic_auth_username_, proxy_basic_auth_password_); } if (!proxy_bearer_token_auth_token_.empty()) { client.set_proxy_bearer_token_auth(proxy_bearer_token_auth_token_); } #ifdef CPPHTTPLIB_SSL_ENABLED if (!proxy_digest_auth_username_.empty()) { client.set_proxy_digest_auth(proxy_digest_auth_username_, proxy_digest_auth_password_); } #endif } // Copy network and socket settings client.set_address_family(address_family_); client.set_tcp_nodelay(tcp_nodelay_); client.set_ipv6_v6only(ipv6_v6only_); if (socket_options_) { client.set_socket_options(socket_options_); } if (!interface_.empty()) { client.set_interface(interface_); } // Copy logging and headers if (logger_) { client.set_logger(logger_); } if (error_logger_) { client.set_error_logger(error_logger_); } // NOTE: DO NOT copy default_headers_ as they may contain stale Host headers // Each new client should generate its own headers based on its target host } inline bool ClientImpl::write_content_with_provider(Stream &strm, const Request &req, Error &error) const { auto is_shutting_down = []() { return false; }; if (req.is_chunked_content_provider_) { // TODO: Brotli support std::unique_ptr compressor; #ifdef CPPHTTPLIB_ZLIB_SUPPORT if (compress_) { compressor = detail::make_unique(); } else #endif { compressor = detail::make_unique(); } return detail::write_content_chunked(strm, req.content_provider_, is_shutting_down, *compressor, error); } else { return detail::write_content_with_progress( strm, req.content_provider_, 0, req.content_length_, is_shutting_down, req.upload_progress, error); } } inline bool ClientImpl::write_request(Stream &strm, Request &req, bool close_connection, Error &error, bool skip_body) { // Prepare additional headers if (close_connection) { if (!req.has_header("Connection")) { req.set_header("Connection", "close"); } } std::string ct_for_defaults; if (!req.has_header("Content-Type") && !req.body.empty()) { ct_for_defaults = "text/plain"; } prepare_default_headers(req, false, ct_for_defaults); if (req.body.empty()) { if (req.content_provider_) { if (!req.is_chunked_content_provider_) { if (!req.has_header("Content-Length")) { auto length = std::to_string(req.content_length_); req.set_header("Content-Length", length); } } } else { if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH") { req.set_header("Content-Length", "0"); } } } if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) { if (!req.has_header("Authorization")) { req.headers.insert(make_basic_authentication_header( basic_auth_username_, basic_auth_password_, false)); } } if (!proxy_basic_auth_username_.empty() && !proxy_basic_auth_password_.empty()) { if (!req.has_header("Proxy-Authorization")) { req.headers.insert(make_basic_authentication_header( proxy_basic_auth_username_, proxy_basic_auth_password_, true)); } } if (!bearer_token_auth_token_.empty()) { if (!req.has_header("Authorization")) { req.headers.insert(make_bearer_token_authentication_header( bearer_token_auth_token_, false)); } } if (!proxy_bearer_token_auth_token_.empty()) { if (!req.has_header("Proxy-Authorization")) { req.headers.insert(make_bearer_token_authentication_header( proxy_bearer_token_auth_token_, true)); } } // Request line and headers { detail::BufferStream bstrm; // Extract path and query from req.path std::string path_part, query_part; auto query_pos = req.path.find('?'); if (query_pos != std::string::npos) { path_part = req.path.substr(0, query_pos); query_part = req.path.substr(query_pos + 1); } else { path_part = req.path; query_part = ""; } // Encode path part. If the original `req.path` already contained a // query component, preserve its raw query string (including parameter // order) instead of reparsing and reassembling it which may reorder // parameters due to container ordering (e.g. `Params` uses // `std::multimap`). When there is no query in `req.path`, fall back to // building a query from `req.params` so existing callers that pass // `Params` continue to work. auto path_with_query = path_encode_ ? detail::encode_path(path_part) : path_part; if (!query_part.empty()) { // Normalize the query string (decode then re-encode) while preserving // the original parameter order. auto normalized = detail::normalize_query_string(query_part); if (!normalized.empty()) { path_with_query += '?' + normalized; } // Still populate req.params for handlers/users who read them. detail::parse_query_text(query_part, req.params); } else { // No query in path; parse any query_part (empty) and append params // from `req.params` when present (preserves prior behavior for // callers who provide Params separately). detail::parse_query_text(query_part, req.params); if (!req.params.empty()) { path_with_query = append_query_params(path_with_query, req.params); } } // Write request line and headers detail::write_request_line(bstrm, req.method, path_with_query); if (!detail::check_and_write_headers(bstrm, req.headers, header_writer_, error)) { output_error_log(error, &req); return false; } // Flush buffer auto &data = bstrm.get_buffer(); if (!detail::write_data(strm, data.data(), data.size())) { error = Error::Write; output_error_log(error, &req); return false; } } // After sending request line and headers, wait briefly for an early server // response (e.g. 4xx) and avoid sending a potentially large request body // unnecessarily. This workaround is only enabled on Windows because Unix // platforms surface write errors (EPIPE) earlier; on Windows kernel send // buffering can accept large writes even when the peer already responded. // Check the stream first (which covers SSL via `is_readable()`), then // fall back to select on the socket. Only perform the wait for very large // request bodies to avoid interfering with normal small requests and // reduce side-effects. Poll briefly (up to 50ms as default) for an early // response. Skip this check when using Expect: 100-continue, as the protocol // handles early responses properly. #if defined(_WIN32) if (!skip_body && req.body.size() > CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD && req.path.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) { auto start = std::chrono::high_resolution_clock::now(); for (;;) { // Prefer socket-level readiness to avoid SSL_pending() false-positives // from SSL internals. If the underlying socket is readable, assume an // early response may be present. auto sock = strm.socket(); if (sock != INVALID_SOCKET && detail::select_read(sock, 0, 0) > 0) { return false; } // Fallback to stream-level check for non-socket streams or when the // socket isn't reporting readable. Avoid using `is_readable()` for // SSL, since `SSL_pending()` may report buffered records that do not // indicate a complete application-level response yet. if (!is_ssl() && strm.is_readable()) { return false; } auto now = std::chrono::high_resolution_clock::now(); auto elapsed = std::chrono::duration_cast(now - start) .count(); if (elapsed >= CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND) { break; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } #endif // Body if (skip_body) { return true; } return write_request_body(strm, req, error); } inline bool ClientImpl::write_request_body(Stream &strm, Request &req, Error &error) { if (req.body.empty()) { return write_content_with_provider(strm, req, error); } if (req.upload_progress) { auto body_size = req.body.size(); size_t written = 0; auto data = req.body.data(); while (written < body_size) { size_t to_write = (std::min)(CPPHTTPLIB_SEND_BUFSIZ, body_size - written); if (!detail::write_data(strm, data + written, to_write)) { error = Error::Write; output_error_log(error, &req); return false; } written += to_write; if (!req.upload_progress(written, body_size)) { error = Error::Canceled; output_error_log(error, &req); return false; } } } else { if (!detail::write_data(strm, req.body.data(), req.body.size())) { error = Error::Write; output_error_log(error, &req); return false; } } return true; } inline std::unique_ptr ClientImpl::send_with_content_provider_and_receiver( Request &req, const char *body, size_t content_length, ContentProvider content_provider, ContentProviderWithoutLength content_provider_without_length, const std::string &content_type, ContentReceiver content_receiver, Error &error) { if (!content_type.empty()) { req.set_header("Content-Type", content_type); } #ifdef CPPHTTPLIB_ZLIB_SUPPORT if (compress_) { req.set_header("Content-Encoding", "gzip"); } #endif #ifdef CPPHTTPLIB_ZLIB_SUPPORT if (compress_ && !content_provider_without_length) { // TODO: Brotli support detail::gzip_compressor compressor; if (content_provider) { auto ok = true; size_t offset = 0; DataSink data_sink; data_sink.write = [&](const char *data, size_t data_len) -> bool { if (ok) { auto last = offset + data_len == content_length; auto ret = compressor.compress( data, data_len, last, [&](const char *compressed_data, size_t compressed_data_len) { req.body.append(compressed_data, compressed_data_len); return true; }); if (ret) { offset += data_len; } else { ok = false; } } return ok; }; while (ok && offset < content_length) { if (!content_provider(offset, content_length - offset, data_sink)) { error = Error::Canceled; output_error_log(error, &req); return nullptr; } } } else { if (!compressor.compress(body, content_length, true, [&](const char *data, size_t data_len) { req.body.append(data, data_len); return true; })) { error = Error::Compression; output_error_log(error, &req); return nullptr; } } } else #endif { if (content_provider) { req.content_length_ = content_length; req.content_provider_ = std::move(content_provider); req.is_chunked_content_provider_ = false; } else if (content_provider_without_length) { req.content_length_ = 0; req.content_provider_ = detail::ContentProviderAdapter( std::move(content_provider_without_length)); req.is_chunked_content_provider_ = true; req.set_header("Transfer-Encoding", "chunked"); } else { req.body.assign(body, content_length); } } if (content_receiver) { req.content_receiver = [content_receiver](const char *data, size_t data_length, size_t /*offset*/, size_t /*total_length*/) { return content_receiver(data, data_length); }; } auto res = detail::make_unique(); return send(req, *res, error) ? std::move(res) : nullptr; } inline Result ClientImpl::send_with_content_provider_and_receiver( const std::string &method, const std::string &path, const Headers &headers, const char *body, size_t content_length, ContentProvider content_provider, ContentProviderWithoutLength content_provider_without_length, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { Request req; req.method = method; req.headers = headers; req.path = path; req.upload_progress = std::move(progress); if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } auto error = Error::Success; auto res = send_with_content_provider_and_receiver( req, body, content_length, std::move(content_provider), std::move(content_provider_without_length), content_type, std::move(content_receiver), error); #ifdef CPPHTTPLIB_SSL_ENABLED return Result{std::move(res), error, std::move(req.headers), last_ssl_error_, last_backend_error_}; #else return Result{std::move(res), error, std::move(req.headers)}; #endif } inline void ClientImpl::output_log(const Request &req, const Response &res) const { if (logger_) { std::lock_guard guard(logger_mutex_); logger_(req, res); } } inline void ClientImpl::output_error_log(const Error &err, const Request *req) const { if (error_logger_) { std::lock_guard guard(logger_mutex_); error_logger_(err, req); } } inline bool ClientImpl::process_request(Stream &strm, Request &req, Response &res, bool close_connection, Error &error) { // Auto-add Expect: 100-continue for large bodies if (CPPHTTPLIB_EXPECT_100_THRESHOLD > 0 && !req.has_header("Expect")) { auto body_size = req.body.empty() ? req.content_length_ : req.body.size(); if (body_size >= CPPHTTPLIB_EXPECT_100_THRESHOLD) { req.set_header("Expect", "100-continue"); } } // Check for Expect: 100-continue auto expect_100_continue = req.get_header_value("Expect") == "100-continue"; // Send request (skip body if using Expect: 100-continue) auto write_request_success = write_request(strm, req, close_connection, error, expect_100_continue); #ifdef CPPHTTPLIB_SSL_ENABLED if (is_ssl() && !expect_100_continue) { auto is_proxy_enabled = !proxy_host_.empty() && proxy_port_ != -1; if (!is_proxy_enabled) { if (tls::is_peer_closed(socket_.ssl, socket_.sock)) { error = Error::SSLPeerCouldBeClosed_; output_error_log(error, &req); return false; } } } #endif // Handle Expect: 100-continue with timeout if (expect_100_continue && CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND > 0) { time_t sec = CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND / 1000; time_t usec = (CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND % 1000) * 1000; auto ret = detail::select_read(strm.socket(), sec, usec); if (ret <= 0) { // Timeout or error: send body anyway (server didn't respond in time) if (!write_request_body(strm, req, error)) { return false; } expect_100_continue = false; // Switch to normal response handling } } // Receive response and headers // When using Expect: 100-continue, don't auto-skip `100 Continue` response if (!read_response_line(strm, req, res, !expect_100_continue) || !detail::read_headers(strm, res.headers)) { if (write_request_success) { error = Error::Read; } output_error_log(error, &req); return false; } if (!write_request_success) { return false; } // Handle Expect: 100-continue response if (expect_100_continue) { if (res.status == StatusCode::Continue_100) { // Server accepted, send the body if (!write_request_body(strm, req, error)) { return false; } // Read the actual response res.headers.clear(); res.body.clear(); if (!read_response_line(strm, req, res) || !detail::read_headers(strm, res.headers)) { error = Error::Read; output_error_log(error, &req); return false; } } // If not 100 Continue, server returned an error; proceed with that response } // Body if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" && req.method != "CONNECT") { auto redirect = 300 < res.status && res.status < 400 && res.status != StatusCode::NotModified_304 && follow_location_; if (req.response_handler && !redirect) { if (!req.response_handler(res)) { error = Error::Canceled; output_error_log(error, &req); return false; } } auto out = req.content_receiver ? static_cast( [&](const char *buf, size_t n, size_t off, size_t len) { if (redirect) { return true; } auto ret = req.content_receiver(buf, n, off, len); if (!ret) { error = Error::Canceled; output_error_log(error, &req); } return ret; }) : static_cast( [&](const char *buf, size_t n, size_t /*off*/, size_t /*len*/) { assert(res.body.size() + n <= res.body.max_size()); if (payload_max_length_ > 0 && (res.body.size() >= payload_max_length_ || n > payload_max_length_ - res.body.size())) { return false; } res.body.append(buf, n); return true; }); auto progress = [&](size_t current, size_t total) { if (!req.download_progress || redirect) { return true; } auto ret = req.download_progress(current, total); if (!ret) { error = Error::Canceled; output_error_log(error, &req); } return ret; }; if (res.has_header("Content-Length")) { if (!req.content_receiver) { auto len = res.get_header_value_u64("Content-Length"); if (len > res.body.max_size()) { error = Error::Read; output_error_log(error, &req); return false; } res.body.reserve(static_cast(len)); } } if (res.status != StatusCode::NotModified_304) { int dummy_status; auto max_length = (!has_payload_max_length_ && req.content_receiver) ? (std::numeric_limits::max)() : payload_max_length_; if (!detail::read_content(strm, res, max_length, dummy_status, std::move(progress), std::move(out), decompress_)) { if (error != Error::Canceled) { error = Error::Read; } output_error_log(error, &req); return false; } } } // Log output_log(req, res); return true; } inline ContentProviderWithoutLength ClientImpl::get_multipart_content_provider( const std::string &boundary, const UploadFormDataItems &items, const FormDataProviderItems &provider_items) const { size_t cur_item = 0; size_t cur_start = 0; // cur_item and cur_start are copied to within the std::function and // maintain state between successive calls return [&, cur_item, cur_start](size_t offset, DataSink &sink) mutable -> bool { if (!offset && !items.empty()) { sink.os << detail::serialize_multipart_formdata(items, boundary, false); return true; } else if (cur_item < provider_items.size()) { if (!cur_start) { const auto &begin = detail::serialize_multipart_formdata_item_begin( provider_items[cur_item], boundary); offset += begin.size(); cur_start = offset; sink.os << begin; } DataSink cur_sink; auto has_data = true; cur_sink.write = sink.write; cur_sink.done = [&]() { has_data = false; }; if (!provider_items[cur_item].provider(offset - cur_start, cur_sink)) { return false; } if (!has_data) { sink.os << detail::serialize_multipart_formdata_item_end(); cur_item++; cur_start = 0; } return true; } else { sink.os << detail::serialize_multipart_formdata_finish(boundary); sink.done(); return true; } }; } inline bool ClientImpl::process_socket( const Socket &socket, std::chrono::time_point start_time, std::function callback) { return detail::process_client_socket( socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, std::move(callback)); } inline bool ClientImpl::is_ssl() const { return false; } inline Result ClientImpl::Get(const std::string &path, DownloadProgress progress) { return Get(path, Headers(), std::move(progress)); } inline Result ClientImpl::Get(const std::string &path, const Params ¶ms, const Headers &headers, DownloadProgress progress) { if (params.empty()) { return Get(path, headers); } std::string path_with_query = append_query_params(path, params); return Get(path_with_query, headers, std::move(progress)); } inline Result ClientImpl::Get(const std::string &path, const Headers &headers, DownloadProgress progress) { Request req; req.method = "GET"; req.path = path; req.headers = headers; req.download_progress = std::move(progress); if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } return send_(std::move(req)); } inline Result ClientImpl::Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress) { return Get(path, Headers(), nullptr, std::move(content_receiver), std::move(progress)); } inline Result ClientImpl::Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress) { return Get(path, headers, nullptr, std::move(content_receiver), std::move(progress)); } inline Result ClientImpl::Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress) { return Get(path, Headers(), std::move(response_handler), std::move(content_receiver), std::move(progress)); } inline Result ClientImpl::Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress) { Request req; req.method = "GET"; req.path = path; req.headers = headers; req.response_handler = std::move(response_handler); req.content_receiver = [content_receiver](const char *data, size_t data_length, size_t /*offset*/, size_t /*total_length*/) { return content_receiver(data, data_length); }; req.download_progress = std::move(progress); if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } return send_(std::move(req)); } inline Result ClientImpl::Get(const std::string &path, const Params ¶ms, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress) { return Get(path, params, headers, nullptr, std::move(content_receiver), std::move(progress)); } inline Result ClientImpl::Get(const std::string &path, const Params ¶ms, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress) { if (params.empty()) { return Get(path, headers, std::move(response_handler), std::move(content_receiver), std::move(progress)); } std::string path_with_query = append_query_params(path, params); return Get(path_with_query, headers, std::move(response_handler), std::move(content_receiver), std::move(progress)); } inline Result ClientImpl::Head(const std::string &path) { return Head(path, Headers()); } inline Result ClientImpl::Head(const std::string &path, const Headers &headers) { Request req; req.method = "HEAD"; req.headers = headers; req.path = path; if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } return send_(std::move(req)); } inline Result ClientImpl::Post(const std::string &path) { return Post(path, std::string(), std::string()); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers) { return Post(path, headers, nullptr, 0, std::string()); } inline Result ClientImpl::Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return Post(path, Headers(), body, content_length, content_type, progress); } inline Result ClientImpl::Post(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress) { return Post(path, Headers(), body, content_type, progress); } inline Result ClientImpl::Post(const std::string &path, const Params ¶ms) { return Post(path, Headers(), params); } inline Result ClientImpl::Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return Post(path, Headers(), content_length, std::move(content_provider), content_type, progress); } inline Result ClientImpl::Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return Post(path, Headers(), content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result ClientImpl::Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return Post(path, Headers(), std::move(content_provider), content_type, progress); } inline Result ClientImpl::Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return Post(path, Headers(), std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const Params ¶ms) { auto query = detail::params_to_query_str(params); return Post(path, headers, query, "application/x-www-form-urlencoded"); } inline Result ClientImpl::Post(const std::string &path, const UploadFormDataItems &items, UploadProgress progress) { return Post(path, Headers(), items, progress); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); const auto &body = detail::serialize_multipart_formdata(items, boundary); return Post(path, headers, body, content_type, progress); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress) { if (!detail::is_multipart_boundary_chars_valid(boundary)) { return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; } const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); const auto &body = detail::serialize_multipart_formdata(items, boundary); return Post(path, headers, body, content_type, progress); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "POST", path, headers, body, content_length, nullptr, nullptr, content_type, nullptr, progress); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "POST", path, headers, body.data(), body.size(), nullptr, nullptr, content_type, nullptr, progress); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "POST", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type, nullptr, progress); } inline Result ClientImpl::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) { return send_with_content_provider_and_receiver( "POST", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type, std::move(content_receiver), std::move(progress)); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "POST", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type, nullptr, progress); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { return send_with_content_provider_and_receiver( "POST", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type, std::move(content_receiver), std::move(progress)); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); return send_with_content_provider_and_receiver( "POST", path, headers, nullptr, 0, nullptr, get_multipart_content_provider(boundary, items, provider_items), content_type, nullptr, progress); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { Request req; req.method = "POST"; req.path = path; req.headers = headers; req.body = body; req.content_receiver = [content_receiver](const char *data, size_t data_length, size_t /*offset*/, size_t /*total_length*/) { return content_receiver(data, data_length); }; req.download_progress = std::move(progress); if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } if (!content_type.empty()) { req.set_header("Content-Type", content_type); } return send_(std::move(req)); } inline Result ClientImpl::Put(const std::string &path) { return Put(path, std::string(), std::string()); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers) { return Put(path, headers, nullptr, 0, std::string()); } inline Result ClientImpl::Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return Put(path, Headers(), body, content_length, content_type, progress); } inline Result ClientImpl::Put(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress) { return Put(path, Headers(), body, content_type, progress); } inline Result ClientImpl::Put(const std::string &path, const Params ¶ms) { return Put(path, Headers(), params); } inline Result ClientImpl::Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return Put(path, Headers(), content_length, std::move(content_provider), content_type, progress); } inline Result ClientImpl::Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return Put(path, Headers(), content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result ClientImpl::Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return Put(path, Headers(), std::move(content_provider), content_type, progress); } inline Result ClientImpl::Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return Put(path, Headers(), std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, const Params ¶ms) { auto query = detail::params_to_query_str(params); return Put(path, headers, query, "application/x-www-form-urlencoded"); } inline Result ClientImpl::Put(const std::string &path, const UploadFormDataItems &items, UploadProgress progress) { return Put(path, Headers(), items, progress); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); const auto &body = detail::serialize_multipart_formdata(items, boundary); return Put(path, headers, body, content_type, progress); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress) { if (!detail::is_multipart_boundary_chars_valid(boundary)) { return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; } const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); const auto &body = detail::serialize_multipart_formdata(items, boundary); return Put(path, headers, body, content_type, progress); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PUT", path, headers, body, content_length, nullptr, nullptr, content_type, nullptr, progress); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PUT", path, headers, body.data(), body.size(), nullptr, nullptr, content_type, nullptr, progress); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PUT", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type, nullptr, progress); } inline Result ClientImpl::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) { return send_with_content_provider_and_receiver( "PUT", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type, std::move(content_receiver), progress); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PUT", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type, nullptr, progress); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return send_with_content_provider_and_receiver( "PUT", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); return send_with_content_provider_and_receiver( "PUT", path, headers, nullptr, 0, nullptr, get_multipart_content_provider(boundary, items, provider_items), content_type, nullptr, progress); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { Request req; req.method = "PUT"; req.path = path; req.headers = headers; req.body = body; req.content_receiver = [content_receiver](const char *data, size_t data_length, size_t /*offset*/, size_t /*total_length*/) { return content_receiver(data, data_length); }; req.download_progress = std::move(progress); if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } if (!content_type.empty()) { req.set_header("Content-Type", content_type); } return send_(std::move(req)); } inline Result ClientImpl::Patch(const std::string &path) { return Patch(path, std::string(), std::string()); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, UploadProgress progress) { return Patch(path, headers, nullptr, 0, std::string(), progress); } inline Result ClientImpl::Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return Patch(path, Headers(), body, content_length, content_type, progress); } inline Result ClientImpl::Patch(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress) { return Patch(path, Headers(), body, content_type, progress); } inline Result ClientImpl::Patch(const std::string &path, const Params ¶ms) { return Patch(path, Headers(), params); } inline Result ClientImpl::Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return Patch(path, Headers(), content_length, std::move(content_provider), content_type, progress); } inline Result ClientImpl::Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return Patch(path, Headers(), content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result ClientImpl::Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return Patch(path, Headers(), std::move(content_provider), content_type, progress); } inline Result ClientImpl::Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return Patch(path, Headers(), std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, const Params ¶ms) { auto query = detail::params_to_query_str(params); return Patch(path, headers, query, "application/x-www-form-urlencoded"); } inline Result ClientImpl::Patch(const std::string &path, const UploadFormDataItems &items, UploadProgress progress) { return Patch(path, Headers(), items, progress); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); const auto &body = detail::serialize_multipart_formdata(items, boundary); return Patch(path, headers, body, content_type, progress); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress) { if (!detail::is_multipart_boundary_chars_valid(boundary)) { return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; } const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); const auto &body = detail::serialize_multipart_formdata(items, boundary); return Patch(path, headers, body, content_type, progress); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PATCH", path, headers, body, content_length, nullptr, nullptr, content_type, nullptr, progress); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PATCH", path, headers, body.data(), body.size(), nullptr, nullptr, content_type, nullptr, progress); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PATCH", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type, nullptr, progress); } inline Result ClientImpl::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) { return send_with_content_provider_and_receiver( "PATCH", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type, std::move(content_receiver), progress); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PATCH", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type, nullptr, progress); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return send_with_content_provider_and_receiver( "PATCH", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); return send_with_content_provider_and_receiver( "PATCH", path, headers, nullptr, 0, nullptr, get_multipart_content_provider(boundary, items, provider_items), content_type, nullptr, progress); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { Request req; req.method = "PATCH"; req.path = path; req.headers = headers; req.body = body; req.content_receiver = [content_receiver](const char *data, size_t data_length, size_t /*offset*/, size_t /*total_length*/) { return content_receiver(data, data_length); }; req.download_progress = std::move(progress); if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } if (!content_type.empty()) { req.set_header("Content-Type", content_type); } return send_(std::move(req)); } inline Result ClientImpl::Delete(const std::string &path, DownloadProgress progress) { return Delete(path, Headers(), std::string(), std::string(), progress); } inline Result ClientImpl::Delete(const std::string &path, const Headers &headers, DownloadProgress progress) { return Delete(path, headers, std::string(), std::string(), progress); } inline Result ClientImpl::Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress) { return Delete(path, Headers(), body, content_length, content_type, progress); } inline Result ClientImpl::Delete(const std::string &path, const std::string &body, const std::string &content_type, DownloadProgress progress) { return Delete(path, Headers(), body.data(), body.size(), content_type, progress); } inline Result ClientImpl::Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, DownloadProgress progress) { return Delete(path, headers, body.data(), body.size(), content_type, progress); } inline Result ClientImpl::Delete(const std::string &path, const Params ¶ms, DownloadProgress progress) { return Delete(path, Headers(), params, progress); } inline Result ClientImpl::Delete(const std::string &path, const Headers &headers, const Params ¶ms, DownloadProgress progress) { auto query = detail::params_to_query_str(params); return Delete(path, headers, query, "application/x-www-form-urlencoded", progress); } inline Result ClientImpl::Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress) { Request req; req.method = "DELETE"; req.headers = headers; req.path = path; req.download_progress = std::move(progress); if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } if (!content_type.empty()) { req.set_header("Content-Type", content_type); } req.body.assign(body, content_length); return send_(std::move(req)); } inline Result ClientImpl::Options(const std::string &path) { return Options(path, Headers()); } inline Result ClientImpl::Options(const std::string &path, const Headers &headers) { Request req; req.method = "OPTIONS"; req.headers = headers; req.path = path; if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } return send_(std::move(req)); } inline void ClientImpl::stop() { std::lock_guard guard(socket_mutex_); // If there is anything ongoing right now, the ONLY thread-safe thing we can // do is to shutdown_socket, so that threads using this socket suddenly // discover they can't read/write any more and error out. Everything else // (closing the socket, shutting ssl down) is unsafe because these actions // are not thread-safe. if (socket_requests_in_flight_ > 0) { shutdown_socket(socket_); // Aside from that, we set a flag for the socket to be closed when we're // done. socket_should_be_closed_when_request_is_done_ = true; return; } // Otherwise, still holding the mutex, we can shut everything down ourselves shutdown_ssl(socket_, true); shutdown_socket(socket_); close_socket(socket_); } inline std::string ClientImpl::host() const { return host_; } inline int ClientImpl::port() const { return port_; } inline size_t ClientImpl::is_socket_open() const { std::lock_guard guard(socket_mutex_); return socket_.is_open(); } inline socket_t ClientImpl::socket() const { return socket_.sock; } inline void ClientImpl::set_connection_timeout(time_t sec, time_t usec) { connection_timeout_sec_ = sec; connection_timeout_usec_ = usec; } inline void ClientImpl::set_read_timeout(time_t sec, time_t usec) { read_timeout_sec_ = sec; read_timeout_usec_ = usec; } inline void ClientImpl::set_write_timeout(time_t sec, time_t usec) { write_timeout_sec_ = sec; write_timeout_usec_ = usec; } inline void ClientImpl::set_max_timeout(time_t msec) { max_timeout_msec_ = msec; } inline void ClientImpl::set_basic_auth(const std::string &username, const std::string &password) { basic_auth_username_ = username; basic_auth_password_ = password; } inline void ClientImpl::set_bearer_token_auth(const std::string &token) { bearer_token_auth_token_ = token; } inline void ClientImpl::set_keep_alive(bool on) { keep_alive_ = on; } inline void ClientImpl::set_follow_location(bool on) { follow_location_ = on; } inline void ClientImpl::set_path_encode(bool on) { path_encode_ = on; } inline void ClientImpl::set_hostname_addr_map(std::map addr_map) { addr_map_ = std::move(addr_map); } inline void ClientImpl::set_default_headers(Headers headers) { default_headers_ = std::move(headers); } inline void ClientImpl::set_header_writer( std::function const &writer) { header_writer_ = writer; } inline void ClientImpl::set_address_family(int family) { address_family_ = family; } inline void ClientImpl::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; } inline void ClientImpl::set_ipv6_v6only(bool on) { ipv6_v6only_ = on; } inline void ClientImpl::set_socket_options(SocketOptions socket_options) { socket_options_ = std::move(socket_options); } inline void ClientImpl::set_compress(bool on) { compress_ = on; } inline void ClientImpl::set_decompress(bool on) { decompress_ = on; } inline void ClientImpl::set_payload_max_length(size_t length) { payload_max_length_ = length; has_payload_max_length_ = true; } inline void ClientImpl::set_interface(const std::string &intf) { interface_ = intf; } inline void ClientImpl::set_proxy(const std::string &host, int port) { proxy_host_ = host; proxy_port_ = port; } inline void ClientImpl::set_proxy_basic_auth(const std::string &username, const std::string &password) { proxy_basic_auth_username_ = username; proxy_basic_auth_password_ = password; } inline void ClientImpl::set_proxy_bearer_token_auth(const std::string &token) { proxy_bearer_token_auth_token_ = token; } #ifdef CPPHTTPLIB_SSL_ENABLED inline void ClientImpl::set_digest_auth(const std::string &username, const std::string &password) { digest_auth_username_ = username; digest_auth_password_ = password; } inline void ClientImpl::set_ca_cert_path(const std::string &ca_cert_file_path, const std::string &ca_cert_dir_path) { ca_cert_file_path_ = ca_cert_file_path; ca_cert_dir_path_ = ca_cert_dir_path; } inline void ClientImpl::set_proxy_digest_auth(const std::string &username, const std::string &password) { proxy_digest_auth_username_ = username; proxy_digest_auth_password_ = password; } inline void ClientImpl::enable_server_certificate_verification(bool enabled) { server_certificate_verification_ = enabled; } inline void ClientImpl::enable_server_hostname_verification(bool enabled) { server_hostname_verification_ = enabled; } #endif // ClientImpl::set_ca_cert_store is defined after TLS namespace (uses helpers) #ifdef CPPHTTPLIB_OPENSSL_SUPPORT inline X509_STORE *ClientImpl::create_ca_cert_store(const char *ca_cert, std::size_t size) const { auto mem = BIO_new_mem_buf(ca_cert, static_cast(size)); auto se = detail::scope_exit([&] { BIO_free_all(mem); }); if (!mem) { return nullptr; } auto inf = PEM_X509_INFO_read_bio(mem, nullptr, nullptr, nullptr); if (!inf) { return nullptr; } auto cts = X509_STORE_new(); if (cts) { for (auto i = 0; i < static_cast(sk_X509_INFO_num(inf)); i++) { auto itmp = sk_X509_INFO_value(inf, i); if (!itmp) { continue; } if (itmp->x509) { X509_STORE_add_cert(cts, itmp->x509); } if (itmp->crl) { X509_STORE_add_crl(cts, itmp->crl); } } } sk_X509_INFO_pop_free(inf, X509_INFO_free); return cts; } inline void ClientImpl::set_server_certificate_verifier( std::function /*verifier*/) { // Base implementation does nothing - SSLClient overrides this } #endif inline void ClientImpl::set_logger(Logger logger) { logger_ = std::move(logger); } inline void ClientImpl::set_error_logger(ErrorLogger error_logger) { error_logger_ = std::move(error_logger); } /* * SSL/TLS Common Implementation */ inline ClientConnection::~ClientConnection() { #ifdef CPPHTTPLIB_SSL_ENABLED if (session) { tls::shutdown(session, true); tls::free_session(session); session = nullptr; } #endif if (sock != INVALID_SOCKET) { detail::close_socket(sock); sock = INVALID_SOCKET; } } // Universal client implementation inline Client::Client(const std::string &scheme_host_port) : Client(scheme_host_port, std::string(), std::string()) {} inline Client::Client(const std::string &scheme_host_port, const std::string &client_cert_path, const std::string &client_key_path) { const static std::regex re( R"((?:([a-z]+):\/\/)?(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)"); std::smatch m; if (std::regex_match(scheme_host_port, m, re)) { auto scheme = m[1].str(); #ifdef CPPHTTPLIB_SSL_ENABLED if (!scheme.empty() && (scheme != "http" && scheme != "https")) { #else if (!scheme.empty() && scheme != "http") { #endif #ifndef CPPHTTPLIB_NO_EXCEPTIONS std::string msg = "'" + scheme + "' scheme is not supported."; throw std::invalid_argument(msg); #endif return; } auto is_ssl = scheme == "https"; auto host = m[2].str(); if (host.empty()) { host = m[3].str(); } auto port_str = m[4].str(); auto port = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80); if (is_ssl) { #ifdef CPPHTTPLIB_SSL_ENABLED cli_ = detail::make_unique(host, port, client_cert_path, client_key_path); is_ssl_ = is_ssl; #endif } else { cli_ = detail::make_unique(host, port, client_cert_path, client_key_path); } } else { // NOTE: Update TEST(UniversalClientImplTest, Ipv6LiteralAddress) // if port param below changes. cli_ = detail::make_unique(scheme_host_port, 80, client_cert_path, client_key_path); } } // namespace detail inline Client::Client(const std::string &host, int port) : cli_(detail::make_unique(host, port)) {} inline Client::Client(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path) : cli_(detail::make_unique(host, port, client_cert_path, client_key_path)) {} inline Client::~Client() = default; inline bool Client::is_valid() const { return cli_ != nullptr && cli_->is_valid(); } inline Result Client::Get(const std::string &path, DownloadProgress progress) { return cli_->Get(path, std::move(progress)); } inline Result Client::Get(const std::string &path, const Headers &headers, DownloadProgress progress) { return cli_->Get(path, headers, std::move(progress)); } inline Result Client::Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Get(path, std::move(content_receiver), std::move(progress)); } inline Result Client::Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Get(path, headers, std::move(content_receiver), std::move(progress)); } inline Result Client::Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Get(path, std::move(response_handler), std::move(content_receiver), std::move(progress)); } inline Result Client::Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Get(path, headers, std::move(response_handler), std::move(content_receiver), std::move(progress)); } inline Result Client::Get(const std::string &path, const Params ¶ms, const Headers &headers, DownloadProgress progress) { return cli_->Get(path, params, headers, std::move(progress)); } inline Result Client::Get(const std::string &path, const Params ¶ms, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Get(path, params, headers, std::move(content_receiver), std::move(progress)); } inline Result Client::Get(const std::string &path, const Params ¶ms, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Get(path, params, headers, std::move(response_handler), std::move(content_receiver), std::move(progress)); } inline Result Client::Head(const std::string &path) { return cli_->Head(path); } inline Result Client::Head(const std::string &path, const Headers &headers) { return cli_->Head(path, headers); } inline Result Client::Post(const std::string &path) { return cli_->Post(path); } inline Result Client::Post(const std::string &path, const Headers &headers) { return cli_->Post(path, headers); } inline Result Client::Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, body, content_length, content_type, progress); } inline Result Client::Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, headers, body, content_length, content_type, progress); } inline Result Client::Post(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, body, content_type, progress); } inline Result Client::Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, headers, body, content_type, progress); } inline Result Client::Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, content_length, std::move(content_provider), content_type, progress); } inline Result Client::Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Post(path, content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result Client::Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, std::move(content_provider), content_type, progress); } inline Result Client::Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Post(path, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result Client::Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, headers, content_length, std::move(content_provider), content_type, progress); } inline Result Client::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) { return cli_->Post(path, headers, content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result Client::Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, headers, std::move(content_provider), content_type, progress); } inline Result Client::Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Post(path, headers, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result Client::Post(const std::string &path, const Params ¶ms) { return cli_->Post(path, params); } inline Result Client::Post(const std::string &path, const Headers &headers, const Params ¶ms) { return cli_->Post(path, headers, params); } inline Result Client::Post(const std::string &path, const UploadFormDataItems &items, UploadProgress progress) { return cli_->Post(path, items, progress); } inline Result Client::Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress) { return cli_->Post(path, headers, items, progress); } inline Result Client::Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress) { return cli_->Post(path, headers, items, boundary, progress); } inline Result Client::Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress) { return cli_->Post(path, headers, items, provider_items, progress); } inline Result Client::Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Post(path, headers, body, content_type, std::move(content_receiver), progress); } inline Result Client::Put(const std::string &path) { return cli_->Put(path); } inline Result Client::Put(const std::string &path, const Headers &headers) { return cli_->Put(path, headers); } inline Result Client::Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, body, content_length, content_type, progress); } inline Result Client::Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, headers, body, content_length, content_type, progress); } inline Result Client::Put(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, body, content_type, progress); } inline Result Client::Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, headers, body, content_type, progress); } inline Result Client::Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, content_length, std::move(content_provider), content_type, progress); } inline Result Client::Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Put(path, content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result Client::Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, std::move(content_provider), content_type, progress); } inline Result Client::Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Put(path, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result Client::Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, headers, content_length, std::move(content_provider), content_type, progress); } inline Result Client::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) { return cli_->Put(path, headers, content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result Client::Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, headers, std::move(content_provider), content_type, progress); } inline Result Client::Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Put(path, headers, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result Client::Put(const std::string &path, const Params ¶ms) { return cli_->Put(path, params); } inline Result Client::Put(const std::string &path, const Headers &headers, const Params ¶ms) { return cli_->Put(path, headers, params); } inline Result Client::Put(const std::string &path, const UploadFormDataItems &items, UploadProgress progress) { return cli_->Put(path, items, progress); } inline Result Client::Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress) { return cli_->Put(path, headers, items, progress); } inline Result Client::Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress) { return cli_->Put(path, headers, items, boundary, progress); } inline Result Client::Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress) { return cli_->Put(path, headers, items, provider_items, progress); } inline Result Client::Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Put(path, headers, body, content_type, content_receiver, progress); } inline Result Client::Patch(const std::string &path) { return cli_->Patch(path); } inline Result Client::Patch(const std::string &path, const Headers &headers) { return cli_->Patch(path, headers); } inline Result Client::Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, body, content_length, content_type, progress); } inline Result Client::Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, headers, body, content_length, content_type, progress); } inline Result Client::Patch(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, body, content_type, progress); } inline Result Client::Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, headers, body, content_type, progress); } inline Result Client::Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, content_length, std::move(content_provider), content_type, progress); } inline Result Client::Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Patch(path, content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result Client::Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, std::move(content_provider), content_type, progress); } inline Result Client::Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Patch(path, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result Client::Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, headers, content_length, std::move(content_provider), content_type, progress); } inline Result Client::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) { return cli_->Patch(path, headers, content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result Client::Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, headers, std::move(content_provider), content_type, progress); } inline Result Client::Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Patch(path, headers, std::move(content_provider), content_type, std::move(content_receiver), progress); } inline Result Client::Patch(const std::string &path, const Params ¶ms) { return cli_->Patch(path, params); } inline Result Client::Patch(const std::string &path, const Headers &headers, const Params ¶ms) { return cli_->Patch(path, headers, params); } inline Result Client::Patch(const std::string &path, const UploadFormDataItems &items, UploadProgress progress) { return cli_->Patch(path, items, progress); } inline Result Client::Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress) { return cli_->Patch(path, headers, items, progress); } inline Result Client::Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress) { return cli_->Patch(path, headers, items, boundary, progress); } inline Result Client::Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress) { return cli_->Patch(path, headers, items, provider_items, progress); } inline Result Client::Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Patch(path, headers, body, content_type, content_receiver, progress); } inline Result Client::Delete(const std::string &path, DownloadProgress progress) { return cli_->Delete(path, progress); } inline Result Client::Delete(const std::string &path, const Headers &headers, DownloadProgress progress) { return cli_->Delete(path, headers, progress); } inline Result Client::Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress) { return cli_->Delete(path, body, content_length, content_type, progress); } inline Result Client::Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress) { return cli_->Delete(path, headers, body, content_length, content_type, progress); } inline Result Client::Delete(const std::string &path, const std::string &body, const std::string &content_type, DownloadProgress progress) { return cli_->Delete(path, body, content_type, progress); } inline Result Client::Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, DownloadProgress progress) { return cli_->Delete(path, headers, body, content_type, progress); } inline Result Client::Delete(const std::string &path, const Params ¶ms, DownloadProgress progress) { return cli_->Delete(path, params, progress); } inline Result Client::Delete(const std::string &path, const Headers &headers, const Params ¶ms, DownloadProgress progress) { return cli_->Delete(path, headers, params, progress); } inline Result Client::Options(const std::string &path) { return cli_->Options(path); } inline Result Client::Options(const std::string &path, const Headers &headers) { return cli_->Options(path, headers); } inline ClientImpl::StreamHandle Client::open_stream(const std::string &method, const std::string &path, const Params ¶ms, const Headers &headers, const std::string &body, const std::string &content_type) { return cli_->open_stream(method, path, params, headers, body, content_type); } inline bool Client::send(Request &req, Response &res, Error &error) { return cli_->send(req, res, error); } inline Result Client::send(const Request &req) { return cli_->send(req); } inline void Client::stop() { cli_->stop(); } inline std::string Client::host() const { return cli_->host(); } inline int Client::port() const { return cli_->port(); } inline size_t Client::is_socket_open() const { return cli_->is_socket_open(); } inline socket_t Client::socket() const { return cli_->socket(); } inline void Client::set_hostname_addr_map(std::map addr_map) { cli_->set_hostname_addr_map(std::move(addr_map)); } inline void Client::set_default_headers(Headers headers) { cli_->set_default_headers(std::move(headers)); } inline void Client::set_header_writer( std::function const &writer) { cli_->set_header_writer(writer); } inline void Client::set_address_family(int family) { cli_->set_address_family(family); } inline void Client::set_tcp_nodelay(bool on) { cli_->set_tcp_nodelay(on); } inline void Client::set_socket_options(SocketOptions socket_options) { cli_->set_socket_options(std::move(socket_options)); } inline void Client::set_connection_timeout(time_t sec, time_t usec) { cli_->set_connection_timeout(sec, usec); } inline void Client::set_read_timeout(time_t sec, time_t usec) { cli_->set_read_timeout(sec, usec); } inline void Client::set_write_timeout(time_t sec, time_t usec) { cli_->set_write_timeout(sec, usec); } inline void Client::set_basic_auth(const std::string &username, const std::string &password) { cli_->set_basic_auth(username, password); } inline void Client::set_bearer_token_auth(const std::string &token) { cli_->set_bearer_token_auth(token); } inline void Client::set_keep_alive(bool on) { cli_->set_keep_alive(on); } inline void Client::set_follow_location(bool on) { cli_->set_follow_location(on); } inline void Client::set_path_encode(bool on) { cli_->set_path_encode(on); } [[deprecated("Use set_path_encode instead")]] inline void Client::set_url_encode(bool on) { cli_->set_path_encode(on); } inline void Client::set_compress(bool on) { cli_->set_compress(on); } inline void Client::set_decompress(bool on) { cli_->set_decompress(on); } inline void Client::set_payload_max_length(size_t length) { cli_->set_payload_max_length(length); } inline void Client::set_interface(const std::string &intf) { cli_->set_interface(intf); } inline void Client::set_proxy(const std::string &host, int port) { cli_->set_proxy(host, port); } inline void Client::set_proxy_basic_auth(const std::string &username, const std::string &password) { cli_->set_proxy_basic_auth(username, password); } inline void Client::set_proxy_bearer_token_auth(const std::string &token) { cli_->set_proxy_bearer_token_auth(token); } inline void Client::set_logger(Logger logger) { cli_->set_logger(std::move(logger)); } inline void Client::set_error_logger(ErrorLogger error_logger) { cli_->set_error_logger(std::move(error_logger)); } /* * Group 6: SSL Server and Client implementation */ #ifdef CPPHTTPLIB_SSL_ENABLED // SSL HTTP server implementation inline SSLServer::SSLServer(const char *cert_path, const char *private_key_path, const char *client_ca_cert_file_path, const char *client_ca_cert_dir_path, const char *private_key_password) { using namespace tls; ctx_ = create_server_context(); if (!ctx_) { return; } // Load server certificate and private key if (!set_server_cert_file(ctx_, cert_path, private_key_path, private_key_password)) { last_ssl_error_ = static_cast(get_error()); free_context(ctx_); ctx_ = nullptr; return; } // Load client CA certificates for client authentication if (client_ca_cert_file_path || client_ca_cert_dir_path) { if (!set_client_ca_file(ctx_, client_ca_cert_file_path, client_ca_cert_dir_path)) { last_ssl_error_ = static_cast(get_error()); free_context(ctx_); ctx_ = nullptr; return; } // Enable client certificate verification set_verify_client(ctx_, true); } } inline SSLServer::SSLServer(const PemMemory &pem) { using namespace tls; ctx_ = create_server_context(); if (ctx_) { if (!set_server_cert_pem(ctx_, pem.cert_pem, pem.key_pem, pem.private_key_password)) { last_ssl_error_ = static_cast(get_error()); free_context(ctx_); ctx_ = nullptr; } else if (pem.client_ca_pem && pem.client_ca_pem_len > 0) { if (!load_ca_pem(ctx_, pem.client_ca_pem, pem.client_ca_pem_len)) { last_ssl_error_ = static_cast(get_error()); free_context(ctx_); ctx_ = nullptr; } else { set_verify_client(ctx_, true); } } } } inline SSLServer::SSLServer(const tls::ContextSetupCallback &setup_callback) { using namespace tls; ctx_ = create_server_context(); if (ctx_) { if (!setup_callback(ctx_)) { free_context(ctx_); ctx_ = nullptr; } } } inline SSLServer::~SSLServer() { if (ctx_) { tls::free_context(ctx_); } } inline bool SSLServer::is_valid() const { return ctx_ != nullptr; } inline bool SSLServer::process_and_close_socket(socket_t sock) { using namespace tls; // Create TLS session with mutex protection session_t session = nullptr; { std::lock_guard guard(ctx_mutex_); session = create_session(static_cast(ctx_), sock); } if (!session) { last_ssl_error_ = static_cast(get_error()); detail::shutdown_socket(sock); detail::close_socket(sock); return false; } // Use scope_exit to ensure cleanup on all paths (including exceptions) bool handshake_done = false; bool ret = false; auto cleanup = detail::scope_exit([&] { // Shutdown gracefully if handshake succeeded and processing was successful if (handshake_done) { shutdown(session, ret); } free_session(session); detail::shutdown_socket(sock); detail::close_socket(sock); }); // Perform TLS accept handshake with timeout TlsError tls_err; if (!accept_nonblocking(session, sock, read_timeout_sec_, read_timeout_usec_, &tls_err)) { #ifdef CPPHTTPLIB_OPENSSL_SUPPORT // Map TlsError to legacy ssl_error for backward compatibility if (tls_err.code == ErrorCode::WantRead) { last_ssl_error_ = SSL_ERROR_WANT_READ; } else if (tls_err.code == ErrorCode::WantWrite) { last_ssl_error_ = SSL_ERROR_WANT_WRITE; } else { last_ssl_error_ = SSL_ERROR_SSL; } #else last_ssl_error_ = static_cast(get_error()); #endif return false; } handshake_done = true; std::string remote_addr; int remote_port = 0; detail::get_remote_ip_and_port(sock, remote_addr, remote_port); std::string local_addr; int local_port = 0; detail::get_local_ip_and_port(sock, local_addr, local_port); ret = detail::process_server_socket_ssl( svr_sock_, session, sock, keep_alive_max_count_, keep_alive_timeout_sec_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, [&](Stream &strm, bool close_connection, bool &connection_closed) { return process_request(strm, remote_addr, remote_port, local_addr, local_port, close_connection, connection_closed, [&](Request &req) { req.ssl = session; }); }); return ret; } inline bool SSLServer::update_certs_pem(const char *cert_pem, const char *key_pem, const char *client_ca_pem, const char *password) { if (!ctx_) { return false; } std::lock_guard guard(ctx_mutex_); if (!tls::update_server_cert(ctx_, cert_pem, key_pem, password)) { return false; } if (client_ca_pem) { return tls::update_server_client_ca(ctx_, client_ca_pem); } return true; } // SSL HTTP client implementation inline SSLClient::~SSLClient() { if (ctx_) { tls::free_context(ctx_); } // Make sure to shut down SSL since shutdown_ssl will resolve to the // base function rather than the derived function once we get to the // base class destructor, and won't free the SSL (causing a leak). shutdown_ssl_impl(socket_, true); } inline bool SSLClient::is_valid() const { return ctx_ != nullptr; } inline void SSLClient::shutdown_ssl(Socket &socket, bool shutdown_gracefully) { shutdown_ssl_impl(socket, shutdown_gracefully); } inline void SSLClient::shutdown_ssl_impl(Socket &socket, bool shutdown_gracefully) { if (socket.sock == INVALID_SOCKET) { assert(socket.ssl == nullptr); return; } if (socket.ssl) { tls::shutdown(socket.ssl, shutdown_gracefully); { std::lock_guard guard(ctx_mutex_); tls::free_session(socket.ssl); } socket.ssl = nullptr; } assert(socket.ssl == nullptr); } inline bool SSLClient::process_socket( const Socket &socket, std::chrono::time_point start_time, std::function callback) { assert(socket.ssl); return detail::process_client_socket_ssl( socket.ssl, socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, std::move(callback)); } inline bool SSLClient::is_ssl() const { return true; } inline bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) { if (!is_valid()) { error = Error::SSLConnection; return false; } return ClientImpl::create_and_connect_socket(socket, error); } // Assumes that socket_mutex_ is locked and that there are no requests in // flight inline bool SSLClient::connect_with_proxy( Socket &socket, std::chrono::time_point start_time, Response &res, bool &success, Error &error) { success = true; Response proxy_res; if (!detail::process_client_socket( socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, [&](Stream &strm) { Request req2; req2.method = "CONNECT"; req2.path = detail::make_host_and_port_string_always_port(host_, port_); if (max_timeout_msec_ > 0) { req2.start_time_ = std::chrono::steady_clock::now(); } return process_request(strm, req2, proxy_res, false, error); })) { // Thread-safe to close everything because we are assuming there are no // requests in flight shutdown_ssl(socket, true); shutdown_socket(socket); close_socket(socket); success = false; return false; } if (proxy_res.status == StatusCode::ProxyAuthenticationRequired_407) { if (!proxy_digest_auth_username_.empty() && !proxy_digest_auth_password_.empty()) { std::map auth; if (detail::parse_www_authenticate(proxy_res, auth, true)) { // Close the current socket and create a new one for the authenticated // request shutdown_ssl(socket, true); shutdown_socket(socket); close_socket(socket); // Create a new socket for the authenticated CONNECT request if (!ensure_socket_connection(socket, error)) { success = false; output_error_log(error, nullptr); return false; } proxy_res = Response(); if (!detail::process_client_socket( socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, [&](Stream &strm) { Request req3; req3.method = "CONNECT"; req3.path = detail::make_host_and_port_string_always_port( host_, port_); req3.headers.insert(detail::make_digest_authentication_header( req3, auth, 1, detail::random_string(10), proxy_digest_auth_username_, proxy_digest_auth_password_, true)); if (max_timeout_msec_ > 0) { req3.start_time_ = std::chrono::steady_clock::now(); } return process_request(strm, req3, proxy_res, false, error); })) { // Thread-safe to close everything because we are assuming there are // no requests in flight shutdown_ssl(socket, true); shutdown_socket(socket); close_socket(socket); success = false; return false; } } } } // If status code is not 200, proxy request is failed. // Set error to ProxyConnection and return proxy response // as the response of the request if (proxy_res.status != StatusCode::OK_200) { error = Error::ProxyConnection; output_error_log(error, nullptr); res = std::move(proxy_res); // Thread-safe to close everything because we are assuming there are // no requests in flight shutdown_ssl(socket, true); shutdown_socket(socket); close_socket(socket); return false; } return true; } inline bool SSLClient::ensure_socket_connection(Socket &socket, Error &error) { if (!ClientImpl::ensure_socket_connection(socket, error)) { return false; } if (!proxy_host_.empty() && proxy_port_ != -1) { return true; } if (!initialize_ssl(socket, error)) { shutdown_socket(socket); close_socket(socket); return false; } return true; } // SSL HTTP client implementation inline SSLClient::SSLClient(const std::string &host) : SSLClient(host, 443, std::string(), std::string()) {} inline SSLClient::SSLClient(const std::string &host, int port) : SSLClient(host, port, std::string(), std::string()) {} inline SSLClient::SSLClient(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path, const std::string &private_key_password) : ClientImpl(host, port, client_cert_path, client_key_path) { ctx_ = tls::create_client_context(); if (!ctx_) { return; } tls::set_min_version(ctx_, tls::Version::TLS1_2); if (!client_cert_path.empty() && !client_key_path.empty()) { const char *password = private_key_password.empty() ? nullptr : private_key_password.c_str(); if (!tls::set_client_cert_file(ctx_, client_cert_path.c_str(), client_key_path.c_str(), password)) { last_backend_error_ = tls::get_error(); tls::free_context(ctx_); ctx_ = nullptr; } } } inline SSLClient::SSLClient(const std::string &host, int port, const PemMemory &pem) : ClientImpl(host, port) { ctx_ = tls::create_client_context(); if (!ctx_) { return; } tls::set_min_version(ctx_, tls::Version::TLS1_2); if (pem.cert_pem && pem.key_pem) { if (!tls::set_client_cert_pem(ctx_, pem.cert_pem, pem.key_pem, pem.private_key_password)) { last_backend_error_ = tls::get_error(); tls::free_context(ctx_); ctx_ = nullptr; } } } inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) { if (ca_cert_store && ctx_) { // set_ca_store takes ownership of ca_cert_store tls::set_ca_store(ctx_, ca_cert_store); } else if (ca_cert_store) { tls::free_ca_store(ca_cert_store); } } inline void SSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) { if (!ctx_) { return; } tls::set_verify_callback(ctx_, verifier); } inline void SSLClient::set_session_verifier( std::function verifier) { session_verifier_ = std::move(verifier); } #if defined(_WIN32) && \ !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) inline void SSLClient::enable_windows_certificate_verification(bool enabled) { enable_windows_cert_verification_ = enabled; } #endif inline void SSLClient::load_ca_cert_store(const char *ca_cert, std::size_t size) { if (ctx_ && ca_cert && size > 0) { ca_cert_pem_.assign(ca_cert, size); // Store for redirect transfer tls::load_ca_pem(ctx_, ca_cert, size); } } inline bool SSLClient::load_certs() { auto ret = true; std::call_once(initialize_cert_, [&]() { std::lock_guard guard(ctx_mutex_); if (!ca_cert_file_path_.empty()) { if (!tls::load_ca_file(ctx_, ca_cert_file_path_.c_str())) { last_backend_error_ = tls::get_error(); ret = false; } } else if (!ca_cert_dir_path_.empty()) { if (!tls::load_ca_dir(ctx_, ca_cert_dir_path_.c_str())) { last_backend_error_ = tls::get_error(); ret = false; } } else if (ca_cert_pem_.empty()) { if (!tls::load_system_certs(ctx_)) { last_backend_error_ = tls::get_error(); } } }); return ret; } inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { using namespace tls; // Load CA certificates if server verification is enabled if (server_certificate_verification_) { if (!load_certs()) { error = Error::SSLLoadingCerts; output_error_log(error, nullptr); return false; } } bool is_ip = detail::is_ip_address(host_); #ifdef CPPHTTPLIB_MBEDTLS_SUPPORT // MbedTLS needs explicit verification mode (OpenSSL uses SSL_VERIFY_NONE // by default and performs all verification post-handshake). // For IP addresses with verification enabled, use OPTIONAL mode since // MbedTLS requires hostname for VERIFY_REQUIRED. if (is_ip && server_certificate_verification_) { set_verify_client(ctx_, false); } else { set_verify_client(ctx_, server_certificate_verification_); } #endif // Create TLS session session_t session = nullptr; { std::lock_guard guard(ctx_mutex_); session = create_session(ctx_, socket.sock); } if (!session) { error = Error::SSLConnection; last_backend_error_ = get_error(); return false; } // Use scope_exit to ensure session is freed on error paths bool success = false; auto session_guard = detail::scope_exit([&] { if (!success) { free_session(session); } }); // Set SNI extension (skip for IP addresses per RFC 6066). // On MbedTLS, set_sni also enables hostname verification internally. // On OpenSSL, set_sni only sets SNI; verification is done post-handshake. if (!is_ip) { if (!set_sni(session, host_.c_str())) { error = Error::SSLConnection; last_backend_error_ = get_error(); return false; } } // Perform non-blocking TLS handshake with timeout TlsError tls_err; if (!connect_nonblocking(session, socket.sock, connection_timeout_sec_, connection_timeout_usec_, &tls_err)) { last_ssl_error_ = static_cast(tls_err.code); last_backend_error_ = tls_err.backend_code; if (tls_err.code == ErrorCode::CertVerifyFailed) { error = Error::SSLServerVerification; } else if (tls_err.code == ErrorCode::HostnameMismatch) { error = Error::SSLServerHostnameVerification; } else { error = Error::SSLConnection; } output_error_log(error, nullptr); return false; } // Post-handshake session verifier callback auto verification_status = SSLVerifierResponse::NoDecisionMade; if (session_verifier_) { verification_status = session_verifier_(session); } if (verification_status == SSLVerifierResponse::CertificateRejected) { last_backend_error_ = get_error(); error = Error::SSLServerVerification; output_error_log(error, nullptr); return false; } // Default server certificate verification if (verification_status == SSLVerifierResponse::NoDecisionMade && server_certificate_verification_) { verify_result_ = tls::get_verify_result(session); if (verify_result_ != 0) { last_backend_error_ = static_cast(verify_result_); error = Error::SSLServerVerification; output_error_log(error, nullptr); return false; } auto server_cert = get_peer_cert(session); if (!server_cert) { last_backend_error_ = get_error(); error = Error::SSLServerVerification; output_error_log(error, nullptr); return false; } auto cert_guard = detail::scope_exit([&] { free_cert(server_cert); }); // Hostname verification (post-handshake for all cases). // On OpenSSL, verification is always post-handshake (SSL_VERIFY_NONE). // On MbedTLS, set_sni already enabled hostname verification during // handshake for non-IP hosts, but this check is still needed for IP // addresses where SNI is not set. if (server_hostname_verification_) { if (!verify_hostname(server_cert, host_.c_str())) { last_backend_error_ = hostname_mismatch_code(); error = Error::SSLServerHostnameVerification; output_error_log(error, nullptr); return false; } } #if defined(_WIN32) && \ !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) // Additional Windows Schannel verification. // This provides real-time certificate validation with Windows Update // integration, working with both OpenSSL and MbedTLS backends. // Skip when a custom CA cert is specified, as the Windows certificate // store would not know about user-provided CA certificates. if (enable_windows_cert_verification_ && ca_cert_file_path_.empty() && ca_cert_dir_path_.empty() && ca_cert_pem_.empty()) { std::vector der; if (get_cert_der(server_cert, der)) { unsigned long wincrypt_error = 0; if (!detail::verify_cert_with_windows_schannel( der, host_, server_hostname_verification_, wincrypt_error)) { last_backend_error_ = wincrypt_error; error = Error::SSLServerVerification; output_error_log(error, nullptr); return false; } } } #endif } success = true; socket.ssl = session; return true; } inline void Client::set_digest_auth(const std::string &username, const std::string &password) { cli_->set_digest_auth(username, password); } inline void Client::set_proxy_digest_auth(const std::string &username, const std::string &password) { cli_->set_proxy_digest_auth(username, password); } inline void Client::enable_server_certificate_verification(bool enabled) { cli_->enable_server_certificate_verification(enabled); } inline void Client::enable_server_hostname_verification(bool enabled) { cli_->enable_server_hostname_verification(enabled); } #if defined(_WIN32) && \ !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) inline void Client::enable_windows_certificate_verification(bool enabled) { if (is_ssl_) { static_cast(*cli_).enable_windows_certificate_verification( enabled); } } #endif inline void Client::set_ca_cert_path(const std::string &ca_cert_file_path, const std::string &ca_cert_dir_path) { cli_->set_ca_cert_path(ca_cert_file_path, ca_cert_dir_path); } inline void Client::set_ca_cert_store(tls::ca_store_t ca_cert_store) { if (is_ssl_) { static_cast(*cli_).set_ca_cert_store(ca_cert_store); } else if (ca_cert_store) { tls::free_ca_store(ca_cert_store); } } inline void Client::load_ca_cert_store(const char *ca_cert, std::size_t size) { set_ca_cert_store(tls::create_ca_store(ca_cert, size)); } inline void Client::set_server_certificate_verifier(tls::VerifyCallback verifier) { if (is_ssl_) { static_cast(*cli_).set_server_certificate_verifier( std::move(verifier)); } } inline void Client::set_session_verifier( std::function verifier) { if (is_ssl_) { static_cast(*cli_).set_session_verifier(std::move(verifier)); } } inline tls::ctx_t Client::tls_context() const { if (is_ssl_) { return static_cast(*cli_).tls_context(); } return nullptr; } #endif // CPPHTTPLIB_SSL_ENABLED /* * Group 7: TLS abstraction layer - Common API */ #ifdef CPPHTTPLIB_SSL_ENABLED namespace tls { // Helper for PeerCert construction inline PeerCert get_peer_cert_from_session(const_session_t session) { return PeerCert(get_peer_cert(session)); } namespace impl { inline VerifyCallback &get_verify_callback() { static thread_local VerifyCallback callback; return callback; } inline VerifyCallback &get_mbedtls_verify_callback() { static thread_local VerifyCallback callback; return callback; } } // namespace impl inline bool set_client_ca_file(ctx_t ctx, const char *ca_file, const char *ca_dir) { if (!ctx) { return false; } bool success = true; if (ca_file && *ca_file) { if (!load_ca_file(ctx, ca_file)) { success = false; } } if (ca_dir && *ca_dir) { if (!load_ca_dir(ctx, ca_dir)) { success = false; } } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT // Set CA list for client certificate request (CertificateRequest message) if (ca_file && *ca_file) { auto list = SSL_load_client_CA_file(ca_file); if (list) { SSL_CTX_set_client_CA_list(static_cast(ctx), list); } } #endif return success; } inline bool set_server_cert_pem(ctx_t ctx, const char *cert, const char *key, const char *password) { return set_client_cert_pem(ctx, cert, key, password); } inline bool set_server_cert_file(ctx_t ctx, const char *cert_path, const char *key_path, const char *password) { return set_client_cert_file(ctx, cert_path, key_path, password); } // PeerCert implementation inline PeerCert::PeerCert() = default; inline PeerCert::PeerCert(cert_t cert) : cert_(cert) {} inline PeerCert::PeerCert(PeerCert &&other) noexcept : cert_(other.cert_) { other.cert_ = nullptr; } inline PeerCert &PeerCert::operator=(PeerCert &&other) noexcept { if (this != &other) { if (cert_) { free_cert(cert_); } cert_ = other.cert_; other.cert_ = nullptr; } return *this; } inline PeerCert::~PeerCert() { if (cert_) { free_cert(cert_); } } inline PeerCert::operator bool() const { return cert_ != nullptr; } inline std::string PeerCert::subject_cn() const { return cert_ ? get_cert_subject_cn(cert_) : std::string(); } inline std::string PeerCert::issuer_name() const { return cert_ ? get_cert_issuer_name(cert_) : std::string(); } inline bool PeerCert::check_hostname(const char *hostname) const { return cert_ ? verify_hostname(cert_, hostname) : false; } inline std::vector PeerCert::sans() const { std::vector result; if (cert_) { get_cert_sans(cert_, result); } return result; } inline bool PeerCert::validity(time_t ¬_before, time_t ¬_after) const { return cert_ ? get_cert_validity(cert_, not_before, not_after) : false; } inline std::string PeerCert::serial() const { return cert_ ? get_cert_serial(cert_) : std::string(); } // VerifyContext method implementations inline std::string VerifyContext::subject_cn() const { return cert ? get_cert_subject_cn(cert) : std::string(); } inline std::string VerifyContext::issuer_name() const { return cert ? get_cert_issuer_name(cert) : std::string(); } inline bool VerifyContext::check_hostname(const char *hostname) const { return cert ? verify_hostname(cert, hostname) : false; } inline std::vector VerifyContext::sans() const { std::vector result; if (cert) { get_cert_sans(cert, result); } return result; } inline bool VerifyContext::validity(time_t ¬_before, time_t ¬_after) const { return cert ? get_cert_validity(cert, not_before, not_after) : false; } inline std::string VerifyContext::serial() const { return cert ? get_cert_serial(cert) : std::string(); } // TlsError static method implementation inline std::string TlsError::verify_error_to_string(long error_code) { return verify_error_string(error_code); } } // namespace tls // Request::peer_cert() implementation inline tls::PeerCert Request::peer_cert() const { return tls::get_peer_cert_from_session(ssl); } // Request::sni() implementation inline std::string Request::sni() const { if (!ssl) { return std::string(); } const char *s = tls::get_sni(ssl); return s ? std::string(s) : std::string(); } #endif // CPPHTTPLIB_SSL_ENABLED /* * Group 8: TLS abstraction layer - OpenSSL backend */ #ifdef CPPHTTPLIB_OPENSSL_SUPPORT inline SSL_CTX *Client::ssl_context() const { if (is_ssl_) { return static_cast(*cli_).ssl_context(); } return nullptr; } inline void Client::set_server_certificate_verifier( std::function verifier) { cli_->set_server_certificate_verifier(verifier); } inline long Client::get_verify_result() const { if (is_ssl_) { return static_cast(*cli_).get_verify_result(); } return -1; // NOTE: -1 doesn't match any of X509_V_ERR_??? } #endif // CPPHTTPLIB_OPENSSL_SUPPORT /* * OpenSSL Backend Implementation */ #ifdef CPPHTTPLIB_OPENSSL_SUPPORT namespace tls { namespace impl { // OpenSSL-specific helpers for converting native types to PEM inline std::string x509_to_pem(X509 *cert) { if (!cert) return {}; BIO *bio = BIO_new(BIO_s_mem()); if (!bio) return {}; if (PEM_write_bio_X509(bio, cert) != 1) { BIO_free(bio); return {}; } char *data = nullptr; long len = BIO_get_mem_data(bio, &data); std::string pem(data, static_cast(len)); BIO_free(bio); return pem; } inline std::string evp_pkey_to_pem(EVP_PKEY *key) { if (!key) return {}; BIO *bio = BIO_new(BIO_s_mem()); if (!bio) return {}; if (PEM_write_bio_PrivateKey(bio, key, nullptr, nullptr, 0, nullptr, nullptr) != 1) { BIO_free(bio); return {}; } char *data = nullptr; long len = BIO_get_mem_data(bio, &data); std::string pem(data, static_cast(len)); BIO_free(bio); return pem; } inline std::string x509_store_to_pem(X509_STORE *store) { if (!store) return {}; std::string pem; auto objs = X509_STORE_get0_objects(store); if (!objs) return {}; auto count = sk_X509_OBJECT_num(objs); for (decltype(count) i = 0; i < count; i++) { auto obj = sk_X509_OBJECT_value(objs, i); if (X509_OBJECT_get_type(obj) == X509_LU_X509) { auto cert = X509_OBJECT_get0_X509(obj); if (cert) { pem += x509_to_pem(cert); } } } return pem; } // Helper to map OpenSSL SSL_get_error to ErrorCode inline ErrorCode map_ssl_error(int ssl_error, int &out_errno) { switch (ssl_error) { case SSL_ERROR_NONE: return ErrorCode::Success; case SSL_ERROR_WANT_READ: return ErrorCode::WantRead; case SSL_ERROR_WANT_WRITE: return ErrorCode::WantWrite; case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed; case SSL_ERROR_SYSCALL: out_errno = errno; return ErrorCode::SyscallError; case SSL_ERROR_SSL: default: return ErrorCode::Fatal; } } // Helper: Create client CA list from PEM string // Returns a new STACK_OF(X509_NAME)* or nullptr on failure // Caller takes ownership of returned list inline STACK_OF(X509_NAME) * create_client_ca_list_from_pem(const char *ca_pem) { if (!ca_pem) { return nullptr; } auto ca_list = sk_X509_NAME_new_null(); if (!ca_list) { return nullptr; } BIO *bio = BIO_new_mem_buf(ca_pem, -1); if (!bio) { sk_X509_NAME_pop_free(ca_list, X509_NAME_free); return nullptr; } X509 *cert = nullptr; while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) != nullptr) { X509_NAME *name = X509_get_subject_name(cert); if (name) { sk_X509_NAME_push(ca_list, X509_NAME_dup(name)); } X509_free(cert); } BIO_free(bio); return ca_list; } // Helper: Extract CA names from X509_STORE // Returns a new STACK_OF(X509_NAME)* or nullptr on failure // Caller takes ownership of returned list inline STACK_OF(X509_NAME) * extract_client_ca_list_from_store(X509_STORE *store) { if (!store) { return nullptr; } auto ca_list = sk_X509_NAME_new_null(); if (!ca_list) { return nullptr; } auto objs = X509_STORE_get0_objects(store); if (!objs) { sk_X509_NAME_free(ca_list); return nullptr; } auto count = sk_X509_OBJECT_num(objs); for (decltype(count) i = 0; i < count; i++) { auto obj = sk_X509_OBJECT_value(objs, i); if (X509_OBJECT_get_type(obj) == X509_LU_X509) { auto cert = X509_OBJECT_get0_X509(obj); if (cert) { auto subject = X509_get_subject_name(cert); if (subject) { auto name_dup = X509_NAME_dup(subject); if (name_dup) { sk_X509_NAME_push(ca_list, name_dup); } } } } } if (sk_X509_NAME_num(ca_list) == 0) { sk_X509_NAME_free(ca_list); return nullptr; } return ca_list; } // OpenSSL verify callback wrapper inline int openssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { auto &callback = get_verify_callback(); if (!callback) { return preverify_ok; } // Get SSL object from X509_STORE_CTX auto ssl = static_cast( X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); if (!ssl) { return preverify_ok; } // Get current certificate and depth auto cert = X509_STORE_CTX_get_current_cert(ctx); int depth = X509_STORE_CTX_get_error_depth(ctx); int error = X509_STORE_CTX_get_error(ctx); // Build context VerifyContext verify_ctx; verify_ctx.session = static_cast(ssl); verify_ctx.cert = static_cast(cert); verify_ctx.depth = depth; verify_ctx.preverify_ok = (preverify_ok != 0); verify_ctx.error_code = error; verify_ctx.error_string = (error != X509_V_OK) ? X509_verify_cert_error_string(error) : nullptr; return callback(verify_ctx) ? 1 : 0; } } // namespace impl inline ctx_t create_client_context() { SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); if (ctx) { // Disable auto-retry to properly handle non-blocking I/O SSL_CTX_clear_mode(ctx, SSL_MODE_AUTO_RETRY); // Set minimum TLS version SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); } return static_cast(ctx); } inline void free_context(ctx_t ctx) { if (ctx) { SSL_CTX_free(static_cast(ctx)); } } inline bool set_min_version(ctx_t ctx, Version version) { if (!ctx) return false; return SSL_CTX_set_min_proto_version(static_cast(ctx), static_cast(version)) == 1; } inline bool load_ca_pem(ctx_t ctx, const char *pem, size_t len) { if (!ctx || !pem || len == 0) return false; auto ssl_ctx = static_cast(ctx); auto store = SSL_CTX_get_cert_store(ssl_ctx); if (!store) return false; auto bio = BIO_new_mem_buf(pem, static_cast(len)); if (!bio) return false; bool ok = true; X509 *cert = nullptr; while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) != nullptr) { if (X509_STORE_add_cert(store, cert) != 1) { // Ignore duplicate errors auto err = ERR_peek_last_error(); if (ERR_GET_REASON(err) != X509_R_CERT_ALREADY_IN_HASH_TABLE) { ok = false; } } X509_free(cert); if (!ok) break; } BIO_free(bio); // Clear any "no more certificates" errors ERR_clear_error(); return ok; } inline bool load_ca_file(ctx_t ctx, const char *file_path) { if (!ctx || !file_path) return false; return SSL_CTX_load_verify_locations(static_cast(ctx), file_path, nullptr) == 1; } inline bool load_ca_dir(ctx_t ctx, const char *dir_path) { if (!ctx || !dir_path) return false; return SSL_CTX_load_verify_locations(static_cast(ctx), nullptr, dir_path) == 1; } inline bool load_system_certs(ctx_t ctx) { if (!ctx) return false; auto ssl_ctx = static_cast(ctx); #ifdef _WIN32 // Windows: Load from system certificate store (ROOT and CA) auto store = SSL_CTX_get_cert_store(ssl_ctx); if (!store) return false; bool loaded_any = false; static const wchar_t *store_names[] = {L"ROOT", L"CA"}; for (auto store_name : store_names) { auto hStore = CertOpenSystemStoreW(NULL, store_name); if (!hStore) continue; PCCERT_CONTEXT pContext = nullptr; while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) != nullptr) { const unsigned char *data = pContext->pbCertEncoded; auto x509 = d2i_X509(nullptr, &data, pContext->cbCertEncoded); if (x509) { if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; } X509_free(x509); } } CertCloseStore(hStore, 0); } return loaded_any; #elif defined(__APPLE__) #ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN // macOS: Load from Keychain auto store = SSL_CTX_get_cert_store(ssl_ctx); if (!store) return false; CFArrayRef certs = nullptr; if (SecTrustCopyAnchorCertificates(&certs) != errSecSuccess || !certs) { return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; } bool loaded_any = false; auto count = CFArrayGetCount(certs); for (CFIndex i = 0; i < count; i++) { auto cert = reinterpret_cast( const_cast(CFArrayGetValueAtIndex(certs, i))); CFDataRef der = SecCertificateCopyData(cert); if (der) { const unsigned char *data = CFDataGetBytePtr(der); auto x509 = d2i_X509(nullptr, &data, CFDataGetLength(der)); if (x509) { if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; } X509_free(x509); } CFRelease(der); } } CFRelease(certs); return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; #else return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; #endif #else // Other Unix: use default verify paths return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; #endif } inline bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key, const char *password) { if (!ctx || !cert || !key) return false; auto ssl_ctx = static_cast(ctx); // Load certificate auto cert_bio = BIO_new_mem_buf(cert, -1); if (!cert_bio) return false; auto x509 = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr); BIO_free(cert_bio); if (!x509) return false; auto cert_ok = SSL_CTX_use_certificate(ssl_ctx, x509) == 1; X509_free(x509); if (!cert_ok) return false; // Load private key auto key_bio = BIO_new_mem_buf(key, -1); if (!key_bio) return false; auto pkey = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, password ? const_cast(password) : nullptr); BIO_free(key_bio); if (!pkey) return false; auto key_ok = SSL_CTX_use_PrivateKey(ssl_ctx, pkey) == 1; EVP_PKEY_free(pkey); return key_ok && SSL_CTX_check_private_key(ssl_ctx) == 1; } inline bool set_client_cert_file(ctx_t ctx, const char *cert_path, const char *key_path, const char *password) { if (!ctx || !cert_path || !key_path) return false; auto ssl_ctx = static_cast(ctx); if (password && password[0] != '\0') { SSL_CTX_set_default_passwd_cb_userdata( ssl_ctx, reinterpret_cast(const_cast(password))); } return SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_path) == 1 && SSL_CTX_use_PrivateKey_file(ssl_ctx, key_path, SSL_FILETYPE_PEM) == 1; } inline ctx_t create_server_context() { SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); if (ctx) { SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION | SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); } return static_cast(ctx); } inline void set_verify_client(ctx_t ctx, bool require) { if (!ctx) return; SSL_CTX_set_verify(static_cast(ctx), require ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) : SSL_VERIFY_NONE, nullptr); } inline session_t create_session(ctx_t ctx, socket_t sock) { if (!ctx || sock == INVALID_SOCKET) return nullptr; auto ssl_ctx = static_cast(ctx); SSL *ssl = SSL_new(ssl_ctx); if (!ssl) return nullptr; // Disable auto-retry for proper non-blocking I/O handling SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY); auto bio = BIO_new_socket(static_cast(sock), BIO_NOCLOSE); if (!bio) { SSL_free(ssl); return nullptr; } SSL_set_bio(ssl, bio, bio); return static_cast(ssl); } inline void free_session(session_t session) { if (session) { SSL_free(static_cast(session)); } } inline bool set_sni(session_t session, const char *hostname) { if (!session || !hostname) return false; auto ssl = static_cast(session); // Set SNI (Server Name Indication) only - does not enable verification #if defined(OPENSSL_IS_BORINGSSL) return SSL_set_tlsext_host_name(ssl, hostname) == 1; #else // Direct call instead of macro to suppress -Wold-style-cast warning return SSL_ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, static_cast(const_cast(hostname))) == 1; #endif } inline bool set_hostname(session_t session, const char *hostname) { if (!session || !hostname) return false; auto ssl = static_cast(session); // Set SNI (Server Name Indication) if (!set_sni(session, hostname)) { return false; } // Enable hostname verification auto param = SSL_get0_param(ssl); if (!param) return false; X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); if (X509_VERIFY_PARAM_set1_host(param, hostname, 0) != 1) { return false; } SSL_set_verify(ssl, SSL_VERIFY_PEER, nullptr); return true; } inline TlsError connect(session_t session) { if (!session) { return TlsError(); } auto ssl = static_cast(session); auto ret = SSL_connect(ssl); TlsError err; if (ret == 1) { err.code = ErrorCode::Success; } else { auto ssl_err = SSL_get_error(ssl, ret); err.code = impl::map_ssl_error(ssl_err, err.sys_errno); err.backend_code = ERR_get_error(); } return err; } inline TlsError accept(session_t session) { if (!session) { return TlsError(); } auto ssl = static_cast(session); auto ret = SSL_accept(ssl); TlsError err; if (ret == 1) { err.code = ErrorCode::Success; } else { auto ssl_err = SSL_get_error(ssl, ret); err.code = impl::map_ssl_error(ssl_err, err.sys_errno); err.backend_code = ERR_get_error(); } return err; } inline bool connect_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err) { if (!session) { if (err) { err->code = ErrorCode::Fatal; } return false; } auto ssl = static_cast(session); auto bio = SSL_get_rbio(ssl); // Set non-blocking mode for handshake detail::set_nonblocking(sock, true); if (bio) { BIO_set_nbio(bio, 1); } auto cleanup = detail::scope_exit([&]() { // Restore blocking mode after handshake if (bio) { BIO_set_nbio(bio, 0); } detail::set_nonblocking(sock, false); }); auto res = 0; while ((res = SSL_connect(ssl)) != 1) { auto ssl_err = SSL_get_error(ssl, res); switch (ssl_err) { case SSL_ERROR_WANT_READ: if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { continue; } break; case SSL_ERROR_WANT_WRITE: if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { continue; } break; default: break; } if (err) { err->code = impl::map_ssl_error(ssl_err, err->sys_errno); err->backend_code = ERR_get_error(); } return false; } if (err) { err->code = ErrorCode::Success; } return true; } inline bool accept_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err) { if (!session) { if (err) { err->code = ErrorCode::Fatal; } return false; } auto ssl = static_cast(session); auto bio = SSL_get_rbio(ssl); // Set non-blocking mode for handshake detail::set_nonblocking(sock, true); if (bio) { BIO_set_nbio(bio, 1); } auto cleanup = detail::scope_exit([&]() { // Restore blocking mode after handshake if (bio) { BIO_set_nbio(bio, 0); } detail::set_nonblocking(sock, false); }); auto res = 0; while ((res = SSL_accept(ssl)) != 1) { auto ssl_err = SSL_get_error(ssl, res); switch (ssl_err) { case SSL_ERROR_WANT_READ: if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { continue; } break; case SSL_ERROR_WANT_WRITE: if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { continue; } break; default: break; } if (err) { err->code = impl::map_ssl_error(ssl_err, err->sys_errno); err->backend_code = ERR_get_error(); } return false; } if (err) { err->code = ErrorCode::Success; } return true; } inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) { if (!session || !buf) { err.code = ErrorCode::Fatal; return -1; } auto ssl = static_cast(session); constexpr auto max_len = static_cast((std::numeric_limits::max)()); if (len > max_len) { len = max_len; } auto ret = SSL_read(ssl, buf, static_cast(len)); if (ret > 0) { err.code = ErrorCode::Success; return ret; } auto ssl_err = SSL_get_error(ssl, ret); err.code = impl::map_ssl_error(ssl_err, err.sys_errno); if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); } return -1; } inline ssize_t write(session_t session, const void *buf, size_t len, TlsError &err) { if (!session || !buf) { err.code = ErrorCode::Fatal; return -1; } auto ssl = static_cast(session); auto ret = SSL_write(ssl, buf, static_cast(len)); if (ret > 0) { err.code = ErrorCode::Success; return ret; } auto ssl_err = SSL_get_error(ssl, ret); err.code = impl::map_ssl_error(ssl_err, err.sys_errno); if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); } return -1; } inline int pending(const_session_t session) { if (!session) return 0; return SSL_pending(static_cast(const_cast(session))); } inline void shutdown(session_t session, bool graceful) { if (!session) return; auto ssl = static_cast(session); if (graceful) { // First call sends close_notify if (SSL_shutdown(ssl) == 0) { // Second call waits for peer's close_notify SSL_shutdown(ssl); } } } inline bool is_peer_closed(session_t session, socket_t sock) { if (!session) return true; // Temporarily set socket to non-blocking to avoid blocking on SSL_peek detail::set_nonblocking(sock, true); auto se = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); auto ssl = static_cast(session); char buf; auto ret = SSL_peek(ssl, &buf, 1); if (ret > 0) return false; auto err = SSL_get_error(ssl, ret); return err == SSL_ERROR_ZERO_RETURN; } inline cert_t get_peer_cert(const_session_t session) { if (!session) return nullptr; return static_cast(SSL_get1_peer_certificate( static_cast(const_cast(session)))); } inline void free_cert(cert_t cert) { if (cert) { X509_free(static_cast(cert)); } } inline bool verify_hostname(cert_t cert, const char *hostname) { if (!cert || !hostname) return false; auto x509 = static_cast(cert); // Use X509_check_ip_asc for IP addresses, X509_check_host for DNS names if (detail::is_ip_address(hostname)) { return X509_check_ip_asc(x509, hostname, 0) == 1; } return X509_check_host(x509, hostname, strlen(hostname), 0, nullptr) == 1; } inline uint64_t hostname_mismatch_code() { return static_cast(X509_V_ERR_HOSTNAME_MISMATCH); } inline long get_verify_result(const_session_t session) { if (!session) return X509_V_ERR_UNSPECIFIED; return SSL_get_verify_result(static_cast(const_cast(session))); } inline std::string get_cert_subject_cn(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); auto subject_name = X509_get_subject_name(x509); if (!subject_name) return ""; char buf[256]; auto len = X509_NAME_get_text_by_NID(subject_name, NID_commonName, buf, sizeof(buf)); if (len < 0) return ""; return std::string(buf, static_cast(len)); } inline std::string get_cert_issuer_name(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); auto issuer_name = X509_get_issuer_name(x509); if (!issuer_name) return ""; char buf[256]; X509_NAME_oneline(issuer_name, buf, sizeof(buf)); return std::string(buf); } inline bool get_cert_sans(cert_t cert, std::vector &sans) { sans.clear(); if (!cert) return false; auto x509 = static_cast(cert); auto names = static_cast( X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr)); if (!names) return true; // No SANs is valid auto count = sk_GENERAL_NAME_num(names); for (int i = 0; i < count; i++) { auto gen = sk_GENERAL_NAME_value(names, i); if (!gen) continue; SanEntry entry; switch (gen->type) { case GEN_DNS: entry.type = SanType::DNS; if (gen->d.dNSName) { entry.value = std::string( reinterpret_cast( ASN1_STRING_get0_data(gen->d.dNSName)), static_cast(ASN1_STRING_length(gen->d.dNSName))); } break; case GEN_IPADD: entry.type = SanType::IP; if (gen->d.iPAddress) { auto data = ASN1_STRING_get0_data(gen->d.iPAddress); auto len = ASN1_STRING_length(gen->d.iPAddress); if (len == 4) { // IPv4 char buf[INET_ADDRSTRLEN]; inet_ntop(AF_INET, data, buf, sizeof(buf)); entry.value = buf; } else if (len == 16) { // IPv6 char buf[INET6_ADDRSTRLEN]; inet_ntop(AF_INET6, data, buf, sizeof(buf)); entry.value = buf; } } break; case GEN_EMAIL: entry.type = SanType::EMAIL; if (gen->d.rfc822Name) { entry.value = std::string( reinterpret_cast( ASN1_STRING_get0_data(gen->d.rfc822Name)), static_cast(ASN1_STRING_length(gen->d.rfc822Name))); } break; case GEN_URI: entry.type = SanType::URI; if (gen->d.uniformResourceIdentifier) { entry.value = std::string( reinterpret_cast( ASN1_STRING_get0_data(gen->d.uniformResourceIdentifier)), static_cast( ASN1_STRING_length(gen->d.uniformResourceIdentifier))); } break; default: entry.type = SanType::OTHER; break; } if (!entry.value.empty()) { sans.push_back(std::move(entry)); } } GENERAL_NAMES_free(names); return true; } inline bool get_cert_validity(cert_t cert, time_t ¬_before, time_t ¬_after) { if (!cert) return false; auto x509 = static_cast(cert); auto nb = X509_get0_notBefore(x509); auto na = X509_get0_notAfter(x509); if (!nb || !na) return false; ASN1_TIME *epoch = ASN1_TIME_new(); if (!epoch) return false; auto se = detail::scope_exit([&] { ASN1_TIME_free(epoch); }); if (!ASN1_TIME_set(epoch, 0)) return false; int pday, psec; if (!ASN1_TIME_diff(&pday, &psec, epoch, nb)) return false; not_before = 86400 * (time_t)pday + psec; if (!ASN1_TIME_diff(&pday, &psec, epoch, na)) return false; not_after = 86400 * (time_t)pday + psec; return true; } inline std::string get_cert_serial(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); auto serial = X509_get_serialNumber(x509); if (!serial) return ""; auto bn = ASN1_INTEGER_to_BN(serial, nullptr); if (!bn) return ""; auto hex = BN_bn2hex(bn); BN_free(bn); if (!hex) return ""; std::string result(hex); OPENSSL_free(hex); return result; } inline bool get_cert_der(cert_t cert, std::vector &der) { if (!cert) return false; auto x509 = static_cast(cert); auto len = i2d_X509(x509, nullptr); if (len < 0) return false; der.resize(static_cast(len)); auto p = der.data(); i2d_X509(x509, &p); return true; } inline const char *get_sni(const_session_t session) { if (!session) return nullptr; auto ssl = static_cast(const_cast(session)); return SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); } inline uint64_t peek_error() { return ERR_peek_last_error(); } inline uint64_t get_error() { return ERR_get_error(); } inline std::string error_string(uint64_t code) { char buf[256]; ERR_error_string_n(static_cast(code), buf, sizeof(buf)); return std::string(buf); } inline ca_store_t create_ca_store(const char *pem, size_t len) { auto mem = BIO_new_mem_buf(pem, static_cast(len)); if (!mem) { return nullptr; } auto mem_guard = detail::scope_exit([&] { BIO_free_all(mem); }); auto inf = PEM_X509_INFO_read_bio(mem, nullptr, nullptr, nullptr); if (!inf) { return nullptr; } auto store = X509_STORE_new(); if (store) { for (auto i = 0; i < static_cast(sk_X509_INFO_num(inf)); i++) { auto itmp = sk_X509_INFO_value(inf, i); if (!itmp) { continue; } if (itmp->x509) { X509_STORE_add_cert(store, itmp->x509); } if (itmp->crl) { X509_STORE_add_crl(store, itmp->crl); } } } sk_X509_INFO_pop_free(inf, X509_INFO_free); return static_cast(store); } inline void free_ca_store(ca_store_t store) { if (store) { X509_STORE_free(static_cast(store)); } } inline bool set_ca_store(ctx_t ctx, ca_store_t store) { if (!ctx || !store) { return false; } auto ssl_ctx = static_cast(ctx); auto x509_store = static_cast(store); // Check if same store is already set if (SSL_CTX_get_cert_store(ssl_ctx) == x509_store) { return true; } // SSL_CTX_set_cert_store takes ownership and frees the old store SSL_CTX_set_cert_store(ssl_ctx, x509_store); return true; } inline size_t get_ca_certs(ctx_t ctx, std::vector &certs) { certs.clear(); if (!ctx) { return 0; } auto ssl_ctx = static_cast(ctx); auto store = SSL_CTX_get_cert_store(ssl_ctx); if (!store) { return 0; } auto objs = X509_STORE_get0_objects(store); if (!objs) { return 0; } auto count = sk_X509_OBJECT_num(objs); for (decltype(count) i = 0; i < count; i++) { auto obj = sk_X509_OBJECT_value(objs, i); if (!obj) { continue; } if (X509_OBJECT_get_type(obj) == X509_LU_X509) { auto x509 = X509_OBJECT_get0_X509(obj); if (x509) { // Increment reference count so caller can free it X509_up_ref(x509); certs.push_back(static_cast(x509)); } } } return certs.size(); } inline std::vector get_ca_names(ctx_t ctx) { std::vector names; if (!ctx) { return names; } auto ssl_ctx = static_cast(ctx); auto store = SSL_CTX_get_cert_store(ssl_ctx); if (!store) { return names; } auto objs = X509_STORE_get0_objects(store); if (!objs) { return names; } auto count = sk_X509_OBJECT_num(objs); for (decltype(count) i = 0; i < count; i++) { auto obj = sk_X509_OBJECT_value(objs, i); if (!obj) { continue; } if (X509_OBJECT_get_type(obj) == X509_LU_X509) { auto x509 = X509_OBJECT_get0_X509(obj); if (x509) { auto subject = X509_get_subject_name(x509); if (subject) { char buf[512]; X509_NAME_oneline(subject, buf, sizeof(buf)); names.push_back(buf); } } } } return names; } inline bool update_server_cert(ctx_t ctx, const char *cert_pem, const char *key_pem, const char *password) { if (!ctx || !cert_pem || !key_pem) { return false; } auto ssl_ctx = static_cast(ctx); // Load certificate from PEM auto cert_bio = BIO_new_mem_buf(cert_pem, -1); if (!cert_bio) { return false; } auto cert = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr); BIO_free(cert_bio); if (!cert) { return false; } // Load private key from PEM auto key_bio = BIO_new_mem_buf(key_pem, -1); if (!key_bio) { X509_free(cert); return false; } auto key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, password ? const_cast(password) : nullptr); BIO_free(key_bio); if (!key) { X509_free(cert); return false; } // Update certificate and key auto ret = SSL_CTX_use_certificate(ssl_ctx, cert) == 1 && SSL_CTX_use_PrivateKey(ssl_ctx, key) == 1; X509_free(cert); EVP_PKEY_free(key); return ret; } inline bool update_server_client_ca(ctx_t ctx, const char *ca_pem) { if (!ctx || !ca_pem) { return false; } auto ssl_ctx = static_cast(ctx); // Create new X509_STORE from PEM auto store = create_ca_store(ca_pem, strlen(ca_pem)); if (!store) { return false; } // SSL_CTX_set_cert_store takes ownership SSL_CTX_set_cert_store(ssl_ctx, static_cast(store)); // Set client CA list for client certificate request auto ca_list = impl::create_client_ca_list_from_pem(ca_pem); if (ca_list) { // SSL_CTX_set_client_CA_list takes ownership of ca_list SSL_CTX_set_client_CA_list(ssl_ctx, ca_list); } return true; } inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { if (!ctx) { return false; } auto ssl_ctx = static_cast(ctx); impl::get_verify_callback() = std::move(callback); if (impl::get_verify_callback()) { SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, impl::openssl_verify_callback); } else { SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, nullptr); } return true; } inline long get_verify_error(const_session_t session) { if (!session) { return -1; } auto ssl = static_cast(const_cast(session)); return SSL_get_verify_result(ssl); } inline std::string verify_error_string(long error_code) { if (error_code == X509_V_OK) { return ""; } const char *str = X509_verify_cert_error_string(static_cast(error_code)); return str ? str : "unknown error"; } namespace impl { // OpenSSL-specific helpers for public API wrappers inline ctx_t create_server_context_from_x509(X509 *cert, EVP_PKEY *key, X509_STORE *client_ca_store, int &out_error) { out_error = 0; auto cert_pem = x509_to_pem(cert); auto key_pem = evp_pkey_to_pem(key); if (cert_pem.empty() || key_pem.empty()) { out_error = static_cast(ERR_get_error()); return nullptr; } auto ctx = create_server_context(); if (!ctx) { out_error = static_cast(get_error()); return nullptr; } if (!set_server_cert_pem(ctx, cert_pem.c_str(), key_pem.c_str(), nullptr)) { out_error = static_cast(get_error()); free_context(ctx); return nullptr; } if (client_ca_store) { // Set cert store for verification (SSL_CTX_set_cert_store takes ownership) SSL_CTX_set_cert_store(static_cast(ctx), client_ca_store); // Extract and set client CA list directly from store (more efficient than // PEM conversion) auto ca_list = extract_client_ca_list_from_store(client_ca_store); if (ca_list) { SSL_CTX_set_client_CA_list(static_cast(ctx), ca_list); } set_verify_client(ctx, true); } return ctx; } inline void update_server_certs_from_x509(ctx_t ctx, X509 *cert, EVP_PKEY *key, X509_STORE *client_ca_store) { auto cert_pem = x509_to_pem(cert); auto key_pem = evp_pkey_to_pem(key); if (!cert_pem.empty() && !key_pem.empty()) { update_server_cert(ctx, cert_pem.c_str(), key_pem.c_str(), nullptr); } if (client_ca_store) { auto ca_pem = x509_store_to_pem(client_ca_store); if (!ca_pem.empty()) { update_server_client_ca(ctx, ca_pem.c_str()); } X509_STORE_free(client_ca_store); } } inline ctx_t create_client_context_from_x509(X509 *cert, EVP_PKEY *key, const char *password, unsigned long &out_error) { out_error = 0; auto ctx = create_client_context(); if (!ctx) { out_error = static_cast(get_error()); return nullptr; } if (cert && key) { auto cert_pem = x509_to_pem(cert); auto key_pem = evp_pkey_to_pem(key); if (cert_pem.empty() || key_pem.empty()) { out_error = ERR_get_error(); free_context(ctx); return nullptr; } if (!set_client_cert_pem(ctx, cert_pem.c_str(), key_pem.c_str(), password)) { out_error = static_cast(get_error()); free_context(ctx); return nullptr; } } return ctx; } } // namespace impl } // namespace tls // ClientImpl::set_ca_cert_store - defined here to use // tls::impl::x509_store_to_pem Deprecated: converts X509_STORE to PEM and // stores for redirect transfer inline void ClientImpl::set_ca_cert_store(X509_STORE *ca_cert_store) { if (ca_cert_store) { ca_cert_pem_ = tls::impl::x509_store_to_pem(ca_cert_store); } } inline SSLServer::SSLServer(X509 *cert, EVP_PKEY *private_key, X509_STORE *client_ca_cert_store) { ctx_ = tls::impl::create_server_context_from_x509( cert, private_key, client_ca_cert_store, last_ssl_error_); } inline SSLServer::SSLServer( const std::function &setup_ssl_ctx_callback) { // Use abstract API to create context ctx_ = tls::create_server_context(); if (ctx_) { // Pass to OpenSSL-specific callback (ctx_ is SSL_CTX* internally) auto ssl_ctx = static_cast(ctx_); if (!setup_ssl_ctx_callback(*ssl_ctx)) { tls::free_context(ctx_); ctx_ = nullptr; } } } inline SSL_CTX *SSLServer::ssl_context() const { return static_cast(ctx_); } inline void SSLServer::update_certs(X509 *cert, EVP_PKEY *private_key, X509_STORE *client_ca_cert_store) { std::lock_guard guard(ctx_mutex_); tls::impl::update_server_certs_from_x509(ctx_, cert, private_key, client_ca_cert_store); } inline SSLClient::SSLClient(const std::string &host, int port, X509 *client_cert, EVP_PKEY *client_key, const std::string &private_key_password) : ClientImpl(host, port) { const char *password = private_key_password.empty() ? nullptr : private_key_password.c_str(); ctx_ = tls::impl::create_client_context_from_x509( client_cert, client_key, password, last_backend_error_); } inline long SSLClient::get_verify_result() const { return verify_result_; } inline void SSLClient::set_server_certificate_verifier( std::function verifier) { // Wrap SSL* callback into backend-independent session_verifier_ auto v = std::make_shared>( std::move(verifier)); session_verifier_ = [v](tls::session_t session) { return (*v)(static_cast(session)); }; } inline SSL_CTX *SSLClient::ssl_context() const { return static_cast(ctx_); } inline bool SSLClient::verify_host(X509 *server_cert) const { /* Quote from RFC2818 section 3.1 "Server Identity" If a subjectAltName extension of type dNSName is present, that MUST be used as the identity. Otherwise, the (most specific) Common Name field in the Subject field of the certificate MUST be used. Although the use of the Common Name is existing practice, it is deprecated and Certification Authorities are encouraged to use the dNSName instead. Matching is performed using the matching rules specified by [RFC2459]. If more than one identity of a given type is present in the certificate (e.g., more than one dNSName name, a match in any one of the set is considered acceptable.) Names may contain the wildcard character * which is considered to match any single domain name component or component fragment. E.g., *.a.com matches foo.a.com but not bar.foo.a.com. f*.com matches foo.com but not bar.com. In some cases, the URI is specified as an IP address rather than a hostname. In this case, the iPAddress subjectAltName must be present in the certificate and must exactly match the IP in the URI. */ return verify_host_with_subject_alt_name(server_cert) || verify_host_with_common_name(server_cert); } inline bool SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const { auto ret = false; auto type = GEN_DNS; struct in6_addr addr6 = {}; struct in_addr addr = {}; size_t addr_len = 0; #ifndef __MINGW32__ if (inet_pton(AF_INET6, host_.c_str(), &addr6)) { type = GEN_IPADD; addr_len = sizeof(struct in6_addr); } else if (inet_pton(AF_INET, host_.c_str(), &addr)) { type = GEN_IPADD; addr_len = sizeof(struct in_addr); } #endif auto alt_names = static_cast( X509_get_ext_d2i(server_cert, NID_subject_alt_name, nullptr, nullptr)); if (alt_names) { auto dsn_matched = false; auto ip_matched = false; auto count = sk_GENERAL_NAME_num(alt_names); for (decltype(count) i = 0; i < count && !dsn_matched; i++) { auto val = sk_GENERAL_NAME_value(alt_names, i); if (!val || val->type != type) { continue; } auto name = reinterpret_cast(ASN1_STRING_get0_data(val->d.ia5)); if (name == nullptr) { continue; } auto name_len = static_cast(ASN1_STRING_length(val->d.ia5)); switch (type) { case GEN_DNS: dsn_matched = detail::match_hostname(std::string(name, name_len), host_); break; case GEN_IPADD: if (!memcmp(&addr6, name, addr_len) || !memcmp(&addr, name, addr_len)) { ip_matched = true; } break; } } if (dsn_matched || ip_matched) { ret = true; } } GENERAL_NAMES_free(const_cast( reinterpret_cast(alt_names))); return ret; } inline bool SSLClient::verify_host_with_common_name(X509 *server_cert) const { const auto subject_name = X509_get_subject_name(server_cert); if (subject_name != nullptr) { char name[BUFSIZ]; auto name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName, name, sizeof(name)); if (name_len != -1) { return detail::match_hostname( std::string(name, static_cast(name_len)), host_); } } return false; } #endif // CPPHTTPLIB_OPENSSL_SUPPORT /* * Group 9: TLS abstraction layer - Mbed TLS backend */ /* * Mbed TLS Backend Implementation */ #ifdef CPPHTTPLIB_MBEDTLS_SUPPORT namespace tls { namespace impl { // Mbed TLS session wrapper struct MbedTlsSession { mbedtls_ssl_context ssl; socket_t sock = INVALID_SOCKET; std::string hostname; // For client: set via set_sni std::string sni_hostname; // For server: received from client via SNI callback MbedTlsSession() { mbedtls_ssl_init(&ssl); } ~MbedTlsSession() { mbedtls_ssl_free(&ssl); } MbedTlsSession(const MbedTlsSession &) = delete; MbedTlsSession &operator=(const MbedTlsSession &) = delete; }; // Thread-local error code accessor for Mbed TLS (since it doesn't have an error // queue) inline int &mbedtls_last_error() { static thread_local int err = 0; return err; } // Helper to map Mbed TLS error to ErrorCode inline ErrorCode map_mbedtls_error(int ret, int &out_errno) { if (ret == 0) { return ErrorCode::Success; } if (ret == MBEDTLS_ERR_SSL_WANT_READ) { return ErrorCode::WantRead; } if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { return ErrorCode::WantWrite; } if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { return ErrorCode::PeerClosed; } if (ret == MBEDTLS_ERR_NET_CONN_RESET || ret == MBEDTLS_ERR_NET_SEND_FAILED || ret == MBEDTLS_ERR_NET_RECV_FAILED) { out_errno = errno; return ErrorCode::SyscallError; } if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) { return ErrorCode::CertVerifyFailed; } return ErrorCode::Fatal; } // BIO-like send callback for Mbed TLS inline int mbedtls_net_send_cb(void *ctx, const unsigned char *buf, size_t len) { auto sock = *static_cast(ctx); #ifdef _WIN32 auto ret = send(sock, reinterpret_cast(buf), static_cast(len), 0); if (ret == SOCKET_ERROR) { int err = WSAGetLastError(); if (err == WSAEWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_WRITE; } return MBEDTLS_ERR_NET_SEND_FAILED; } #else auto ret = send(sock, buf, len, 0); if (ret < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_WRITE; } return MBEDTLS_ERR_NET_SEND_FAILED; } #endif return static_cast(ret); } // BIO-like recv callback for Mbed TLS inline int mbedtls_net_recv_cb(void *ctx, unsigned char *buf, size_t len) { auto sock = *static_cast(ctx); #ifdef _WIN32 auto ret = recv(sock, reinterpret_cast(buf), static_cast(len), 0); if (ret == SOCKET_ERROR) { int err = WSAGetLastError(); if (err == WSAEWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_READ; } return MBEDTLS_ERR_NET_RECV_FAILED; } #else auto ret = recv(sock, buf, len, 0); if (ret < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_READ; } return MBEDTLS_ERR_NET_RECV_FAILED; } #endif if (ret == 0) { return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; } return static_cast(ret); } // MbedTlsContext constructor/destructor implementations inline MbedTlsContext::MbedTlsContext() { mbedtls_ssl_config_init(&conf); mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_init(&ctr_drbg); mbedtls_x509_crt_init(&ca_chain); mbedtls_x509_crt_init(&own_cert); mbedtls_pk_init(&own_key); } inline MbedTlsContext::~MbedTlsContext() { mbedtls_pk_free(&own_key); mbedtls_x509_crt_free(&own_cert); mbedtls_x509_crt_free(&ca_chain); mbedtls_ctr_drbg_free(&ctr_drbg); mbedtls_entropy_free(&entropy); mbedtls_ssl_config_free(&conf); } // Thread-local storage for SNI captured during handshake // This is needed because the SNI callback doesn't have a way to pass // session-specific data before the session is fully set up inline std::string &mbedpending_sni() { static thread_local std::string sni; return sni; } // SNI callback for Mbed TLS server to capture client's SNI hostname inline int mbedtls_sni_callback(void *p_ctx, mbedtls_ssl_context *ssl, const unsigned char *name, size_t name_len) { (void)p_ctx; (void)ssl; // Store SNI name in thread-local storage // It will be retrieved and stored in the session after handshake if (name && name_len > 0) { mbedpending_sni().assign(reinterpret_cast(name), name_len); } else { mbedpending_sni().clear(); } return 0; // Accept any SNI } inline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt, int cert_depth, uint32_t *flags); // Check if a string is an IPv4 address inline bool is_ipv4_address(const std::string &str) { int dots = 0; for (char c : str) { if (c == '.') { dots++; } else if (!isdigit(static_cast(c))) { return false; } } return dots == 3; } // Parse IPv4 address string to bytes inline bool parse_ipv4(const std::string &str, unsigned char *out) { int parts[4]; if (sscanf(str.c_str(), "%d.%d.%d.%d", &parts[0], &parts[1], &parts[2], &parts[3]) != 4) { return false; } for (int i = 0; i < 4; i++) { if (parts[i] < 0 || parts[i] > 255) return false; out[i] = static_cast(parts[i]); } return true; } // MbedTLS verify callback wrapper inline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt, int cert_depth, uint32_t *flags) { auto &callback = get_verify_callback(); if (!callback) { return 0; } // Continue with default verification // data points to the MbedTlsSession auto *session = static_cast(data); // Build context VerifyContext verify_ctx; verify_ctx.session = static_cast(session); verify_ctx.cert = static_cast(crt); verify_ctx.depth = cert_depth; verify_ctx.preverify_ok = (*flags == 0); verify_ctx.error_code = static_cast(*flags); // Convert Mbed TLS flags to error string static thread_local char error_buf[256]; if (*flags != 0) { mbedtls_x509_crt_verify_info(error_buf, sizeof(error_buf), "", *flags); verify_ctx.error_string = error_buf; } else { verify_ctx.error_string = nullptr; } bool accepted = callback(verify_ctx); if (accepted) { *flags = 0; // Clear all error flags return 0; } return MBEDTLS_ERR_X509_CERT_VERIFY_FAILED; } } // namespace impl inline ctx_t create_client_context() { auto ctx = new (std::nothrow) impl::MbedTlsContext(); if (!ctx) { return nullptr; } ctx->is_server = false; // Seed the random number generator const char *pers = "httplib_client"; int ret = mbedtls_ctr_drbg_seed( &ctx->ctr_drbg, mbedtls_entropy_func, &ctx->entropy, reinterpret_cast(pers), strlen(pers)); if (ret != 0) { impl::mbedtls_last_error() = ret; delete ctx; return nullptr; } // Set up SSL config for client ret = mbedtls_ssl_config_defaults(&ctx->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); if (ret != 0) { impl::mbedtls_last_error() = ret; delete ctx; return nullptr; } // Set random number generator mbedtls_ssl_conf_rng(&ctx->conf, mbedtls_ctr_drbg_random, &ctx->ctr_drbg); // Default: verify peer certificate mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED); // Set minimum TLS version to 1.2 #ifdef CPPHTTPLIB_MBEDTLS_V3 mbedtls_ssl_conf_min_tls_version(&ctx->conf, MBEDTLS_SSL_VERSION_TLS1_2); #else mbedtls_ssl_conf_min_version(&ctx->conf, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3); #endif return static_cast(ctx); } inline ctx_t create_server_context() { auto ctx = new (std::nothrow) impl::MbedTlsContext(); if (!ctx) { return nullptr; } ctx->is_server = true; // Seed the random number generator const char *pers = "httplib_server"; int ret = mbedtls_ctr_drbg_seed( &ctx->ctr_drbg, mbedtls_entropy_func, &ctx->entropy, reinterpret_cast(pers), strlen(pers)); if (ret != 0) { impl::mbedtls_last_error() = ret; delete ctx; return nullptr; } // Set up SSL config for server ret = mbedtls_ssl_config_defaults(&ctx->conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); if (ret != 0) { impl::mbedtls_last_error() = ret; delete ctx; return nullptr; } // Set random number generator mbedtls_ssl_conf_rng(&ctx->conf, mbedtls_ctr_drbg_random, &ctx->ctr_drbg); // Default: don't verify client mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_NONE); // Set minimum TLS version to 1.2 #ifdef CPPHTTPLIB_MBEDTLS_V3 mbedtls_ssl_conf_min_tls_version(&ctx->conf, MBEDTLS_SSL_VERSION_TLS1_2); #else mbedtls_ssl_conf_min_version(&ctx->conf, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3); #endif // Set SNI callback to capture client's SNI hostname mbedtls_ssl_conf_sni(&ctx->conf, impl::mbedtls_sni_callback, nullptr); return static_cast(ctx); } inline void free_context(ctx_t ctx) { if (ctx) { delete static_cast(ctx); } } inline bool set_min_version(ctx_t ctx, Version version) { if (!ctx) { return false; } auto mctx = static_cast(ctx); #ifdef CPPHTTPLIB_MBEDTLS_V3 // Mbed TLS 3.x uses mbedtls_ssl_protocol_version enum mbedtls_ssl_protocol_version min_ver = MBEDTLS_SSL_VERSION_TLS1_2; if (version >= Version::TLS1_3) { #if defined(MBEDTLS_SSL_PROTO_TLS1_3) min_ver = MBEDTLS_SSL_VERSION_TLS1_3; #endif } mbedtls_ssl_conf_min_tls_version(&mctx->conf, min_ver); #else // Mbed TLS 2.x uses major/minor version numbers int major = MBEDTLS_SSL_MAJOR_VERSION_3; int minor = MBEDTLS_SSL_MINOR_VERSION_3; // TLS 1.2 if (version >= Version::TLS1_3) { #if defined(MBEDTLS_SSL_PROTO_TLS1_3) minor = MBEDTLS_SSL_MINOR_VERSION_4; // TLS 1.3 #else minor = MBEDTLS_SSL_MINOR_VERSION_3; // Fall back to TLS 1.2 #endif } mbedtls_ssl_conf_min_version(&mctx->conf, major, minor); #endif return true; } inline bool load_ca_pem(ctx_t ctx, const char *pem, size_t len) { if (!ctx || !pem) { return false; } auto mctx = static_cast(ctx); // mbedtls_x509_crt_parse expects null-terminated string for PEM // Add null terminator if not present std::string pem_str(pem, len); int ret = mbedtls_x509_crt_parse( &mctx->ca_chain, reinterpret_cast(pem_str.c_str()), pem_str.size() + 1); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); return true; } inline bool load_ca_file(ctx_t ctx, const char *file_path) { if (!ctx || !file_path) { return false; } auto mctx = static_cast(ctx); int ret = mbedtls_x509_crt_parse_file(&mctx->ca_chain, file_path); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); return true; } inline bool load_ca_dir(ctx_t ctx, const char *dir_path) { if (!ctx || !dir_path) { return false; } auto mctx = static_cast(ctx); int ret = mbedtls_x509_crt_parse_path(&mctx->ca_chain, dir_path); if (ret < 0) { // Returns number of certs on success, negative on error impl::mbedtls_last_error() = ret; return false; } mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); return true; } inline bool load_system_certs(ctx_t ctx) { if (!ctx) { return false; } auto mctx = static_cast(ctx); bool loaded = false; #ifdef _WIN32 // Load from Windows certificate store (ROOT and CA) static const wchar_t *store_names[] = {L"ROOT", L"CA"}; for (auto store_name : store_names) { HCERTSTORE hStore = CertOpenSystemStoreW(0, store_name); if (hStore) { PCCERT_CONTEXT pContext = nullptr; while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) != nullptr) { int ret = mbedtls_x509_crt_parse_der( &mctx->ca_chain, pContext->pbCertEncoded, pContext->cbCertEncoded); if (ret == 0) { loaded = true; } } CertCloseStore(hStore, 0); } } #elif defined(__APPLE__) && defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) // Load from macOS Keychain CFArrayRef certs = nullptr; OSStatus status = SecTrustCopyAnchorCertificates(&certs); if (status == errSecSuccess && certs) { CFIndex count = CFArrayGetCount(certs); for (CFIndex i = 0; i < count; i++) { SecCertificateRef cert = (SecCertificateRef)CFArrayGetValueAtIndex(certs, i); CFDataRef data = SecCertificateCopyData(cert); if (data) { int ret = mbedtls_x509_crt_parse_der( &mctx->ca_chain, CFDataGetBytePtr(data), static_cast(CFDataGetLength(data))); if (ret == 0) { loaded = true; } CFRelease(data); } } CFRelease(certs); } #else // Try common CA certificate locations on Linux/Unix static const char *ca_paths[] = { "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu "/etc/pki/tls/certs/ca-bundle.crt", // RHEL/CentOS "/etc/ssl/ca-bundle.pem", // OpenSUSE "/etc/pki/tls/cacert.pem", // OpenELEC "/etc/ssl/cert.pem", // Alpine, FreeBSD nullptr}; for (const char **path = ca_paths; *path; ++path) { int ret = mbedtls_x509_crt_parse_file(&mctx->ca_chain, *path); if (ret >= 0) { loaded = true; break; } } // Also try the CA directory if (!loaded) { static const char *ca_dirs[] = {"/etc/ssl/certs", // Debian/Ubuntu "/etc/pki/tls/certs", // RHEL/CentOS "/usr/share/ca-certificates", nullptr}; for (const char **dir = ca_dirs; *dir; ++dir) { int ret = mbedtls_x509_crt_parse_path(&mctx->ca_chain, *dir); if (ret >= 0) { loaded = true; break; } } } #endif if (loaded) { mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); } return loaded; } inline bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key, const char *password) { if (!ctx || !cert || !key) { return false; } auto mctx = static_cast(ctx); // Parse certificate std::string cert_str(cert); int ret = mbedtls_x509_crt_parse( &mctx->own_cert, reinterpret_cast(cert_str.c_str()), cert_str.size() + 1); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } // Parse private key std::string key_str(key); const unsigned char *pwd = password ? reinterpret_cast(password) : nullptr; size_t pwd_len = password ? strlen(password) : 0; #ifdef CPPHTTPLIB_MBEDTLS_V3 ret = mbedtls_pk_parse_key( &mctx->own_key, reinterpret_cast(key_str.c_str()), key_str.size() + 1, pwd, pwd_len, mbedtls_ctr_drbg_random, &mctx->ctr_drbg); #else ret = mbedtls_pk_parse_key( &mctx->own_key, reinterpret_cast(key_str.c_str()), key_str.size() + 1, pwd, pwd_len); #endif if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } ret = mbedtls_ssl_conf_own_cert(&mctx->conf, &mctx->own_cert, &mctx->own_key); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } return true; } inline bool set_client_cert_file(ctx_t ctx, const char *cert_path, const char *key_path, const char *password) { if (!ctx || !cert_path || !key_path) { return false; } auto mctx = static_cast(ctx); // Parse certificate file int ret = mbedtls_x509_crt_parse_file(&mctx->own_cert, cert_path); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } // Parse private key file #ifdef CPPHTTPLIB_MBEDTLS_V3 ret = mbedtls_pk_parse_keyfile(&mctx->own_key, key_path, password, mbedtls_ctr_drbg_random, &mctx->ctr_drbg); #else ret = mbedtls_pk_parse_keyfile(&mctx->own_key, key_path, password); #endif if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } ret = mbedtls_ssl_conf_own_cert(&mctx->conf, &mctx->own_cert, &mctx->own_key); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } return true; } inline void set_verify_client(ctx_t ctx, bool require) { if (!ctx) { return; } auto mctx = static_cast(ctx); mctx->verify_client = require; if (require) { mbedtls_ssl_conf_authmode(&mctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED); } else { // If a verify callback is set, use OPTIONAL mode to ensure the callback // is called (matching OpenSSL behavior). Otherwise use NONE. mbedtls_ssl_conf_authmode(&mctx->conf, mctx->has_verify_callback ? MBEDTLS_SSL_VERIFY_OPTIONAL : MBEDTLS_SSL_VERIFY_NONE); } } inline session_t create_session(ctx_t ctx, socket_t sock) { if (!ctx || sock == INVALID_SOCKET) { return nullptr; } auto mctx = static_cast(ctx); auto session = new (std::nothrow) impl::MbedTlsSession(); if (!session) { return nullptr; } session->sock = sock; int ret = mbedtls_ssl_setup(&session->ssl, &mctx->conf); if (ret != 0) { impl::mbedtls_last_error() = ret; delete session; return nullptr; } // Set BIO callbacks mbedtls_ssl_set_bio(&session->ssl, &session->sock, impl::mbedtls_net_send_cb, impl::mbedtls_net_recv_cb, nullptr); // Set per-session verify callback with session pointer if callback is // registered if (mctx->has_verify_callback) { mbedtls_ssl_set_verify(&session->ssl, impl::mbedtls_verify_callback, session); } return static_cast(session); } inline void free_session(session_t session) { if (session) { delete static_cast(session); } } inline bool set_sni(session_t session, const char *hostname) { if (!session || !hostname) { return false; } auto msession = static_cast(session); int ret = mbedtls_ssl_set_hostname(&msession->ssl, hostname); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } msession->hostname = hostname; return true; } inline bool set_hostname(session_t session, const char *hostname) { // In Mbed TLS, set_hostname also sets up hostname verification return set_sni(session, hostname); } inline TlsError connect(session_t session) { TlsError err; if (!session) { err.code = ErrorCode::Fatal; return err; } auto msession = static_cast(session); int ret = mbedtls_ssl_handshake(&msession->ssl); if (ret == 0) { err.code = ErrorCode::Success; } else { err.code = impl::map_mbedtls_error(ret, err.sys_errno); err.backend_code = static_cast(-ret); impl::mbedtls_last_error() = ret; } return err; } inline TlsError accept(session_t session) { // Same as connect for Mbed TLS - handshake works for both client and server auto result = connect(session); // After successful handshake, capture SNI from thread-local storage if (result.code == ErrorCode::Success && session) { auto msession = static_cast(session); msession->sni_hostname = std::move(impl::mbedpending_sni()); impl::mbedpending_sni().clear(); } return result; } inline bool connect_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err) { if (!session) { if (err) { err->code = ErrorCode::Fatal; } return false; } auto msession = static_cast(session); // Set socket to non-blocking mode detail::set_nonblocking(sock, true); auto cleanup = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); int ret; while ((ret = mbedtls_ssl_handshake(&msession->ssl)) != 0) { if (ret == MBEDTLS_ERR_SSL_WANT_READ) { if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { continue; } } else if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { continue; } } // TlsError or timeout if (err) { err->code = impl::map_mbedtls_error(ret, err->sys_errno); err->backend_code = static_cast(-ret); } impl::mbedtls_last_error() = ret; return false; } if (err) { err->code = ErrorCode::Success; } return true; } inline bool accept_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err) { // Same implementation as connect for Mbed TLS bool result = connect_nonblocking(session, sock, timeout_sec, timeout_usec, err); // After successful handshake, capture SNI from thread-local storage if (result && session) { auto msession = static_cast(session); msession->sni_hostname = std::move(impl::mbedpending_sni()); impl::mbedpending_sni().clear(); } return result; } inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) { if (!session || !buf) { err.code = ErrorCode::Fatal; return -1; } auto msession = static_cast(session); int ret = mbedtls_ssl_read(&msession->ssl, static_cast(buf), len); if (ret > 0) { err.code = ErrorCode::Success; return static_cast(ret); } if (ret == 0) { err.code = ErrorCode::PeerClosed; return 0; } err.code = impl::map_mbedtls_error(ret, err.sys_errno); err.backend_code = static_cast(-ret); impl::mbedtls_last_error() = ret; return -1; } inline ssize_t write(session_t session, const void *buf, size_t len, TlsError &err) { if (!session || !buf) { err.code = ErrorCode::Fatal; return -1; } auto msession = static_cast(session); int ret = mbedtls_ssl_write(&msession->ssl, static_cast(buf), len); if (ret > 0) { err.code = ErrorCode::Success; return static_cast(ret); } if (ret == 0) { err.code = ErrorCode::PeerClosed; return 0; } err.code = impl::map_mbedtls_error(ret, err.sys_errno); err.backend_code = static_cast(-ret); impl::mbedtls_last_error() = ret; return -1; } inline int pending(const_session_t session) { if (!session) { return 0; } auto msession = static_cast(const_cast(session)); return static_cast(mbedtls_ssl_get_bytes_avail(&msession->ssl)); } inline void shutdown(session_t session, bool graceful) { if (!session) { return; } auto msession = static_cast(session); if (graceful) { // Try to send close_notify, but don't block forever int ret; int attempts = 0; while ((ret = mbedtls_ssl_close_notify(&msession->ssl)) != 0 && attempts < 3) { if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { break; } attempts++; } } } inline bool is_peer_closed(session_t session, socket_t sock) { if (!session || sock == INVALID_SOCKET) { return true; } auto msession = static_cast(session); // Check if there's already decrypted data available in the TLS buffer // If so, the connection is definitely alive if (mbedtls_ssl_get_bytes_avail(&msession->ssl) > 0) { return false; } // Set socket to non-blocking to avoid blocking on read detail::set_nonblocking(sock, true); auto cleanup = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); // Try a 1-byte read to check connection status // Note: This will consume the byte if data is available, but for the // purpose of checking if peer is closed, this should be acceptable // since we're only called when we expect the connection might be closing unsigned char buf; int ret = mbedtls_ssl_read(&msession->ssl, &buf, 1); // If we got data or WANT_READ (would block), connection is alive if (ret > 0 || ret == MBEDTLS_ERR_SSL_WANT_READ) { return false; } // If we get a peer close notify or a connection reset, the peer is closed return ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || ret == MBEDTLS_ERR_NET_CONN_RESET || ret == 0; } inline cert_t get_peer_cert(const_session_t session) { if (!session) { return nullptr; } auto msession = static_cast(const_cast(session)); // Mbed TLS returns a pointer to the internal peer cert chain. // WARNING: This pointer is only valid while the session is active. // Do not use the certificate after calling free_session(). const mbedtls_x509_crt *cert = mbedtls_ssl_get_peer_cert(&msession->ssl); return const_cast(cert); } inline void free_cert(cert_t cert) { // Mbed TLS: peer certificate is owned by the SSL context. // No-op here, but callers should still call this for cross-backend // portability. (void)cert; } inline bool verify_hostname(cert_t cert, const char *hostname) { if (!cert || !hostname) { return false; } auto mcert = static_cast(cert); std::string host_str(hostname); // Check if hostname is an IP address bool is_ip = impl::is_ipv4_address(host_str); unsigned char ip_bytes[4]; if (is_ip) { impl::parse_ipv4(host_str, ip_bytes); } // Check Subject Alternative Names (SAN) // In Mbed TLS 3.x, subject_alt_names contains raw values without ASN.1 tags // - DNS names: raw string bytes // - IP addresses: raw IP bytes (4 for IPv4, 16 for IPv6) const mbedtls_x509_sequence *san = &mcert->subject_alt_names; while (san != nullptr && san->buf.p != nullptr && san->buf.len > 0) { const unsigned char *p = san->buf.p; size_t len = san->buf.len; if (is_ip) { // Check if this SAN is an IPv4 address (4 bytes) if (len == 4 && memcmp(p, ip_bytes, 4) == 0) { return true; } // Check if this SAN is an IPv6 address (16 bytes) - skip for now } else { // Check if this SAN is a DNS name (printable ASCII string) bool is_dns = len > 0; for (size_t i = 0; i < len && is_dns; i++) { if (p[i] < 32 || p[i] > 126) { is_dns = false; } } if (is_dns) { std::string san_name(reinterpret_cast(p), len); if (detail::match_hostname(san_name, host_str)) { return true; } } } san = san->next; } // Fallback: Check Common Name (CN) in subject char cn[256]; int ret = mbedtls_x509_dn_gets(cn, sizeof(cn), &mcert->subject); if (ret > 0) { std::string cn_str(cn); // Look for "CN=" in the DN string size_t cn_pos = cn_str.find("CN="); if (cn_pos != std::string::npos) { size_t start = cn_pos + 3; size_t end = cn_str.find(',', start); std::string cn_value = cn_str.substr(start, end == std::string::npos ? end : end - start); if (detail::match_hostname(cn_value, host_str)) { return true; } } } return false; } inline uint64_t hostname_mismatch_code() { return static_cast(MBEDTLS_X509_BADCERT_CN_MISMATCH); } inline long get_verify_result(const_session_t session) { if (!session) { return -1; } auto msession = static_cast(const_cast(session)); uint32_t flags = mbedtls_ssl_get_verify_result(&msession->ssl); // Return 0 (X509_V_OK equivalent) if verification passed return flags == 0 ? 0 : static_cast(flags); } inline std::string get_cert_subject_cn(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); // Find the CN in the subject const mbedtls_x509_name *name = &x509->subject; while (name != nullptr) { if (MBEDTLS_OID_CMP(MBEDTLS_OID_AT_CN, &name->oid) == 0) { return std::string(reinterpret_cast(name->val.p), name->val.len); } name = name->next; } return ""; } inline std::string get_cert_issuer_name(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); // Build a human-readable issuer name string char buf[512]; int ret = mbedtls_x509_dn_gets(buf, sizeof(buf), &x509->issuer); if (ret < 0) return ""; return std::string(buf); } inline bool get_cert_sans(cert_t cert, std::vector &sans) { sans.clear(); if (!cert) return false; auto x509 = static_cast(cert); // Parse the Subject Alternative Name extension const mbedtls_x509_sequence *cur = &x509->subject_alt_names; while (cur != nullptr) { if (cur->buf.len > 0) { // Mbed TLS stores SAN as ASN.1 sequences // The tag byte indicates the type const unsigned char *p = cur->buf.p; size_t len = cur->buf.len; // First byte is the tag unsigned char tag = *p; p++; len--; // Parse length (simple single-byte length assumed) if (len > 0 && *p < 0x80) { size_t value_len = *p; p++; len--; if (value_len <= len) { SanEntry entry; // ASN.1 context tags for GeneralName switch (tag & 0x1F) { case 2: // dNSName entry.type = SanType::DNS; entry.value = std::string(reinterpret_cast(p), value_len); break; case 7: // iPAddress entry.type = SanType::IP; if (value_len == 4) { // IPv4 char buf[16]; snprintf(buf, sizeof(buf), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]); entry.value = buf; } else if (value_len == 16) { // IPv6 char buf[64]; snprintf(buf, sizeof(buf), "%02x%02x:%02x%02x:%02x%02x:%02x%02x:" "%02x%02x:%02x%02x:%02x%02x:%02x%02x", p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9], p[10], p[11], p[12], p[13], p[14], p[15]); entry.value = buf; } break; case 1: // rfc822Name (email) entry.type = SanType::EMAIL; entry.value = std::string(reinterpret_cast(p), value_len); break; case 6: // uniformResourceIdentifier entry.type = SanType::URI; entry.value = std::string(reinterpret_cast(p), value_len); break; default: entry.type = SanType::OTHER; break; } if (!entry.value.empty()) { sans.push_back(std::move(entry)); } } } } cur = cur->next; } return true; } inline bool get_cert_validity(cert_t cert, time_t ¬_before, time_t ¬_after) { if (!cert) return false; auto x509 = static_cast(cert); // Convert mbedtls_x509_time to time_t auto to_time_t = [](const mbedtls_x509_time &t) -> time_t { struct tm tm_time = {}; tm_time.tm_year = t.year - 1900; tm_time.tm_mon = t.mon - 1; tm_time.tm_mday = t.day; tm_time.tm_hour = t.hour; tm_time.tm_min = t.min; tm_time.tm_sec = t.sec; #ifdef _WIN32 return _mkgmtime(&tm_time); #else return timegm(&tm_time); #endif }; not_before = to_time_t(x509->valid_from); not_after = to_time_t(x509->valid_to); return true; } inline std::string get_cert_serial(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); // Convert serial number to hex string std::string result; result.reserve(x509->serial.len * 2); for (size_t i = 0; i < x509->serial.len; i++) { char hex[3]; snprintf(hex, sizeof(hex), "%02X", x509->serial.p[i]); result += hex; } return result; } inline bool get_cert_der(cert_t cert, std::vector &der) { if (!cert) return false; auto crt = static_cast(cert); if (!crt->raw.p || crt->raw.len == 0) return false; der.assign(crt->raw.p, crt->raw.p + crt->raw.len); return true; } inline const char *get_sni(const_session_t session) { if (!session) return nullptr; auto msession = static_cast(session); // For server: return SNI received from client during handshake if (!msession->sni_hostname.empty()) { return msession->sni_hostname.c_str(); } // For client: return the hostname set via set_sni if (!msession->hostname.empty()) { return msession->hostname.c_str(); } return nullptr; } inline uint64_t peek_error() { // Mbed TLS doesn't have an error queue, return the last error return static_cast(-impl::mbedtls_last_error()); } inline uint64_t get_error() { // Mbed TLS doesn't have an error queue, return and clear the last error uint64_t err = static_cast(-impl::mbedtls_last_error()); impl::mbedtls_last_error() = 0; return err; } inline std::string error_string(uint64_t code) { char buf[256]; mbedtls_strerror(-static_cast(code), buf, sizeof(buf)); return std::string(buf); } inline ca_store_t create_ca_store(const char *pem, size_t len) { auto *ca_chain = new (std::nothrow) mbedtls_x509_crt; if (!ca_chain) { return nullptr; } mbedtls_x509_crt_init(ca_chain); // mbedtls_x509_crt_parse expects null-terminated PEM int ret = mbedtls_x509_crt_parse(ca_chain, reinterpret_cast(pem), len + 1); // +1 for null terminator if (ret != 0) { // Try without +1 in case PEM is already null-terminated ret = mbedtls_x509_crt_parse( ca_chain, reinterpret_cast(pem), len); if (ret != 0) { mbedtls_x509_crt_free(ca_chain); delete ca_chain; return nullptr; } } return static_cast(ca_chain); } inline void free_ca_store(ca_store_t store) { if (store) { auto *ca_chain = static_cast(store); mbedtls_x509_crt_free(ca_chain); delete ca_chain; } } inline bool set_ca_store(ctx_t ctx, ca_store_t store) { if (!ctx || !store) { return false; } auto *mbed_ctx = static_cast(ctx); auto *ca_chain = static_cast(store); // Free existing CA chain mbedtls_x509_crt_free(&mbed_ctx->ca_chain); mbedtls_x509_crt_init(&mbed_ctx->ca_chain); // Copy the CA chain (deep copy) // Parse from the raw data of the source cert mbedtls_x509_crt *src = ca_chain; while (src != nullptr) { int ret = mbedtls_x509_crt_parse_der(&mbed_ctx->ca_chain, src->raw.p, src->raw.len); if (ret != 0) { return false; } src = src->next; } // Update the SSL config to use the new CA chain mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->ca_chain, nullptr); return true; } inline size_t get_ca_certs(ctx_t ctx, std::vector &certs) { certs.clear(); if (!ctx) { return 0; } auto *mbed_ctx = static_cast(ctx); // Iterate through the CA chain mbedtls_x509_crt *cert = &mbed_ctx->ca_chain; while (cert != nullptr && cert->raw.len > 0) { // Create a copy of the certificate for the caller auto *copy = new mbedtls_x509_crt; mbedtls_x509_crt_init(copy); int ret = mbedtls_x509_crt_parse_der(copy, cert->raw.p, cert->raw.len); if (ret == 0) { certs.push_back(static_cast(copy)); } else { mbedtls_x509_crt_free(copy); delete copy; } cert = cert->next; } return certs.size(); } inline std::vector get_ca_names(ctx_t ctx) { std::vector names; if (!ctx) { return names; } auto *mbed_ctx = static_cast(ctx); // Iterate through the CA chain mbedtls_x509_crt *cert = &mbed_ctx->ca_chain; while (cert != nullptr && cert->raw.len > 0) { char buf[512]; int ret = mbedtls_x509_dn_gets(buf, sizeof(buf), &cert->subject); if (ret > 0) { names.push_back(buf); } cert = cert->next; } return names; } inline bool update_server_cert(ctx_t ctx, const char *cert_pem, const char *key_pem, const char *password) { if (!ctx || !cert_pem || !key_pem) { return false; } auto *mbed_ctx = static_cast(ctx); // Free existing certificate and key mbedtls_x509_crt_free(&mbed_ctx->own_cert); mbedtls_pk_free(&mbed_ctx->own_key); mbedtls_x509_crt_init(&mbed_ctx->own_cert); mbedtls_pk_init(&mbed_ctx->own_key); // Parse certificate PEM int ret = mbedtls_x509_crt_parse( &mbed_ctx->own_cert, reinterpret_cast(cert_pem), strlen(cert_pem) + 1); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } // Parse private key PEM #ifdef CPPHTTPLIB_MBEDTLS_V3 ret = mbedtls_pk_parse_key( &mbed_ctx->own_key, reinterpret_cast(key_pem), strlen(key_pem) + 1, password ? reinterpret_cast(password) : nullptr, password ? strlen(password) : 0, mbedtls_ctr_drbg_random, &mbed_ctx->ctr_drbg); #else ret = mbedtls_pk_parse_key( &mbed_ctx->own_key, reinterpret_cast(key_pem), strlen(key_pem) + 1, password ? reinterpret_cast(password) : nullptr, password ? strlen(password) : 0); #endif if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } // Configure SSL to use the new certificate and key ret = mbedtls_ssl_conf_own_cert(&mbed_ctx->conf, &mbed_ctx->own_cert, &mbed_ctx->own_key); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } return true; } inline bool update_server_client_ca(ctx_t ctx, const char *ca_pem) { if (!ctx || !ca_pem) { return false; } auto *mbed_ctx = static_cast(ctx); // Free existing CA chain mbedtls_x509_crt_free(&mbed_ctx->ca_chain); mbedtls_x509_crt_init(&mbed_ctx->ca_chain); // Parse CA PEM int ret = mbedtls_x509_crt_parse( &mbed_ctx->ca_chain, reinterpret_cast(ca_pem), strlen(ca_pem) + 1); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } // Update SSL config to use new CA chain mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->ca_chain, nullptr); return true; } inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { if (!ctx) { return false; } auto *mbed_ctx = static_cast(ctx); impl::get_verify_callback() = std::move(callback); mbed_ctx->has_verify_callback = static_cast(impl::get_verify_callback()); if (mbed_ctx->has_verify_callback) { // Set OPTIONAL mode to ensure callback is called even when verification // is disabled (matching OpenSSL behavior where SSL_VERIFY_PEER is set) mbedtls_ssl_conf_authmode(&mbed_ctx->conf, MBEDTLS_SSL_VERIFY_OPTIONAL); mbedtls_ssl_conf_verify(&mbed_ctx->conf, impl::mbedtls_verify_callback, nullptr); } else { mbedtls_ssl_conf_verify(&mbed_ctx->conf, nullptr, nullptr); } return true; } inline long get_verify_error(const_session_t session) { if (!session) { return -1; } auto *msession = static_cast(const_cast(session)); return static_cast(mbedtls_ssl_get_verify_result(&msession->ssl)); } inline std::string verify_error_string(long error_code) { if (error_code == 0) { return ""; } char buf[256]; mbedtls_x509_crt_verify_info(buf, sizeof(buf), "", static_cast(error_code)); // Remove trailing newline if present std::string result(buf); while (!result.empty() && (result.back() == '\n' || result.back() == ' ')) { result.pop_back(); } return result; } } // namespace tls #endif // CPPHTTPLIB_MBEDTLS_SUPPORT // ---------------------------------------------------------------------------- } // namespace httplib #endif // CPPHTTPLIB_HTTPLIB_H ================================================ FILE: src/libs/CMakeLists.txt ================================================ add_subdirectory(browser) add_subdirectory(core) add_subdirectory(registry) add_subdirectory(sidebar) add_subdirectory(ui) add_subdirectory(util) ================================================ FILE: src/libs/browser/CMakeLists.txt ================================================ add_library(Browser STATIC searchtoolbar.cpp settings.cpp urlrequestinterceptor.cpp webbridge.cpp webcontrol.cpp webpage.cpp webview.cpp ) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS WebChannel WebEngineWidgets REQUIRED) target_link_libraries(Browser PRIVATE Qt${QT_VERSION_MAJOR}::WebChannel Qt${QT_VERSION_MAJOR}::WebEngineWidgets) ================================================ FILE: src/libs/browser/searchtoolbar.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "searchtoolbar.h" #include #include #include #include #include #include #include #include #include using namespace Zeal::Browser; SearchToolBar::SearchToolBar(QWebEngineView *webView, QWidget *parent) : QWidget(parent) , m_webView(webView) { auto layout = new QHBoxLayout(this); layout->setContentsMargins(4, 4, 4, 4); layout->setSpacing(4); m_lineEdit = new QLineEdit(); m_lineEdit->installEventFilter(this); m_lineEdit->setMaximumWidth(200); m_lineEdit->setPlaceholderText(tr("Find in page")); connect(m_lineEdit, &QLineEdit::textChanged, this, &SearchToolBar::findNext); connect(m_lineEdit, &QLineEdit::textChanged, this, &SearchToolBar::updateHighlight); layout->addWidget(m_lineEdit); m_findPreviousButton = new QToolButton(); m_findPreviousButton->setAutoRaise(true); m_findPreviousButton->setIcon(qApp->style()->standardIcon(QStyle::SP_ArrowBack)); m_findPreviousButton->setToolTip(tr("Previous result")); connect(m_findPreviousButton, &QToolButton::clicked, this, &SearchToolBar::findPrevious); layout->addWidget(m_findPreviousButton); // A workaround for QAbstractButton lacking support for multiple shortcuts. auto action = new QAction(m_findPreviousButton); action->setShortcuts(QKeySequence::FindPrevious); connect(action, &QAction::triggered, this, [this]() { m_findPreviousButton->animateClick(); }); addAction(action); m_findNextButton = new QToolButton(); m_findNextButton->setAutoRaise(true); m_findNextButton->setIcon(qApp->style()->standardIcon(QStyle::SP_ArrowForward)); m_findNextButton->setToolTip(tr("Next result")); connect(m_findNextButton, &QToolButton::clicked, this, &SearchToolBar::findNext); layout->addWidget(m_findNextButton); action = new QAction(m_findNextButton); action->setShortcuts(QKeySequence::FindNext); connect(action, &QAction::triggered, this, [this]() { m_findNextButton->animateClick(); }); addAction(action); m_highlightAllButton = new QToolButton(); m_highlightAllButton->setAutoRaise(true); m_highlightAllButton->setCheckable(true); m_highlightAllButton->setText(tr("High&light All")); connect(m_highlightAllButton, &QToolButton::toggled, this, &SearchToolBar::updateHighlight); layout->addWidget(m_highlightAllButton); m_matchCaseButton = new QToolButton(); m_matchCaseButton->setAutoRaise(true); m_matchCaseButton->setCheckable(true); m_matchCaseButton->setText(tr("Mat&ch Case")); connect(m_matchCaseButton, &QToolButton::toggled, this, &SearchToolBar::updateHighlight); layout->addWidget(m_matchCaseButton); layout->addStretch(); auto closeButton = new QToolButton(); closeButton->setAutoRaise(true); closeButton->setIcon(qApp->style()->standardIcon(QStyle::SP_TitleBarCloseButton)); closeButton->setToolTip(tr("Close find bar")); connect(closeButton, &QToolButton::clicked, this, &QWidget::hide); layout->addWidget(closeButton); setLayout(layout); setMaximumHeight(sizeHint().height()); setMinimumWidth(sizeHint().width()); } void SearchToolBar::setText(const QString &text) { m_lineEdit->setText(text); } void SearchToolBar::activate() { show(); m_lineEdit->selectAll(); m_lineEdit->setFocus(); } bool SearchToolBar::eventFilter(QObject *object, QEvent *event) { if (object == m_lineEdit && event->type() == QEvent::KeyPress) { auto keyEvent = static_cast(event); switch (keyEvent->key()) { case Qt::Key_Enter: case Qt::Key_Return: if (keyEvent->modifiers().testFlag(Qt::ControlModifier)) { m_highlightAllButton->toggle(); } else if (keyEvent->modifiers().testFlag(Qt::ShiftModifier)) { findPrevious(); } else { findNext(); } return true; case Qt::Key_Down: case Qt::Key_Up: case Qt::Key_PageDown: case Qt::Key_PageUp: QCoreApplication::sendEvent(m_webView->focusProxy(), event); return true; default: break; } } return QWidget::eventFilter(object, event); } void SearchToolBar::hideEvent(QHideEvent *event) { hideHighlight(); m_webView->setFocus(); QWidget::hideEvent(event); } void SearchToolBar::showEvent(QShowEvent *event) { activate(); QWidget::showEvent(event); } void SearchToolBar::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { hide(); } } void SearchToolBar::findNext() { if (!isVisible()) { return; } QWebEnginePage::FindFlags ff; ff.setFlag(QWebEnginePage::FindCaseSensitively, m_matchCaseButton->isChecked()); m_webView->findText(m_lineEdit->text(), ff); } void SearchToolBar::findPrevious() { if (!isVisible()) { return; } QWebEnginePage::FindFlags ff; ff.setFlag(QWebEnginePage::FindCaseSensitively, m_matchCaseButton->isChecked()); ff.setFlag(QWebEnginePage::FindBackward); m_webView->findText(m_lineEdit->text(), ff); } void SearchToolBar::hideHighlight() { m_webView->findText(QString()); } void SearchToolBar::updateHighlight() { hideHighlight(); if (m_highlightAllButton->isChecked()) { QWebEnginePage::FindFlags ff; ff.setFlag(QWebEnginePage::FindCaseSensitively, m_matchCaseButton->isChecked()); m_webView->findText(m_lineEdit->text(), ff); } } ================================================ FILE: src/libs/browser/searchtoolbar.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_BROWSER_SEARCHTOOLBAR_H #define ZEAL_BROWSER_SEARCHTOOLBAR_H #include class QLineEdit; class QToolButton; class QWebEngineView; namespace Zeal { namespace Browser { class SearchToolBar final : public QWidget { Q_OBJECT Q_DISABLE_COPY_MOVE(SearchToolBar) public: explicit SearchToolBar(QWebEngineView *webView, QWidget *parent = nullptr); void setText(const QString &text); void activate(); bool eventFilter(QObject *object, QEvent *event) override; protected: void hideEvent(QHideEvent *event) override; void showEvent(QShowEvent *event) override; void keyPressEvent(QKeyEvent *event) override; private: void findNext(); void findPrevious(); void hideHighlight(); void updateHighlight(); QLineEdit *m_lineEdit = nullptr; QToolButton *m_findNextButton = nullptr; QToolButton *m_findPreviousButton = nullptr; QToolButton *m_highlightAllButton = nullptr; QToolButton *m_matchCaseButton = nullptr; QWebEngineView *m_webView = nullptr; }; } // namespace Browser } // namespace Zeal #endif // ZEAL_BROWSER_SEARCHTOOLBAR_H ================================================ FILE: src/libs/browser/settings.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "settings.h" #include "urlrequestinterceptor.h" #include #include #include #include #include #include #include namespace { constexpr char HighlightOnNavigateCssPath[] = ":/browser/assets/css/highlight.css"; } using namespace Zeal; using namespace Zeal::Browser; static Q_LOGGING_CATEGORY(log, "zeal.browser.settings") QWebEngineProfile *Settings::m_webProfile = nullptr; Settings::Settings(Core::Settings *appSettings, QObject *parent) : QObject(parent) , m_appSettings(appSettings) { Q_ASSERT(!m_webProfile); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // Create a new off-the-record profile. m_webProfile = new QWebEngineProfile(this); #else // Default profile for Qt 6 is off-the-record. m_webProfile = QWebEngineProfile::defaultProfile(); #endif // Setup URL interceptor. m_webProfile->setUrlRequestInterceptor(new UrlRequestInterceptor(this)); // Listen to settings changes. connect(m_appSettings, &Core::Settings::updated, this, &Settings::applySettings); applySettings(); } void Settings::applySettings() { m_webProfile->settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, m_appSettings->isSmoothScrollingEnabled); // Qt 6.7+ does not require restart to enable dark mode. #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) m_webProfile->settings()->setAttribute(QWebEngineSettings::ForceDarkMode, m_appSettings->isDarkModeEnabled()); #endif // Apply custom CSS. // TODO: Apply to all open pages. m_webProfile->scripts()->clear(); // Remove all scripts first. if (m_appSettings->isHighlightOnNavigateEnabled) { setCustomStyleSheet(QStringLiteral("_zeal_highlightstylesheet"), HighlightOnNavigateCssPath); } if (QFileInfo::exists(m_appSettings->customCssFile)) { setCustomStyleSheet(QStringLiteral("_zeal_userstylesheet"), m_appSettings->customCssFile); } } QWebEngineProfile *Settings::defaultProfile() { Q_ASSERT(m_webProfile); return m_webProfile; } void Settings::setCustomStyleSheet(const QString &name, const QString &path) { // Read the stylesheet file content. QFile file(path); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qCWarning(log, "Failed to read stylesheet file '%s'.", qPrintable(path)); return; } QString stylesheet = QString::fromUtf8(file.readAll()); file.close(); // Escape single quotes and newlines for JavaScript string literal. stylesheet.replace(QLatin1String("'"), QLatin1String("\\'")); stylesheet.replace(QLatin1String("\n"), QLatin1String("\\n")); QString cssInjectCode = QLatin1String( "(() => {" "const head = document.getElementsByTagName('head')[0];" "if (!head) { console.error('Cannot set custom stylesheet.'); return; }" "const stylesheet = document.createElement('style');" "stylesheet.setAttribute('type', 'text/css');" "stylesheet.textContent = '%1';" "head.appendChild(stylesheet);" "})()" ); QWebEngineScript script; script.setName(name); script.setSourceCode(cssInjectCode.arg(stylesheet)); script.setInjectionPoint(QWebEngineScript::DocumentReady); script.setRunsOnSubFrames(true); script.setWorldId(QWebEngineScript::ApplicationWorld); m_webProfile->scripts()->insert(script); } ================================================ FILE: src/libs/browser/settings.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_BROWSER_SETTINGS_H #define ZEAL_BROWSER_SETTINGS_H #include class QWebEngineProfile; namespace Zeal { namespace Core { class Settings; } namespace Browser { class Settings final : public QObject { Q_OBJECT Q_DISABLE_COPY_MOVE(Settings) public: explicit Settings(Core::Settings *appSettings, QObject *parent = nullptr); static QWebEngineProfile *defaultProfile(); private slots: void applySettings(); private: void setCustomStyleSheet(const QString &name, const QString &cssUrl); Core::Settings *m_appSettings = nullptr; static QWebEngineProfile *m_webProfile; }; } // namespace Browser } // namespace Zeal #endif // ZEAL_BROWSER_SETTINGS_H ================================================ FILE: src/libs/browser/urlrequestinterceptor.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2019 Kay Gawlik // SPDX-License-Identifier: GPL-3.0-or-later #include "urlrequestinterceptor.h" #include #include using namespace Zeal::Browser; static Q_LOGGING_CATEGORY(log, "zeal.browser.urlrequestinterceptor") UrlRequestInterceptor::UrlRequestInterceptor(QObject *parent) : QWebEngineUrlRequestInterceptor(parent) { } void UrlRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo &info) { const QUrl requestUrl = info.requestUrl(); const QUrl firstPartyUrl = info.firstPartyUrl(); // Block invalid requests. if (!requestUrl.isValid() || !firstPartyUrl.isValid()) { blockRequest(info); return; } // Direct links are controlled in the WebPage if (info.resourceType() == QWebEngineUrlRequestInfo::ResourceTypeMainFrame) { return; } bool isFirstPartyUrlLocal = Core::NetworkAccessManager::isLocalUrl(firstPartyUrl); bool isRequestUrlLocal = Core::NetworkAccessManager::isLocalUrl(requestUrl); // Allow local resources on local pages and external resources on external pages. if (isFirstPartyUrlLocal == isRequestUrlLocal) { return; } blockRequest(info); } void UrlRequestInterceptor::blockRequest(QWebEngineUrlRequestInfo &info) { qCDebug(log, "Blocked request: %s '%s' (resource_type=%d, navigation_type=%d).", info.requestMethod().data(), qPrintable(info.requestUrl().toString()), info.resourceType(), info.navigationType()); info.block(true); } ================================================ FILE: src/libs/browser/urlrequestinterceptor.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2019 Kay Gawlik // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_BROWSER_URLREQUESTINTERCEPTOR_H #define ZEAL_BROWSER_URLREQUESTINTERCEPTOR_H #include namespace Zeal { namespace Browser { class UrlRequestInterceptor final : public QWebEngineUrlRequestInterceptor { Q_OBJECT Q_DISABLE_COPY_MOVE(UrlRequestInterceptor) public: UrlRequestInterceptor(QObject *parent = nullptr); void interceptRequest(QWebEngineUrlRequestInfo &info) override; private: void blockRequest(QWebEngineUrlRequestInfo &info); }; } // namespace Browser } // namespace Zeal #endif // ZEAL_BROWSER_URLREQUESTINTERCEPTOR_H ================================================ FILE: src/libs/browser/webbridge.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "webbridge.h" #include #include #include using namespace Zeal::Browser; WebBridge::WebBridge(QObject *parent) : QObject(parent) { } void WebBridge::openShortUrl(const QString &key) { QDesktopServices::openUrl(QUrl(QStringLiteral("https://go.zealdocs.org/l/") + key)); } void WebBridge::triggerAction(const QString &action) { emit actionTriggered(action); } QString WebBridge::appVersion() const { return Core::Application::versionString(); } ================================================ FILE: src/libs/browser/webbridge.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_BROWSER_WEBBRIDGE_H #define ZEAL_BROWSER_WEBBRIDGE_H #include namespace Zeal { namespace Browser { class WebBridge final : public QObject { Q_OBJECT Q_DISABLE_COPY_MOVE(WebBridge) Q_PROPERTY(QString AppVersion READ appVersion CONSTANT) public: explicit WebBridge(QObject *parent = nullptr); signals: void actionTriggered(const QString &action); public slots: Q_INVOKABLE void openShortUrl(const QString &key); Q_INVOKABLE void triggerAction(const QString &action); private: QString appVersion() const; }; } // namespace Browser } // namespace Zeal #endif // ZEAL_BROWSER_WEBBRIDGE_H ================================================ FILE: src/libs/browser/webcontrol.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "webcontrol.h" #include "searchtoolbar.h" #include "webview.h" #include #include #include #include #include #include #include #include using namespace Zeal::Browser; WebControl::WebControl(QWidget *parent) : QWidget(parent) { auto layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); m_webView = new WebView(); setFocusProxy(m_webView); connect(m_webView->page(), &QWebEnginePage::linkHovered, this, [this](const QString &link) { if (Core::NetworkAccessManager::isLocalUrl(QUrl(link))) { return; } m_webView->setToolTip(link); }); connect(m_webView, &QWebEngineView::titleChanged, this, &WebControl::titleChanged); connect(m_webView, &QWebEngineView::urlChanged, this, &WebControl::urlChanged); layout->addWidget(m_webView); setLayout(layout); } void WebControl::focus() { m_webView->setFocus(); } int WebControl::zoomLevel() const { return m_webView->zoomLevel(); } void WebControl::setZoomLevel(int level) { m_webView->setZoomLevel(level); } void WebControl::zoomIn() { m_webView->zoomIn(); } void WebControl::zoomOut() { m_webView->zoomOut(); } void WebControl::resetZoom() { m_webView->resetZoom(); } void WebControl::setJavaScriptEnabled(bool enabled) { m_webView->page()->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, enabled); } void WebControl::setWebBridgeObject(const QString &name, QObject *object) { QWebEnginePage *page = m_webView->page(); QWebChannel *channel = new QWebChannel(page); channel->registerObject(name, object); page->setWebChannel(channel); } void WebControl::load(const QUrl &url) { m_webView->load(url); } void WebControl::activateSearchBar() { if (m_searchToolBar == nullptr) { m_searchToolBar = new SearchToolBar(m_webView); layout()->addWidget(m_searchToolBar); } if (m_webView->hasSelection()) { const QString selectedText = m_webView->selectedText().simplified(); if (!selectedText.isEmpty()) { m_searchToolBar->setText(selectedText); } } m_searchToolBar->activate(); } void WebControl::back() { m_webView->back(); } void WebControl::forward() { m_webView->forward(); } bool WebControl::canGoBack() const { return m_webView->history()->canGoBack(); } bool WebControl::canGoForward() const { return m_webView->history()->canGoForward(); } QString WebControl::title() const { return m_webView->title(); } QUrl WebControl::url() const { return m_webView->url(); } QWebEngineHistory *WebControl::history() const { return m_webView->history(); } void WebControl::restoreHistory(const QByteArray &array) { QDataStream stream(array); stream >> *m_webView->history(); } QByteArray WebControl::saveHistory() const { QByteArray array; QDataStream stream(&array, QIODevice::WriteOnly); stream << *m_webView->history(); return array; } void WebControl::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Slash: activateSearchBar(); break; default: event->ignore(); break; } } ================================================ FILE: src/libs/browser/webcontrol.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_BROWSER_WEBCONTROL_H #define ZEAL_BROWSER_WEBCONTROL_H #include class QWebEngineHistory; namespace Zeal { namespace Browser { class SearchToolBar; class WebView; class WebControl final : public QWidget { Q_OBJECT Q_DISABLE_COPY_MOVE(WebControl) public: explicit WebControl(QWidget *parent = nullptr); void focus(); void load(const QUrl &url); bool canGoBack() const; bool canGoForward() const; QString title() const; QUrl url() const; QWebEngineHistory *history() const; void restoreHistory(const QByteArray &array); QByteArray saveHistory() const; int zoomLevel() const; void setZoomLevel(int level); void setJavaScriptEnabled(bool enabled); void setWebBridgeObject(const QString &name, QObject *object); signals: void titleChanged(const QString &title); void urlChanged(const QUrl &url); public slots: void activateSearchBar(); void back(); void forward(); void zoomIn(); void zoomOut(); void resetZoom(); protected: void keyPressEvent(QKeyEvent *event) override; private: friend class WebView; WebView *m_webView = nullptr; SearchToolBar *m_searchToolBar = nullptr; }; } // namespace Browser } // namespace Zeal #endif // ZEAL_BROWSER_WEBCONTROL_H ================================================ FILE: src/libs/browser/webpage.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2019 Kay Gawlik // SPDX-License-Identifier: GPL-3.0-or-later #include "webpage.h" #include "settings.h" #include #include #include #include #include #include #include #include #include using namespace Zeal::Browser; static Q_LOGGING_CATEGORY(log, "zeal.browser.webpage") WebPage::WebPage(QObject *parent) : QWebEnginePage(Settings::defaultProfile(), parent) { } bool WebPage::acceptNavigationRequest(const QUrl &requestUrl, QWebEnginePage::NavigationType type, bool isMainFrame) { Q_UNUSED(type) // Local elements are always allowed. if (Core::NetworkAccessManager::isLocalUrl(requestUrl)) { return true; } // Allow external resources if already on an external page. const QUrl pageUrl = url(); if (pageUrl.isValid() && !Core::NetworkAccessManager::isLocalUrl(pageUrl)) { return true; } // Block external elements on local pages. if (!isMainFrame) { qCDebug(log, "Blocked request to '%s'.", qPrintable(requestUrl.toString())); return false; } auto appSettings = Core::Application::instance()->settings(); // TODO: [C++20] using enum Core::Settings::ExternalLinkPolicy; typedef Core::Settings::ExternalLinkPolicy ExternalLinkPolicy; switch (appSettings->externalLinkPolicy) { case ExternalLinkPolicy::Open: return true; case ExternalLinkPolicy::Ask: { QMessageBox mb; mb.setIcon(QMessageBox::Question); mb.setText(tr("How do you want to open the external link?
URL: %1") .arg(requestUrl.toString())); QCheckBox *checkBox = new QCheckBox("Do ¬ ask again"); mb.setCheckBox(checkBox); QPushButton *openInBrowserButton = mb.addButton(tr("Open in &Desktop Browser"), QMessageBox::ActionRole); QPushButton *openInZealButton = mb.addButton(tr("Open in &Zeal"), QMessageBox::ActionRole); mb.addButton(QMessageBox::Cancel); mb.setDefaultButton(openInBrowserButton); if (mb.exec() == QMessageBox::Cancel) { qCDebug(log, "Blocked request to '%s'.", qPrintable(requestUrl.toString())); return false; } if (mb.clickedButton() == openInZealButton) { if (checkBox->isChecked()) { appSettings->externalLinkPolicy = ExternalLinkPolicy::Open; appSettings->save(); } return true; } if (mb.clickedButton() == openInBrowserButton) { if (checkBox->isChecked()) { appSettings->externalLinkPolicy = ExternalLinkPolicy::OpenInSystemBrowser; appSettings->save(); } QDesktopServices::openUrl(requestUrl); return false; } break; } case ExternalLinkPolicy::OpenInSystemBrowser: QDesktopServices::openUrl(requestUrl); return false; } // This code should not be reachable so log a warning. qCWarning(log, "Blocked request to '%s'.", qPrintable(requestUrl.toString())); return false; } void WebPage::javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message, int lineNumber, const QString &sourceId) { qCDebug(log, "%s [%s:%d] %s", qPrintable(QVariant::fromValue(level).toString()), qPrintable(sourceId), lineNumber, qPrintable(message)); } ================================================ FILE: src/libs/browser/webpage.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2019 Kay Gawlik // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_BROWSER_WEBPAGE_H #define ZEAL_BROWSER_WEBPAGE_H #include namespace Zeal { namespace Browser { class WebPage final : public QWebEnginePage { Q_OBJECT Q_DISABLE_COPY_MOVE(WebPage) public: explicit WebPage(QObject *parent = nullptr); protected: bool acceptNavigationRequest(const QUrl &requestUrl, NavigationType type, bool isMainFrame) override; void javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message, int lineNumber, const QString &sourceId) override; }; } // namespace Browser } // namespace Zeal #endif // ZEAL_BROWSER_WEBPAGE_H ================================================ FILE: src/libs/browser/webview.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "webview.h" #include "webcontrol.h" #include "webpage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #include #else #include #endif #include using namespace Zeal::Browser; WebView::WebView(QWidget *parent) : QWebEngineView(parent) { setPage(new WebPage(this)); setZoomLevel(defaultZoomLevel()); // Enable plugins for PDF support. settings()->setAttribute(QWebEngineSettings::PluginsEnabled, true); settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false); QApplication::instance()->installEventFilter(this); } int WebView::zoomLevel() const { return m_zoomLevel; } void WebView::setZoomLevel(int level) { if (level == m_zoomLevel) { return; } level = qMax(0, level); level = qMin(level, availableZoomLevels().size() - 1); m_zoomLevel = level; // Scale the webview relative to the DPI of the screen. // Only scale up for HiDPI displays (>96 DPI), never scale down. const double dpiZoomFactor = std::max(1.0, logicalDpiY() / 96.0); setZoomFactor(availableZoomLevels().at(level) / 100.0 * dpiZoomFactor); emit zoomLevelChanged(); } const QVector &WebView::availableZoomLevels() { static const QVector zoomLevels = {30, 40, 50, 67, 80, 90, 100, 110, 120, 133, 150, 170, 200, 220, 233, 250, 270, 285, 300}; return zoomLevels; } int WebView::defaultZoomLevel() { static const int level = availableZoomLevels().indexOf(100); return level; } void WebView::zoomIn() { setZoomLevel(m_zoomLevel + 1); } void WebView::zoomOut() { setZoomLevel(m_zoomLevel - 1); } void WebView::resetZoom() { setZoomLevel(defaultZoomLevel()); } QWebEngineView *WebView::createWindow(QWebEnginePage::WebWindowType type) { Q_UNUSED(type) return Core::Application::instance()->mainWindow()->createTab()->webControl()->m_webView; } void WebView::contextMenuEvent(QContextMenuEvent *event) { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) const QWebEngineContextMenuData& contextData = page()->contextMenuData(); if (!contextData.isValid()) { QWebEngineView::contextMenuEvent(event); return; } #else QWebEngineContextMenuRequest *contextMenuRequest = lastContextMenuRequest(); if (contextMenuRequest == nullptr) { QWebEngineView::contextMenuEvent(event); return; } #endif event->accept(); if (m_contextMenu) { m_contextMenu->deleteLater(); } m_contextMenu = new QMenu(this); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) QUrl linkUrl = contextData.linkUrl(); #else QUrl linkUrl = contextMenuRequest->linkUrl(); #endif if (linkUrl.isValid()) { const QString scheme = linkUrl.scheme(); if (scheme != QLatin1String("javascript")) { m_contextMenu->addAction(tr("Open Link in New Tab"), this, [this]() { triggerPageAction(QWebEnginePage::WebAction::OpenLinkInNewWindow); }); } if (scheme != QLatin1String("qrc")) { if (scheme != QLatin1String("javascript")) { m_contextMenu->addAction(tr("Open Link in Desktop Browser"), this, [linkUrl]() { QDesktopServices::openUrl(linkUrl); }); } m_contextMenu->addAction(pageAction(QWebEnginePage::CopyLinkToClipboard)); } } #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) const QString selectedText = contextData.selectedText(); #else const QString selectedText = contextMenuRequest->selectedText(); #endif if (!selectedText.isEmpty()) { if (!m_contextMenu->isEmpty()) { m_contextMenu->addSeparator(); } m_contextMenu->addAction(pageAction(QWebEnginePage::Copy)); } if (!linkUrl.isValid() && url().scheme() != QLatin1String("qrc")) { if (!m_contextMenu->isEmpty()) { m_contextMenu->addSeparator(); } m_contextMenu->addAction(pageAction(QWebEnginePage::Back)); m_contextMenu->addAction(pageAction(QWebEnginePage::Forward)); m_contextMenu->addSeparator(); m_contextMenu->addAction(tr("Open Page in Desktop Browser"), this, [this]() { QDesktopServices::openUrl(url()); }); } if (m_contextMenu->isEmpty()) { return; } m_contextMenu->popup(event->globalPos()); } bool WebView::handleMouseReleaseEvent(QMouseEvent *event) { switch (event->button()) { case Qt::BackButton: // Check if cursor is still inside webview. if (rect().contains(event->pos())) { back(); } event->accept(); return true; case Qt::ForwardButton: if (rect().contains(event->pos())) { forward(); } event->accept(); return true; default: break; } return false; } bool WebView::handleWheelEvent(QWheelEvent *event) { if (event->modifiers() & Qt::ControlModifier) { const QPoint angleDelta = event->angleDelta(); int delta = qAbs(angleDelta.x()) > qAbs(angleDelta.y()) ? angleDelta.x() : angleDelta.y(); const int direction = delta > 0 ? 1 : -1; int levelDelta = 0; while (delta * direction >= 120) { levelDelta += direction; delta -= 120 * direction; } setZoomLevel(m_zoomLevel + levelDelta); event->accept(); return true; } return false; } bool WebView::eventFilter(QObject *watched, QEvent *event) { if (watched->parent() == this) { switch (event->type()) { case QEvent::MouseButtonRelease: if (handleMouseReleaseEvent(static_cast(event))) { return true; } break; case QEvent::Wheel: if (handleWheelEvent(static_cast(event))) { return true; } break; default: break; } } return QWebEngineView::eventFilter(watched, event); } ================================================ FILE: src/libs/browser/webview.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_BROWSER_WEBVIEW_H #define ZEAL_BROWSER_WEBVIEW_H #include namespace Zeal { namespace Browser { class WebView final : public QWebEngineView { Q_OBJECT Q_DISABLE_COPY_MOVE(WebView) public: explicit WebView(QWidget *parent = nullptr); int zoomLevel() const; void setZoomLevel(int level); bool eventFilter(QObject *watched, QEvent *event) override; static const QVector &availableZoomLevels(); static int defaultZoomLevel(); public slots: void zoomIn(); void zoomOut(); void resetZoom(); signals: void zoomLevelChanged(); protected: QWebEngineView *createWindow(QWebEnginePage::WebWindowType type) override; void contextMenuEvent(QContextMenuEvent *event) override; private: bool handleMouseReleaseEvent(QMouseEvent *event); bool handleWheelEvent(QWheelEvent *event); QMenu *m_contextMenu = nullptr; QUrl m_clickedLink; int m_zoomLevel = 0; }; } // namespace Browser } // namespace Zeal #endif // ZEAL_BROWSER_WEBVIEW_H ================================================ FILE: src/libs/core/CMakeLists.txt ================================================ add_library(Core STATIC application.cpp applicationsingleton.cpp extractor.cpp filemanager.cpp httpserver.cpp networkaccessmanager.cpp settings.cpp ) target_link_libraries(Core PRIVATE Registry Ui) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Network WebEngineCore Widgets REQUIRED) target_link_libraries(Core PRIVATE Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::WebEngineCore Qt${QT_VERSION_MAJOR}::Widgets) if(QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS WebEngineCore REQUIRED) target_link_libraries(Core PRIVATE Qt6::WebEngineCore) else() find_package(Qt5 COMPONENTS WebEngineWidgets REQUIRED) target_link_libraries(Core PRIVATE Qt5::WebEngineWidgets) endif() find_package(LibArchive QUIET) if(NOT LibArchive_FOUND) find_path(LibArchive_INCLUDE_DIRS archive.h PATHS /opt/homebrew/opt/libarchive/include /usr/local/opt/libarchive/include REQUIRED ) find_library(LibArchive_LIBRARIES NAMES archive libarchive PATHS /opt/homebrew/opt/libarchive/lib /usr/local/opt/libarchive/lib REQUIRED NO_DEFAULT_PATH ) endif() if((CMAKE_VERSION VERSION_GREATER_EQUAL 3.17.0) AND (TARGET LibArchive::LibArchive)) target_link_libraries(Core PRIVATE LibArchive::LibArchive) else() include_directories(${LibArchive_INCLUDE_DIRS}) target_link_libraries(Core PRIVATE ${LibArchive_LIBRARIES}) endif() # Configure cpp-httplib. add_definitions(-DCPPHTTPLIB_USE_POLL) find_package(httplib CONFIG QUIET) if(httplib_FOUND) target_link_libraries(Core PRIVATE httplib::httplib) else() # Use bundled version of cpp-httplib if not found. # TODO: Replace with QHttpServer once Qt 5 is dropped. include_directories("${CMAKE_SOURCE_DIR}/src/contrib/cpp-httplib") endif() # Required by cpp-httplib. if(NOT WIN32) set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) target_link_libraries(Core PRIVATE Threads::Threads) endif() ================================================ FILE: src/libs/core/application.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "application.h" #include "extractor.h" #include "filemanager.h" #include "httpserver.h" #include "networkaccessmanager.h" #include "settings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Zeal; using namespace Zeal::Core; namespace { constexpr char ReleasesApiUrl[] = "https://api.zealdocs.org/v1/releases"; } // namespace Application *Application::m_instance = nullptr; Application::Application(QObject *parent) : QObject(parent) { // Ensure only one instance of Application Q_ASSERT(!m_instance); m_instance = this; m_settings = new Settings(this); m_networkManager = new NetworkAccessManager(this); m_fileManager = new FileManager(this); m_httpServer = new HttpServer(this); connect(m_networkManager, &QNetworkAccessManager::sslErrors, this, [this](QNetworkReply *reply, const QList &errors) { Q_UNUSED(errors); if (m_settings->isIgnoreSslErrorsEnabled) { reply->ignoreSslErrors(); } }); // Extractor setup m_extractorThread = new QThread(this); m_extractor = new Extractor(); m_extractor->moveToThread(m_extractorThread); m_extractorThread->start(); connect(m_extractor, &Extractor::completed, this, &Application::extractionCompleted); connect(m_extractor, &Extractor::error, this, &Application::extractionError); connect(m_extractor, &Extractor::progress, this, &Application::extractionProgress); m_docsetRegistry = new Registry::DocsetRegistry(); connect(m_settings, &Settings::updated, this, &Application::applySettings); applySettings(); m_mainWindow = new WidgetUi::MainWindow(this); } Application::~Application() { m_extractorThread->quit(); m_extractorThread->wait(); delete m_extractor; delete m_mainWindow; delete m_docsetRegistry; } /*! * \internal * \brief Returns a pointer to the Core::Application instance. * \return A pointer or \c nullptr, if no instance has been created. */ Application *Application::instance() { return m_instance; } WidgetUi::MainWindow *Application::mainWindow() const { return m_mainWindow; } void Application::showMainWindow(bool forceMinimized) { if (m_mainWindow->isVisible()) { return; } if (forceMinimized || m_settings->startMinimized) { if (m_settings->showSystrayIcon && m_settings->minimizeToSystray) { return; } m_mainWindow->showMinimized(); } else { m_mainWindow->show(); } } QNetworkAccessManager *Application::networkManager() const { return m_networkManager; } Settings *Application::settings() const { return m_settings; } Registry::DocsetRegistry *Application::docsetRegistry() { return m_docsetRegistry; } FileManager *Application::fileManager() const { return m_fileManager; } HttpServer *Application::httpServer() const { return m_httpServer; } QString Application::cacheLocation() { #ifndef PORTABLE_BUILD return QStandardPaths::writableLocation(QStandardPaths::CacheLocation); #else return QCoreApplication::applicationDirPath() + QLatin1String("/cache"); #endif } QString Application::configLocation() { #ifndef PORTABLE_BUILD // TODO: Replace 'Zeal/Zeal' with 'zeal'. return QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); #else return QCoreApplication::applicationDirPath() + QLatin1String("/config"); #endif } QVersionNumber Application::version() { static const auto vn = QVersionNumber::fromString(QCoreApplication::applicationVersion()); return vn; } QString Application::versionString() { static const auto v = QStringLiteral("v%1").arg(QCoreApplication::applicationVersion()); return v; } void Application::executeQuery(const Registry::SearchQuery &query, bool preventActivation) { m_mainWindow->search(query); if (preventActivation) return; m_mainWindow->bringToFront(); } void Application::extract(const QString &filePath, const QString &destination, const QString &root) { QMetaObject::invokeMethod(m_extractor, "extract", Qt::QueuedConnection, Q_ARG(QString, filePath), Q_ARG(QString, destination), Q_ARG(QString, root)); } QNetworkReply *Application::download(const QUrl &url) { static const QString ua = userAgent(); static const QByteArray uaJson = userAgentJson().toUtf8(); QNetworkRequest request(url); request.setHeader(QNetworkRequest::UserAgentHeader, ua); if (url.host().endsWith(QLatin1String(".zealdocs.org", Qt::CaseInsensitive))) request.setRawHeader("X-Zeal-User-Agent", uaJson); return m_networkManager->get(request); } /*! \internal Performs a check whether a new Zeal version is available. Setting \a quiet to true suppresses error and "you are using the latest version" message boxes. */ void Application::checkForUpdates(bool quiet) { QNetworkReply *reply = download(QUrl(ReleasesApiUrl)); connect(reply, &QNetworkReply::finished, this, [this, quiet]() { QScopedPointer reply( qobject_cast(sender())); if (reply->error() != QNetworkReply::NoError) { if (!quiet) emit updateCheckError(reply->errorString()); return; } QJsonParseError jsonError; const QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { if (!quiet) emit updateCheckError(jsonError.errorString()); return; } const QJsonObject versionInfo = jsonDoc.array().first().toObject(); // Latest is the first. const auto latestVersion = QVersionNumber::fromString(versionInfo[QLatin1String("version")].toString()); if (latestVersion > version()) { emit updateCheckDone(latestVersion.toString()); } else if (!quiet) { emit updateCheckDone(); } }); } void Application::applySettings() { m_docsetRegistry->setFuzzySearchEnabled(m_settings->isFuzzySearchEnabled); m_docsetRegistry->setStoragePath(m_settings->docsetPath); // HTTP Proxy Settings switch (m_settings->proxyType) { case Settings::ProxyType::None: QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); break; case Settings::ProxyType::System: QNetworkProxyFactory::setUseSystemConfiguration(true); break; case Settings::ProxyType::Http: case Settings::ProxyType::Socks5: { const QNetworkProxy::ProxyType type = m_settings->proxyType == Settings::ProxyType::Socks5 ? QNetworkProxy::Socks5Proxy : QNetworkProxy::HttpProxy; QNetworkProxy proxy(type, m_settings->proxyHost, m_settings->proxyPort); if (m_settings->proxyAuthenticate) { proxy.setUser(m_settings->proxyUserName); proxy.setPassword(m_settings->proxyPassword); } QNetworkProxy::setApplicationProxy(proxy); break; } } // Force NM to pick up changes. m_networkManager->clearAccessCache(); } QString Application::userAgent() { return QStringLiteral("Zeal/%1").arg(QCoreApplication::applicationVersion()); } QString Application::userAgentJson() const { QJsonObject app = { {QStringLiteral("version"), QCoreApplication::applicationVersion()}, {QStringLiteral("qt_version"), qVersion()}, {QStringLiteral("install_id"), m_settings->installId} }; QJsonObject os = { {QStringLiteral("arch"), QSysInfo::currentCpuArchitecture()}, {QStringLiteral("name"), QSysInfo::prettyProductName()}, {QStringLiteral("product_type"), QSysInfo::productType()}, {QStringLiteral("product_version"), QSysInfo::productVersion()}, {QStringLiteral("kernel_type"), QSysInfo::kernelType()}, {QStringLiteral("kernel_version"), QSysInfo::kernelVersion()}, {QStringLiteral("locale"), QLocale::system().name()} }; QJsonObject ua = { {QStringLiteral("app"), app}, {QStringLiteral("os"), os} }; return QString::fromUtf8(QJsonDocument(ua).toJson(QJsonDocument::Compact)); } ================================================ FILE: src/libs/core/application.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_CORE_APPLICATION_H #define ZEAL_CORE_APPLICATION_H #include #include class QNetworkAccessManager; class QNetworkReply; class QThread; namespace Zeal { namespace Registry { class DocsetRegistry; class SearchQuery; } // namespace Registry namespace WidgetUi { class MainWindow; } // namespace WidgetUi namespace Core { class Extractor; class FileManager; class HttpServer; class Settings; class Application final : public QObject { Q_OBJECT Q_DISABLE_COPY_MOVE(Application) public: explicit Application(QObject *parent = nullptr); ~Application() override; static Application *instance(); WidgetUi::MainWindow *mainWindow() const; void showMainWindow(bool forceMinimized = false); QNetworkAccessManager *networkManager() const; Settings *settings() const; Registry::DocsetRegistry *docsetRegistry(); FileManager *fileManager() const; HttpServer *httpServer() const; static QString cacheLocation(); static QString configLocation(); static QVersionNumber version(); static QString versionString(); public slots: void executeQuery(const Registry::SearchQuery &query, bool preventActivation); void extract(const QString &filePath, const QString &destination, const QString &root = QString()); QNetworkReply *download(const QUrl &url); void checkForUpdates(bool quiet = false); signals: void extractionCompleted(const QString &filePath); void extractionError(const QString &filePath, const QString &errorString); void extractionProgress(const QString &filePath, qint64 extracted, qint64 total); void updateCheckDone(const QString &version = QString()); void updateCheckError(const QString &message); private slots: void applySettings(); private: static inline QString userAgent(); QString userAgentJson() const; static Application *m_instance; Settings *m_settings = nullptr; QNetworkAccessManager *m_networkManager = nullptr; FileManager *m_fileManager = nullptr; HttpServer *m_httpServer = nullptr; QThread *m_extractorThread = nullptr; Extractor *m_extractor = nullptr; Registry::DocsetRegistry *m_docsetRegistry = nullptr; WidgetUi::MainWindow *m_mainWindow = nullptr; }; } // namespace Core } // namespace Zeal #endif // ZEAL_CORE_APPLICATION_H ================================================ FILE: src/libs/core/applicationsingleton.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "applicationsingleton.h" #include #include #include #include #include #include #include #include using namespace Zeal::Core; static Q_LOGGING_CATEGORY(log, "zeal.core.applicationsingleton") struct SharedData { qint64 primaryPid; }; ApplicationSingleton::ApplicationSingleton(QObject *parent) : QObject(parent) { if (QCoreApplication::instance() == nullptr) { #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) qCFatal(log, "QCoreApplication (or derived type) must be created before ApplicationSingleton."); #else qFatal("QCoreApplication (or derived type) must be created before ApplicationSingleton."); #endif } m_id = computeId(); qCDebug(log, "Singleton ID: %s", qPrintable(m_id)); m_sharedMemory = new QSharedMemory(m_id, this); m_isPrimary = m_sharedMemory->create(sizeof(SharedData)); if (m_isPrimary) { setupPrimary(); return; } #ifdef Q_OS_UNIX // Verify it's not a segment that survived an application crash. m_sharedMemory->attach(); m_sharedMemory->detach(); m_isPrimary = m_sharedMemory->create(sizeof(SharedData)); if (m_isPrimary) { setupPrimary(); return; } #endif if (!m_sharedMemory->attach(QSharedMemory::ReadOnly)) { qCWarning(log) << "Cannot attach to the shared memory segment:" << m_sharedMemory->errorString(); return; } setupSecondary(); } bool ApplicationSingleton::isPrimary() const { return m_isPrimary; } bool ApplicationSingleton::isSecondary() const { return !m_isPrimary; } qint64 ApplicationSingleton::primaryPid() const { return m_primaryPid; } bool ApplicationSingleton::sendMessage(QByteArray &data, int timeout) { // No support for primary to secondary communication. if (m_isPrimary) { return false; } QScopedPointer socket(new QLocalSocket); socket->connectToServer(m_id); if (!socket->waitForConnected(timeout)) { qCWarning(log) << "Cannot connect to the local service:" << socket->errorString(); return false; } socket->write(data); socket->flush(); // Required for Linux. return socket->waitForBytesWritten(timeout); } void ApplicationSingleton::setupPrimary() { m_primaryPid = QCoreApplication::applicationPid(); qCInfo(log, "Starting as a primary instance. (PID: %lld)", m_primaryPid); m_sharedMemory->lock(); auto sd = static_cast(m_sharedMemory->data()); sd->primaryPid = m_primaryPid; m_sharedMemory->unlock(); QLocalServer::removeServer(m_id); m_localServer = new QLocalServer(this); m_localServer->setSocketOptions(QLocalServer::UserAccessOption); connect(m_localServer, &QLocalServer::newConnection, this, [this] { QLocalSocket *socket = m_localServer->nextPendingConnection(); connect(socket, &QLocalSocket::readyRead, this, [this, socket] { QByteArray data = socket->readAll(); emit messageReceived(data); socket->deleteLater(); }); }); if (!m_localServer->listen(m_id)) { qCWarning(log) << "Cannot start the local service:" << m_localServer->errorString(); return; } } void ApplicationSingleton::setupSecondary() { m_sharedMemory->lock(); auto sd = static_cast(m_sharedMemory->data()); m_primaryPid = sd->primaryPid; m_sharedMemory->unlock(); qCInfo(log, "Starting as a secondary instance. (Primary PID: %lld)", m_primaryPid); } QString ApplicationSingleton::computeId() { // Make sure the result can be used as a name for the local socket. static const QByteArray::Base64Options base64Options = QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals; QCryptographicHash hash(QCryptographicHash::Sha256); hash.addData(QCoreApplication::applicationName().toUtf8()); hash.addData(QCoreApplication::organizationName().toUtf8()); hash.addData(QCoreApplication::organizationDomain().toUtf8()); // Support multi-user setup. hash.addData(QDir::homePath().toUtf8()); return QString::fromLatin1(hash.result().toBase64(base64Options)); } ================================================ FILE: src/libs/core/applicationsingleton.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_CORE_APPLICATIONSINGLETON_H #define ZEAL_CORE_APPLICATIONSINGLETON_H #include class QLocalServer; class QSharedMemory; namespace Zeal { namespace Core { class ApplicationSingleton final : public QObject { Q_OBJECT Q_DISABLE_COPY_MOVE(ApplicationSingleton) public: explicit ApplicationSingleton(QObject *parent = nullptr); bool isPrimary() const; bool isSecondary() const; qint64 primaryPid() const; bool sendMessage(QByteArray &data, int timeout = 500); signals: void messageReceived(const QByteArray &data); private: void setupPrimary(); void setupSecondary(); static QString computeId(); QString m_id; bool m_isPrimary = false; qint64 m_primaryPid = 0; QSharedMemory *m_sharedMemory = nullptr; QLocalServer *m_localServer = nullptr; }; } // namespace Core } // namespace Zeal #endif // ZEAL_CORE_APPLICATIONSINGLETON_H ================================================ FILE: src/libs/core/extractor.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "extractor.h" #include #include #include #include #include using namespace Zeal::Core; static Q_LOGGING_CATEGORY(log, "zeal.core.extractor") Extractor::Extractor(QObject *parent) : QObject(parent) { } void Extractor::extract(const QString &sourceFile, const QString &destination, const QString &root) { ExtractInfo info = { archive_read_new(), // archiveHandle sourceFile, // filePath QFileInfo(sourceFile).size(), // totalBytes 0 // extractedBytes }; archive_read_support_filter_all(info.archiveHandle); archive_read_support_format_all(info.archiveHandle); int r = archive_read_open_filename(info.archiveHandle, qPrintable(sourceFile), 10240); if (r) { emit error(sourceFile, QString::fromLocal8Bit(archive_error_string(info.archiveHandle))); archive_read_free(info.archiveHandle); return; } QDir destinationDir(destination); if (!root.isEmpty()) { destinationDir.setPath(destinationDir.filePath(root)); } // Destination directory must be created before any other files. destinationDir.mkpath(QLatin1String(".")); // TODO: Do not strip root directory in archive if it equals to 'root' archive_entry *entry; while (archive_read_next_header(info.archiveHandle, &entry) == ARCHIVE_OK) { // See https://github.com/libarchive/libarchive/issues/587 for more on UTF-8. QString pathname = QString::fromUtf8(archive_entry_pathname_utf8(entry)); if (!root.isEmpty()) { pathname.remove(0, pathname.indexOf(QLatin1String("/")) + 1); } const QString filePath = destinationDir.absoluteFilePath(pathname); const auto filetype = archive_entry_filetype(entry); if (filetype == S_IFDIR) { QDir().mkpath(QFileInfo(filePath).absolutePath()); continue; } if (filetype != S_IFREG) { qCWarning(log, "Unsupported filetype %d at '%s'.", filetype, qPrintable(pathname)); continue; } QScopedPointer file(new QFile(filePath)); if (!file->open(QIODevice::WriteOnly)) { qCWarning(log, "Cannot open file for writing at '%s'.", qPrintable(pathname)); continue; } const void *buffer; size_t size; std::int64_t offset; for (;;) { int rc = archive_read_data_block(info.archiveHandle, &buffer, &size, &offset); if (rc != ARCHIVE_OK) { if (rc == ARCHIVE_EOF) { break; } qCWarning(log, "Cannot read from archive: %s.", archive_error_string(info.archiveHandle)); emit error(sourceFile, QString::fromLocal8Bit(archive_error_string(info.archiveHandle))); archive_read_free(info.archiveHandle); return; } file->write(static_cast(buffer), size); } emitProgress(info); } emit completed(sourceFile); archive_read_free(info.archiveHandle); } void Extractor::emitProgress(ExtractInfo &info) { const qint64 extractedBytes = archive_filter_bytes(info.archiveHandle, -1); if (extractedBytes == info.extractedBytes) { return; } info.extractedBytes = extractedBytes; emit progress(info.filePath, extractedBytes, info.totalBytes); } ================================================ FILE: src/libs/core/extractor.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_CORE_EXTRACTOR_H #define ZEAL_CORE_EXTRACTOR_H #include struct archive; namespace Zeal { namespace Core { class Extractor final : public QObject { Q_OBJECT Q_DISABLE_COPY_MOVE(Extractor) public: explicit Extractor(QObject *parent = nullptr); public slots: void extract(const QString &sourceFile, const QString &destination, const QString &root = QString()); signals: void error(const QString &filePath, const QString &message); void completed(const QString &filePath); void progress(const QString &filePath, qint64 extracted, qint64 total); private: struct ExtractInfo { archive *archiveHandle; QString filePath; qint64 totalBytes; qint64 extractedBytes; }; void emitProgress(ExtractInfo &info); }; } // namespace Core } // namespace Zeal #endif // ZEAL_CORE_EXTRACTOR_H ================================================ FILE: src/libs/core/filemanager.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "filemanager.h" #include "application.h" #include #include #include #include #include #include #include using namespace Zeal::Core; static Q_LOGGING_CATEGORY(log, "zeal.core.filemanager") FileManager::FileManager(QObject *parent) : QObject(parent) { // Ensure that cache location exists. if (!QDir().mkpath(Application::cacheLocation())) { qCWarning(log, "Failed to create cache directory '%s'.", qPrintable(Application::cacheLocation())); } } bool FileManager::removeRecursively(const QString &path) { qCDebug(log, "Removing '%s'...", qPrintable(path)); if (!QFileInfo(path).isDir()) { qCWarning(log, "'%s' is not a directory.", qPrintable(path)); return false; } const QString deletePath = QStringLiteral("%1.%2.deleteme") .arg(path, QString::number(QDateTime::currentMSecsSinceEpoch())); if (!QDir().rename(path, deletePath)) { qCWarning(log, "Failed to rename '%s' to '%s'.", qPrintable(path), qPrintable(deletePath)); return false; } qCDebug(log, "Renamed '%s' to '%s'.", qPrintable(path), qPrintable(deletePath)); std::future f = std::async(std::launch::async, [deletePath](){ return QDir(deletePath).removeRecursively(); }); f.wait(); if (!f.get()) { qCWarning(log, "Failed to remove '%s'.", qPrintable(deletePath)); } else { qCDebug(log, "Removed '%s'.", qPrintable(deletePath)); } return true; } ================================================ FILE: src/libs/core/filemanager.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_CORE_FILEMANAGER_H #define ZEAL_CORE_FILEMANAGER_H #include namespace Zeal { namespace Core { class FileManager final : public QObject { Q_OBJECT Q_DISABLE_COPY_MOVE(FileManager) public: explicit FileManager(QObject *parent = nullptr); bool removeRecursively(const QString &path); }; } // namespace Core } // namespace Zeal #endif // ZEAL_CORE_FILEMANAGER_H ================================================ FILE: src/libs/core/httpserver.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "httpserver.h" #include "application.h" #include #include #include using namespace Zeal::Core; namespace { constexpr char LocalHttpServerHost[] = "127.0.0.1"; // macOS only routes 127.0.0.1 by default. } // namespace static Q_LOGGING_CATEGORY(log, "zeal.core.httpserver") HttpServer::HttpServer(QObject *parent) : QObject(parent) { m_server = std::make_unique(); const int port = m_server->bind_to_any_port(LocalHttpServerHost); m_baseUrl.setScheme(QStringLiteral("http")); m_baseUrl.setHost(LocalHttpServerHost); m_baseUrl.setPort(port); m_server->set_error_handler([](const auto& req, auto& res) { const QString html = QStringLiteral("ERROR %1
Request path: %2
") .arg(res.status) .arg(QString::fromStdString(req.path)); res.set_content(html.toUtf8().data(), "text/html"); }); m_future = std::async(std::launch::async, &httplib::Server::listen_after_bind, m_server.get()); qCDebug(log, "Listening on %s...", qPrintable(m_baseUrl.toString())); } HttpServer::~HttpServer() { m_server->stop(); auto status = m_future.wait_for(std::chrono::seconds(2)); if (status != std::future_status::ready) { qCWarning(log) << "Failed to stop server within timeout."; } } QUrl HttpServer::baseUrl() const { return m_baseUrl; } QUrl HttpServer::mount(const QString &prefix, const QString &path) { const QString pfx = sanitizePrefix(prefix); const bool ok = m_server->set_mount_point(pfx.toStdString(), path.toStdString()); if (!ok) { qCWarning(log, "Failed to mount '%s' to '%s'.", qPrintable(path), qPrintable(pfx)); return QUrl(); } qCDebug(log, "Mounted '%s' to '%s'.", qPrintable(path), qPrintable(pfx)); QUrl mountUrl = m_baseUrl; mountUrl.setPath(m_baseUrl.path() + pfx); return mountUrl; } bool HttpServer::unmount(const QString &prefix) { const QString pfx = sanitizePrefix(prefix); const bool ok = m_server->remove_mount_point(pfx.toStdString()); if (!ok) { qCWarning(log, "Failed to unmount '%s' to '%s'.", qPrintable(prefix), qPrintable(pfx)); } qCDebug(log, "Unmounted prefix '%s' ('%s').", qPrintable(prefix), qPrintable(pfx)); return ok; } QString HttpServer::sanitizePrefix(const QString &prefix) { QString pfx = (prefix.startsWith(QLatin1String("/")) ? prefix.right(1) : prefix).toLower(); pfx.replace(QRegularExpression(QStringLiteral("[^a-zA-Z0-9-_]")), QStringLiteral("_")); pfx.prepend(QLatin1Char('/')); return pfx; } ================================================ FILE: src/libs/core/httpserver.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_CORE_HTTPSERVER_H #define ZEAL_CORE_HTTPSERVER_H #include #include #include #include namespace httplib { class Server; } // namespace httplib namespace Zeal { namespace Core { class HttpServer : public QObject { Q_OBJECT Q_DISABLE_COPY_MOVE(HttpServer) public: explicit HttpServer(QObject *parent = nullptr); ~HttpServer() override; QUrl baseUrl() const; QUrl mount(const QString &prefix, const QString &path); bool unmount(const QString &prefix); private: static QString sanitizePrefix(const QString &prefix); std::unique_ptr m_server; std::future m_future; QUrl m_baseUrl; }; } // namespace Core } // namespace Zeal #endif // ZEAL_CORE_HTTPSERVER_H ================================================ FILE: src/libs/core/networkaccessmanager.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "networkaccessmanager.h" #include "application.h" #include "httpserver.h" #include using namespace Zeal::Core; NetworkAccessManager::NetworkAccessManager(QObject *parent) : QNetworkAccessManager(parent) { } bool NetworkAccessManager::isLocalFile(const QUrl &url) { return url.isLocalFile() || url.scheme() == QLatin1String("qrc"); } bool NetworkAccessManager::isLocalUrl(const QUrl &url) { if (isLocalFile(url)) { return true; } const QUrl &baseUrl = Application::instance()->httpServer()->baseUrl(); if (baseUrl.isParentOf(url)) { return true; } return false; } QNetworkReply *NetworkAccessManager::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) { QNetworkRequest overrideRequest(request); overrideRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, true); // Forward all non-local schemaless URLs via HTTPS. const QUrl url = request.url(); if (isLocalFile(url) && !url.host().isEmpty()) { QUrl overrideUrl(url); overrideUrl.setScheme(QStringLiteral("https")); overrideRequest.setUrl(overrideUrl); op = QNetworkAccessManager::GetOperation; } QSslConfiguration sslConfig = overrideRequest.sslConfiguration(); sslConfig.setCaCertificates(QSslConfiguration::systemCaCertificates()); overrideRequest.setSslConfiguration(sslConfig); return QNetworkAccessManager::createRequest(op, overrideRequest, outgoingData); } ================================================ FILE: src/libs/core/networkaccessmanager.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_CORE_NETWORKACCESSMANAGER_H #define ZEAL_CORE_NETWORKACCESSMANAGER_H #include namespace Zeal { namespace Core { class NetworkAccessManager final : public QNetworkAccessManager { Q_OBJECT Q_DISABLE_COPY_MOVE(NetworkAccessManager) public: NetworkAccessManager(QObject *parent = nullptr); static bool isLocalFile(const QUrl &url); static bool isLocalUrl(const QUrl &url); protected: QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData = nullptr) override; }; } // namespace Core } // namespace Zeal #endif // ZEAL_CORE_NETWORKACCESSMANAGER_H ================================================ FILE: src/libs/core/settings.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "settings.h" #include "application.h" #include #include #include #include #include #include #include #include #include #include #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) #include #include #include #else #include #endif namespace { // Configuration file groups constexpr char GroupUI[] = "ui"; constexpr char GroupContent[] = "content"; constexpr char GroupDocsets[] = "docsets"; constexpr char GroupGlobalShortcuts[] = "global_shortcuts"; constexpr char GroupSearch[] = "search"; constexpr char GroupTabs[] = "tabs"; constexpr char GroupInternal[] = "internal"; constexpr char GroupState[] = "state"; constexpr char GroupProxy[] = "proxy"; } // namespace using namespace Zeal::Core; static Q_LOGGING_CATEGORY(log, "zeal.core.settings") Settings::Settings(QObject *parent) : QObject(parent) { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) qRegisterMetaTypeStreamOperators("ContentAppearance"); qRegisterMetaTypeStreamOperators("ExternalLinkPolicy"); #else qRegisterMetaType("ContentAppearance"); qRegisterMetaType("ExternalLinkPolicy"); #endif #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) // When the OS color scheme changes, reapply the color scheme. connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, this, [this]() { if (contentAppearance == ContentAppearance::Automatic) { applyColorScheme(); } }); #endif load(); } Settings::~Settings() { save(); } bool Settings::isDarkModeEnabled() const { if (contentAppearance == ContentAppearance::Dark) { return true; } if (contentAppearance == ContentAppearance::Automatic && colorScheme() == ColorScheme::Dark) { return true; } return false; } #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) void Settings::applyColorScheme() { Qt::ColorScheme scheme = Qt::ColorScheme::Unknown; switch (contentAppearance) { case ContentAppearance::Light: scheme = Qt::ColorScheme::Light; break; case ContentAppearance::Dark: scheme = Qt::ColorScheme::Dark; break; default: break; } qApp->styleHints()->setColorScheme(scheme); // setColorScheme() alone doesn't reliably update existing widgets: // - Widgets with stylesheets (QStyleSheetStyle) only update on polish(). // - Direct palette consumers (QTreeView, QLineEdit) only update when the // application palette changes. // Re-instantiating the style triggers the full unpolish-polish cycle for // QStyleSheetStyle widgets and also updates the application palette via // the new style's standardPalette(). QStyle *newStyle = QStyleFactory::create(qApp->style()->name()); if (newStyle != nullptr) { qApp->setStyle(newStyle); } } #endif Settings::ColorScheme Settings::colorScheme() { #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) return QApplication::styleHints()->colorScheme(); #else // Pre Qt 6.5 detection from https://www.qt.io/blog/dark-mode-on-windows-11-with-qt-6.5. const QPalette p; if (p.color(QPalette::WindowText).lightness() > p.color(QPalette::Window).lightness()) { return ColorScheme::Dark; } return ColorScheme::Light; #endif } void Settings::load() { QScopedPointer settings(qsettings()); migrate(settings.data()); // TODO: Put everything in groups startMinimized = settings->value(QStringLiteral("start_minimized"), false).toBool(); checkForUpdate = settings->value(QStringLiteral("check_for_update"), true).toBool(); showSystrayIcon = settings->value(QStringLiteral("show_systray_icon"), true).toBool(); minimizeToSystray = settings->value(QStringLiteral("minimize_to_systray"), false).toBool(); hideOnClose = settings->value(QStringLiteral("hide_on_close"), false).toBool(); settings->beginGroup(GroupUI); hideMenuBar = settings->value(QStringLiteral("hide_menu_bar"), false).toBool(); settings->endGroup(); settings->beginGroup(GroupGlobalShortcuts); showShortcut = settings->value(QStringLiteral("show")).value(); settings->endGroup(); settings->beginGroup(GroupTabs); openNewTabAfterActive = settings->value(QStringLiteral("open_new_tab_after_active"), false).toBool(); settings->endGroup(); settings->beginGroup(GroupSearch); isFuzzySearchEnabled = settings->value(QStringLiteral("fuzzy_search_enabled"), false).toBool(); settings->endGroup(); settings->beginGroup(GroupContent); contentAppearance = settings->value(QStringLiteral("appearance"), QVariant::fromValue(ContentAppearance::Automatic)).value(); #if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) // Dark mode needs to be applied before Qt WebEngine is initialized. if (isDarkModeEnabled()) { qputenv("QTWEBENGINE_CHROMIUM_FLAGS", "--blink-settings=forceDarkModeEnabled=true,darkModeInversionAlgorithm=4"); } #endif #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) applyColorScheme(); #endif // Fonts QWebEngineSettings *webSettings = QWebEngineProfile::defaultProfile()->settings(); serifFontFamily = settings->value(QStringLiteral("serif_font_family"), webSettings->fontFamily(QWebEngineSettings::SerifFont)).toString(); sansSerifFontFamily = settings->value(QStringLiteral("sans_serif_font_family"), webSettings->fontFamily(QWebEngineSettings::SansSerifFont)).toString(); fixedFontFamily = settings->value(QStringLiteral("fixed_font_family"), webSettings->fontFamily(QWebEngineSettings::FixedFont)).toString(); static const QMap fontFamilies = { {QStringLiteral("sans-serif"), QWebEngineSettings::SansSerifFont}, {QStringLiteral("serif"), QWebEngineSettings::SerifFont}, {QStringLiteral("monospace"), QWebEngineSettings::FixedFont} }; defaultFontFamily = settings->value(QStringLiteral("default_font_family"), QStringLiteral("serif")).toString(); // Fallback to the serif font family. if (!fontFamilies.contains(defaultFontFamily)) { defaultFontFamily = QStringLiteral("serif"); } webSettings->setFontFamily(QWebEngineSettings::SansSerifFont, sansSerifFontFamily); webSettings->setFontFamily(QWebEngineSettings::SerifFont, serifFontFamily); webSettings->setFontFamily(QWebEngineSettings::FixedFont, fixedFontFamily); const QString defaultFontFamilyResolved = webSettings->fontFamily(fontFamilies.value(defaultFontFamily)); webSettings->setFontFamily(QWebEngineSettings::StandardFont, defaultFontFamilyResolved); defaultFontSize = settings->value(QStringLiteral("default_font_size"), webSettings->fontSize(QWebEngineSettings::DefaultFontSize)).toInt(); defaultFixedFontSize = settings->value(QStringLiteral("default_fixed_font_size"), webSettings->fontSize(QWebEngineSettings::DefaultFixedFontSize)).toInt(); minimumFontSize = settings->value(QStringLiteral("minimum_font_size"), webSettings->fontSize(QWebEngineSettings::MinimumFontSize)).toInt(); webSettings->setFontSize(QWebEngineSettings::DefaultFontSize, defaultFontSize); webSettings->setFontSize(QWebEngineSettings::DefaultFixedFontSize, defaultFixedFontSize); webSettings->setFontSize(QWebEngineSettings::MinimumFontSize, minimumFontSize); isHighlightOnNavigateEnabled = settings->value(QStringLiteral("highlight_on_navigate"), true).toBool(); customCssFile = settings->value(QStringLiteral("custom_css_file")).toString(); externalLinkPolicy = settings->value(QStringLiteral("external_link_policy"), QVariant::fromValue(ExternalLinkPolicy::Ask)).value(); isSmoothScrollingEnabled = settings->value(QStringLiteral("smooth_scrolling"), true).toBool(); settings->endGroup(); settings->beginGroup(GroupProxy); proxyType = static_cast(settings->value(QStringLiteral("type"), ProxyType::System).toUInt()); proxyHost = settings->value(QStringLiteral("host")).toString(); proxyPort = static_cast(settings->value(QStringLiteral("port"), 0).toUInt()); proxyAuthenticate = settings->value(QStringLiteral("authenticate"), false).toBool(); proxyUserName = settings->value(QStringLiteral("username")).toString(); proxyPassword = settings->value(QStringLiteral("password")).toString(); isIgnoreSslErrorsEnabled = settings->value(QStringLiteral("ignore_ssl_errors"), false).toBool(); settings->endGroup(); settings->beginGroup(GroupDocsets); if (settings->contains(QStringLiteral("path"))) { docsetPath = settings->value(QStringLiteral("path")).toString(); } else { #ifndef PORTABLE_BUILD docsetPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QLatin1String("/docsets"); #else docsetPath = QStringLiteral("docsets"); #endif } settings->endGroup(); // Create the docset storage directory if it doesn't exist. const QFileInfo fi(docsetPath); if (!fi.exists()) { const QString path = fi.isRelative() ? QCoreApplication::applicationDirPath() + QLatin1String("/") + docsetPath : docsetPath; if (!QDir().mkpath(path)) { qCWarning(log, "Failed to create docset storage directory '%s'.", qPrintable(path)); } } settings->beginGroup(GroupState); windowGeometry = settings->value(QStringLiteral("window_geometry")).toByteArray(); verticalSplitterGeometry = settings->value(QStringLiteral("splitter_geometry")).toByteArray(); tocSplitterState = settings->value(QStringLiteral("toc_splitter_state")).toByteArray(); settings->endGroup(); settings->beginGroup(GroupInternal); installId = settings->value(QStringLiteral("install_id"), // Avoid curly braces (QTBUG-885) QUuid::createUuid().toString().mid(1, 36)).toString(); settings->endGroup(); } void Settings::save() { QScopedPointer settings(qsettings()); // TODO: Put everything in groups settings->setValue(QStringLiteral("start_minimized"), startMinimized); settings->setValue(QStringLiteral("check_for_update"), checkForUpdate); settings->setValue(QStringLiteral("show_systray_icon"), showSystrayIcon); settings->setValue(QStringLiteral("minimize_to_systray"), minimizeToSystray); settings->setValue(QStringLiteral("hide_on_close"), hideOnClose); settings->beginGroup(GroupUI); settings->setValue(QStringLiteral("hide_menu_bar"), hideMenuBar); settings->endGroup(); settings->beginGroup(GroupGlobalShortcuts); settings->setValue(QStringLiteral("show"), showShortcut); settings->endGroup(); settings->beginGroup(GroupTabs); settings->setValue(QStringLiteral("open_new_tab_after_active"), openNewTabAfterActive); settings->endGroup(); settings->beginGroup(GroupSearch); settings->setValue(QStringLiteral("fuzzy_search_enabled"), isFuzzySearchEnabled); settings->endGroup(); settings->beginGroup(GroupContent); settings->setValue(QStringLiteral("default_font_family"), defaultFontFamily); settings->setValue(QStringLiteral("serif_font_family"), serifFontFamily); settings->setValue(QStringLiteral("sans_serif_font_family"), sansSerifFontFamily); settings->setValue(QStringLiteral("fixed_font_family"), fixedFontFamily); settings->setValue(QStringLiteral("default_font_size"), defaultFontSize); settings->setValue(QStringLiteral("default_fixed_font_size"), defaultFixedFontSize); settings->setValue(QStringLiteral("minimum_font_size"), minimumFontSize); settings->setValue(QStringLiteral("appearance"), QVariant::fromValue(contentAppearance)); settings->setValue(QStringLiteral("highlight_on_navigate"), isHighlightOnNavigateEnabled); settings->setValue(QStringLiteral("custom_css_file"), customCssFile); settings->setValue(QStringLiteral("external_link_policy"), QVariant::fromValue(externalLinkPolicy)); settings->setValue(QStringLiteral("smooth_scrolling"), isSmoothScrollingEnabled); settings->endGroup(); settings->beginGroup(GroupProxy); settings->setValue(QStringLiteral("type"), proxyType); settings->setValue(QStringLiteral("host"), proxyHost); settings->setValue(QStringLiteral("port"), proxyPort); settings->setValue(QStringLiteral("authenticate"), proxyAuthenticate); settings->setValue(QStringLiteral("username"), proxyUserName); settings->setValue(QStringLiteral("password"), proxyPassword); settings->setValue(QStringLiteral("ignore_ssl_errors"), isIgnoreSslErrorsEnabled); settings->endGroup(); settings->beginGroup(GroupDocsets); settings->setValue(QStringLiteral("path"), docsetPath); settings->endGroup(); settings->beginGroup(GroupState); settings->setValue(QStringLiteral("window_geometry"), windowGeometry); settings->setValue(QStringLiteral("splitter_geometry"), verticalSplitterGeometry); settings->setValue(QStringLiteral("toc_splitter_state"), tocSplitterState); settings->endGroup(); settings->beginGroup(GroupInternal); settings->setValue(QStringLiteral("install_id"), installId); // Version of configuration file format, should match Zeal version. Used for migration rules. settings->setValue(QStringLiteral("version"), Application::version().toString()); settings->endGroup(); settings->sync(); #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) applyColorScheme(); #endif emit updated(); } /*! * \internal * \brief Migrates settings from older application versions. * \param settings QSettings object to update. * * The settings migration process relies on 'internal/version' option, that was introduced in the * release 0.2.0, so a missing option indicates pre-0.2 release. */ void Settings::migrate(QSettings *settings) const { settings->beginGroup(GroupInternal); const auto version = QVersionNumber::fromString(settings->value(QStringLiteral("version")).toString()); settings->endGroup(); // // 0.6.0 // // Unset content.default_fixed_font_size. // The causing bug was 0.6.1 (#903), but the incorrect setting still comes to haunt us (#1054). if (version == QVersionNumber(0, 6, 0)) { settings->beginGroup(GroupContent); settings->remove(QStringLiteral("default_fixed_font_size")); settings->endGroup(); } // // Pre 0.4 // // Rename 'browser' group into 'content'. if (version < QVersionNumber(0, 4, 0)) { settings->beginGroup(QStringLiteral("browser")); const QVariant tmpMinimumFontSize = settings->value(QStringLiteral("minimum_font_size")); settings->endGroup(); settings->remove(QStringLiteral("browser")); if (tmpMinimumFontSize.isValid()) { settings->beginGroup(GroupContent); settings->setValue(QStringLiteral("minimum_font_size"), tmpMinimumFontSize); settings->endGroup(); } } // // Pre 0.3 // // Unset 'state/splitter_geometry', because custom styles were removed. if (version < QVersionNumber(0, 3, 0)) { settings->beginGroup(GroupState); settings->remove(QStringLiteral("splitter_geometry")); settings->endGroup(); } } /*! * \internal * \brief Returns an initialized QSettings object. * \param parent Optional parent object. * \return QSettings object. * * QSettings is initialized according to build options, e.g. standard vs portable. * Caller is responsible for deleting the returned object. */ QSettings *Settings::qsettings(QObject *parent) { #ifndef PORTABLE_BUILD return new QSettings(parent); #else return new QSettings(QCoreApplication::applicationDirPath() + QLatin1String("/zeal.ini"), QSettings::IniFormat, parent); #endif } QDataStream &operator<<(QDataStream &out, Settings::ContentAppearance policy) { out << static_cast>(policy); return out; } QDataStream &operator>>(QDataStream &in, Settings::ContentAppearance &policy) { std::underlying_type_t value; in >> value; policy = static_cast(value); return in; } QDataStream &operator<<(QDataStream &out, Settings::ExternalLinkPolicy policy) { out << static_cast>(policy); return out; } QDataStream &operator>>(QDataStream &in, Settings::ExternalLinkPolicy &policy) { std::underlying_type_t value; in >> value; policy = static_cast(value); return in; } ================================================ FILE: src/libs/core/settings.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_CORE_SETTINGS_H #define ZEAL_CORE_SETTINGS_H #include #include #include class QSettings; namespace Zeal { namespace Core { class Settings final : public QObject { Q_OBJECT Q_DISABLE_COPY_MOVE(Settings) public: /* This public members are here just for simplification and should go away * once a more advanced settings management come in place. */ // Startup bool startMinimized; bool checkForUpdate; bool hideMenuBar; // TODO: bool restoreLastState; // System Tray bool showSystrayIcon; bool minimizeToSystray; bool hideOnClose; // Global Shortcuts QKeySequence showShortcut; // TODO: QKeySequence searchSelectedTextShortcut; // Tabs Behavior bool openNewTabAfterActive; // Search bool isFuzzySearchEnabled; // Content QString defaultFontFamily; QString serifFontFamily; QString sansSerifFontFamily; QString fixedFontFamily; int defaultFontSize; int defaultFixedFontSize; int minimumFontSize; enum class ExternalLinkPolicy : unsigned int { Ask = 0, Open, OpenInSystemBrowser }; Q_ENUM(ExternalLinkPolicy) ExternalLinkPolicy externalLinkPolicy = ExternalLinkPolicy::Ask; enum class ContentAppearance : unsigned int { Automatic = 0, Light, Dark }; Q_ENUM(ContentAppearance) ContentAppearance contentAppearance = ContentAppearance::Automatic; bool isHighlightOnNavigateEnabled; QString customCssFile; bool isSmoothScrollingEnabled; // Network enum ProxyType : unsigned int { None = 0, System = 1, Http = 3, Socks5 = 4 }; Q_ENUM(ProxyType) // Internal // -------- // InstallId is a UUID used to identify a Zeal installation. Created on first start or after // a settings wipe. It is not attached to user hardware or software, and is sent exclusively // to *.zealdocs.org hosts. QString installId; ProxyType proxyType = ProxyType::System; QString proxyHost; quint16 proxyPort; bool proxyAuthenticate; QString proxyUserName; QString proxyPassword; bool isIgnoreSslErrorsEnabled; // Other QString docsetPath; // State QByteArray windowGeometry; QByteArray verticalSplitterGeometry; QByteArray tocSplitterState; explicit Settings(QObject *parent = nullptr); ~Settings() override; // Helper functions. bool isDarkModeEnabled() const; #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) typedef Qt::ColorScheme ColorScheme; #else enum class ColorScheme { Unknown, Light, Dark, }; #endif static ColorScheme colorScheme(); public slots: void load(); void save(); signals: void updated(); private: void migrate(QSettings *settings) const; #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) void applyColorScheme(); #endif static QSettings *qsettings(QObject *parent = nullptr); }; } // namespace Core } // namespace Zeal QDataStream &operator<<(QDataStream &out, Zeal::Core::Settings::ContentAppearance policy); QDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::ContentAppearance &policy); QDataStream &operator<<(QDataStream &out, Zeal::Core::Settings::ExternalLinkPolicy policy); QDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::ExternalLinkPolicy &policy); Q_DECLARE_METATYPE(Zeal::Core::Settings::ContentAppearance) Q_DECLARE_METATYPE(Zeal::Core::Settings::ExternalLinkPolicy) #endif // ZEAL_CORE_SETTINGS_H ================================================ FILE: src/libs/registry/CMakeLists.txt ================================================ add_library(Registry STATIC docset.cpp docsetmetadata.cpp docsetregistry.cpp listmodel.cpp searchmodel.cpp searchquery.cpp # Show headers without .cpp in Qt Creator. cancellationtoken.h itemdatarole.h searchresult.h ) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Concurrent Gui Network REQUIRED) target_link_libraries(Registry PRIVATE Util Qt${QT_VERSION_MAJOR}::Concurrent Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Network) ================================================ FILE: src/libs/registry/cancellationtoken.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2015 Artur Spychaj // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_REGISTRY_CANCELLATIONTOKEN_H #define ZEAL_REGISTRY_CANCELLATIONTOKEN_H #include namespace Zeal { namespace Registry { /// Token that stores whether cancel was called on it. /// In async code can be used to check if another thread called cancel. class CancellationToken { public: inline bool isCanceled() const { return m_canceled; } inline void cancel() { m_canceled = true; } inline void reset() { m_canceled = false; } private: std::atomic_bool m_canceled; }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_CANCELLATIONTOKEN_H ================================================ FILE: src/libs/registry/docset.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "docset.h" #include "cancellationtoken.h" #include "searchresult.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Zeal::Registry; static Q_LOGGING_CATEGORY(log, "zeal.registry.docset") namespace { constexpr char IndexNamePrefix[] = "__zi_name"; // zi - Zeal index constexpr char IndexNameVersion[] = "0001"; // Current index version constexpr char NotFoundPageUrl[] = "qrc:///browser/404.html"; namespace InfoPlist { constexpr char CFBundleName[] = "CFBundleName"; //const char CFBundleIdentifier[] = "CFBundleIdentifier"; constexpr char DashDocSetFamily[] = "DashDocSetFamily"; constexpr char DashDocSetKeyword[] = "DashDocSetKeyword"; constexpr char DashDocSetPluginKeyword[] = "DashDocSetPluginKeyword"; constexpr char DashIndexFilePath[] = "dashIndexFilePath"; constexpr char DocSetPlatformFamily[] = "DocSetPlatformFamily"; //const char IsDashDocset[] = "isDashDocset"; constexpr char IsJavaScriptEnabled[] = "isJavaScriptEnabled"; } // namespace InfoPlist } // namespace static void sqliteScoreFunction(sqlite3_context *context, int argc, sqlite3_value **argv); Docset::Docset(QString path) : m_path(std::move(path)) { QDir dir(m_path); if (!dir.exists()) return; loadMetadata(); // Attempt to find the icon in any supported format const auto iconFiles = dir.entryList({QStringLiteral("icon.*")}, QDir::Files); for (const QString &iconFile : iconFiles) { m_icon = QIcon(dir.filePath(iconFile)); if (!m_icon.availableSizes().isEmpty()) break; } if (!dir.cd(QStringLiteral("Contents"))) { qCWarning(log, "[%s] Cannot change directory into 'Contents' at '%s'.", qPrintable(m_name), qPrintable(m_path)); return; } // TODO: 'info.plist' is invalid according to Apple, and must always be 'Info.plist' // https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPRuntimeConfig // /Articles/ConfigFiles.html Util::Plist plist; if (dir.exists(QStringLiteral("Info.plist"))) plist.read(dir.filePath(QStringLiteral("Info.plist"))); else if (dir.exists(QStringLiteral("info.plist"))) plist.read(dir.filePath(QStringLiteral("info.plist"))); else { qCWarning(log, "Cannot find file 'Info.plist' or 'info.plist' for docset at '%s'.", qPrintable(m_path)); return; } if (plist.hasError()) { qCWarning(log, "Failed to parse 'Info.plist' for docset at '%s'.", qPrintable(m_path)); return; } if (m_name.isEmpty()) { // Fallback if meta.json is absent if (plist.contains(InfoPlist::CFBundleName)) { m_name = m_title = plist[InfoPlist::CFBundleName].toString(); // TODO: Remove when MainWindow::docsetName() will not use directory name m_name.replace(QLatin1Char(' '), QLatin1Char('_')); } else { m_name = QFileInfo(m_path).fileName().remove(QStringLiteral(".docset")); } } if (m_title.isEmpty()) { m_title = m_name; m_title.replace(QLatin1Char('_'), QLatin1Char(' ')); } // TODO: Verify if this is needed if (plist.contains(InfoPlist::DashDocSetFamily) && plist[InfoPlist::DashDocSetFamily].toString() == QLatin1String("cheatsheet")) { m_name = m_name + QLatin1String("cheats"); } if (!dir.cd(QStringLiteral("Resources"))) { qCWarning(log, "[%s] Cannot change directory into 'Resources' at '%s'.", qPrintable(m_name), qPrintable(m_path)); return; } if (!dir.exists(QStringLiteral("docSet.dsidx"))) { qCWarning(log, "[%s] Cannot access 'docSet.dsidx' at '%s'.", qPrintable(m_name), qPrintable(m_path)); return; } m_db = new Util::SQLiteDatabase(dir.filePath(QStringLiteral("docSet.dsidx"))); if (!m_db->isOpen()) { qCWarning(log, "[%s] Cannot open database: %s.", qPrintable(m_name), qPrintable(m_db->lastError())); return; } sqlite3_create_function(m_db->handle(), "zealScore", 2, SQLITE_UTF8, nullptr, sqliteScoreFunction, nullptr, nullptr); m_type = m_db->tables().contains(QStringLiteral("searchIndex"), Qt::CaseInsensitive) ? Type::Dash : Type::ZDash; createIndex(); if (m_type == Docset::Type::ZDash) { createView(); } if (!dir.cd(QStringLiteral("Documents"))) { qCWarning(log, "[%s] Cannot change directory into 'Documents' at '%s'.", qPrintable(m_name), qPrintable(m_path)); m_type = Type::Invalid; return; } // Setup keywords if (plist.contains(InfoPlist::DocSetPlatformFamily)) m_keywords << plist[InfoPlist::DocSetPlatformFamily].toString(); if (plist.contains(InfoPlist::DashDocSetPluginKeyword)) m_keywords << plist[InfoPlist::DashDocSetPluginKeyword].toString(); if (plist.contains(InfoPlist::DashDocSetKeyword)) m_keywords << plist[InfoPlist::DashDocSetKeyword].toString(); if (plist.contains(InfoPlist::DashDocSetFamily)) { const QString kw = plist[InfoPlist::DashDocSetFamily].toString(); if (!kw.contains(QLatin1String("dashtoc"))) { m_keywords << kw; } } if (plist.contains(InfoPlist::IsJavaScriptEnabled)) { m_isJavaScriptEnabled = plist[InfoPlist::IsJavaScriptEnabled].toBool(); } m_keywords.removeDuplicates(); // Determine index page. This is ridiculous. const QString mdIndexFilePath = m_indexFilePath; // Save path from the metadata. // Prefer index path provided by the docset. if (plist.contains(InfoPlist::DashIndexFilePath)) { const QString indexFilePath = plist[InfoPlist::DashIndexFilePath].toString(); if (dir.exists(indexFilePath)) { m_indexFilePath = indexFilePath; } } // Check the metadata. if (m_indexFilePath.isEmpty() && !mdIndexFilePath.isEmpty() && dir.exists(mdIndexFilePath)) { m_indexFilePath = mdIndexFilePath; } // What if there is index.html. if (m_indexFilePath.isEmpty() && dir.exists(QStringLiteral("index.html"))) { m_indexFilePath = QStringLiteral("index.html"); } // Log if unable to determine the index page. Otherwise the path will be set in setBaseUrl(). if (m_indexFilePath.isEmpty()) { qCInfo(log, "[%s] Cannot determine index file.", qPrintable(m_name)); m_indexFileUrl.setUrl(NotFoundPageUrl); } else { m_indexFileUrl = createPageUrl(m_indexFilePath); } countSymbols(); } Docset::~Docset() { delete m_db; } bool Docset::isValid() const { return m_type != Type::Invalid; } QString Docset::name() const { return m_name; } QString Docset::title() const { return m_title; } QStringList Docset::keywords() const { return m_keywords; } QString Docset::version() const { return m_version; } int Docset::revision() const { return m_revision; } QString Docset::feedUrl() const { return m_feedUrl; } QString Docset::path() const { return m_path; } QString Docset::documentPath() const { return QDir(m_path).filePath(QStringLiteral("Contents/Resources/Documents")); } QIcon Docset::icon() const { return m_icon; } QIcon Docset::symbolTypeIcon(const QString &symbolType) const { static const QIcon unknownIcon(QStringLiteral("typeIcon:Unknown.png")); const QIcon icon(QStringLiteral("typeIcon:%1.png").arg(symbolType)); return icon.availableSizes().isEmpty() ? unknownIcon : icon; } QUrl Docset::indexFileUrl() const { return m_indexFileUrl; } QMap Docset::symbolCounts() const { return m_symbolCounts; } int Docset::symbolCount(const QString &symbolType) const { return m_symbolCounts.value(symbolType); } const QMultiMap &Docset::symbols(const QString &symbolType) const { if (!m_symbols.contains(symbolType)) loadSymbols(symbolType); return m_symbols[symbolType]; } QList Docset::search(const QString &query, const CancellationToken &token) const { QString sql; if (m_type == Docset::Type::Dash) { if (m_isFuzzySearchEnabled) { sql = QStringLiteral("SELECT name, type, path, '', zealScore('%1', name) as score" " FROM searchIndex" " WHERE score > 0" " ORDER BY score DESC"); } else { sql = QStringLiteral("SELECT name, type, path, '', -length(name) as score" " FROM searchIndex" " WHERE (name LIKE '%%1%' ESCAPE '\\')" " ORDER BY score DESC"); } } else { if (m_isFuzzySearchEnabled) { sql = QStringLiteral("SELECT name, type, path, fragment, zealScore('%1', name) as score" " FROM searchIndex" " WHERE score > 0" " ORDER BY score DESC"); } else { sql = QStringLiteral("SELECT name, type, path, fragment, -length(name) as score" " FROM searchIndex" " WHERE (name LIKE '%%1%' ESCAPE '\\')" " ORDER BY score DESC"); } } // Limit for very short queries. // TODO: Show a notification about the reduced result set. if (query.size() < 3) { sql += QLatin1String(" LIMIT 1000"); } // Make it safe to use in a SQL query. QString sanitizedQuery = query; sanitizedQuery.replace(QLatin1Char('\''), QLatin1String("''")); m_db->prepare(sql.arg(sanitizedQuery)); QList results; while (m_db->next() && !token.isCanceled()) { SearchResult result; result.name = m_db->value(0).toString(); result.type = parseSymbolType(m_db->value(1).toString()); result.urlPath = m_db->value(2).toString(); result.urlFragment = m_db->value(3).toString(); result.docset = const_cast(this); result.score = m_db->value(4).toDouble(); // Compute match positions for highlighting. if (m_isFuzzySearchEnabled) { // Fuzzy search: use fuzzy matching algorithm. Util::Fuzzy::score(query, result.name, &result.matchPositions); } else { // Non-fuzzy search: highlight only first occurrence. const int pos = result.name.indexOf(query, 0, Qt::CaseInsensitive); if (pos != -1) { for (int i = 0; i < query.length(); ++i) { result.matchPositions.append(pos + i); } } } results.append(std::move(result)); } return results; } QList Docset::relatedLinks(const QUrl &url) const { if (!m_baseUrl.isParentOf(url)) { return {}; } // Get page path within the docset. const QString path = url.path().mid(m_baseUrl.path().length() + 1); // Prepare the query to look up all pages with the same url. QString sql; if (m_type == Docset::Type::Dash) { sql = QStringLiteral("SELECT name, type, path" " FROM searchIndex" " WHERE path LIKE \"%1%%\" AND path <> \"%1\""); } else if (m_type == Docset::Type::ZDash) { sql = QStringLiteral("SELECT name, type, path, fragment" " FROM searchIndex" " WHERE path = \"%1\" AND fragment IS NOT NULL"); } QList results; m_db->prepare(sql.arg(path)); while (m_db->next()) { results.append({m_db->value(0).toString(), parseSymbolType(m_db->value(1).toString()), m_db->value(2).toString(), m_db->value(3).toString(), const_cast(this), 0}); } if (results.size() == 1) { return {}; } return results; } QUrl Docset::searchResultUrl(const SearchResult &result) const { return createPageUrl(result.urlPath, result.urlFragment); } void Docset::loadMetadata() { const QDir dir(m_path); // Fallback if meta.json is absent if (!dir.exists(QStringLiteral("meta.json"))) return; QScopedPointer file(new QFile(dir.filePath(QStringLiteral("meta.json")))); if (!file->open(QIODevice::ReadOnly)) return; QJsonParseError jsonError; const QJsonObject jsonObject = QJsonDocument::fromJson(file->readAll(), &jsonError).object(); if (jsonError.error != QJsonParseError::NoError) return; m_name = jsonObject[QStringLiteral("name")].toString(); m_title = jsonObject[QStringLiteral("title")].toString(); m_version = jsonObject[QStringLiteral("version")].toString(); m_revision = jsonObject[QStringLiteral("revision")].toString().toInt(); if (jsonObject.contains(QStringLiteral("feed_url"))) { m_feedUrl = jsonObject[QStringLiteral("feed_url")].toString(); } if (jsonObject.contains(QStringLiteral("extra"))) { const QJsonObject extra = jsonObject[QStringLiteral("extra")].toObject(); if (extra.contains(QStringLiteral("indexFilePath"))) { m_indexFilePath = extra[QStringLiteral("indexFilePath")].toString(); } if (extra.contains(QStringLiteral("keywords"))) { for (const QJsonValueRef kw : extra[QStringLiteral("keywords")].toArray()) { m_keywords << kw.toString(); } } if (extra.contains(QStringLiteral("isJavaScriptEnabled"))) { m_isJavaScriptEnabled = extra[QStringLiteral("isJavaScriptEnabled")].toBool(); } } } void Docset::countSymbols() { static const QString sql = QStringLiteral("SELECT type, COUNT(*)" " FROM searchIndex" " GROUP BY type"); if (!m_db->prepare(sql)) { qCWarning(log, "[%s] Cannot prepare statement to count symbols: %s.", qPrintable(m_name), qPrintable(m_db->lastError())); return; } while (m_db->next()) { const QString symbolTypeStr = m_db->value(0).toString(); // A workaround for https://github.com/zealdocs/zeal/issues/980. if (symbolTypeStr.isEmpty()) { qCDebug(log, "[%s] Found empty symbol type, skipping...", qPrintable(m_name)); continue; } const QString symbolType = parseSymbolType(symbolTypeStr); m_symbolStrings.insert(symbolType, symbolTypeStr); m_symbolCounts[symbolType] += m_db->value(1).toInt(); } } // TODO: Fetch and cache only portions of symbols void Docset::loadSymbols(const QString &symbolType) const { // Iterator `it` is a QPair, // with it.first and it.second respectively pointing to the start and the end // of the range of nodes having symbolType as key. It effectively represents a // contiguous view over the nodes with a specified key. for (auto it = std::as_const(m_symbolStrings).equal_range(symbolType); it.first != it.second; ++it.first) { loadSymbols(symbolType, it.first.value()); } } void Docset::loadSymbols(const QString &symbolType, const QString &symbolString) const { QString sql; if (m_type == Docset::Type::Dash) { sql = QStringLiteral("SELECT name, path" " FROM searchIndex" " WHERE type='%1'" " ORDER BY name"); } else { sql = QStringLiteral("SELECT name, path, fragment" " FROM searchIndex" " WHERE type='%1'" " ORDER BY name"); } if (!m_db->prepare(sql.arg(symbolString))) { qCWarning(log, "[%s] Cannot prepare statement to load symbols for type '%s': %s.", qPrintable(m_name), qPrintable(symbolString), qPrintable(m_db->lastError())); return; } QMultiMap &symbols = m_symbols[symbolType]; while (m_db->next()) { symbols.insert(m_db->value(0).toString(), createPageUrl(m_db->value(1).toString(), m_db->value(2).toString())); } } void Docset::createIndex() { static const QString indexListQuery = QStringLiteral("PRAGMA INDEX_LIST('%1')"); static const QString indexDropQuery = QStringLiteral("DROP INDEX '%1'"); static const QString indexCreateQuery = QStringLiteral("CREATE INDEX IF NOT EXISTS %1%2" " ON %3 (%4 COLLATE NOCASE)"); const QString tableName = m_type == Type::Dash ? QStringLiteral("searchIndex") : QStringLiteral("ztoken"); const QString columnName = m_type == Type::Dash ? QStringLiteral("name") : QStringLiteral("ztokenname"); m_db->prepare(indexListQuery.arg(tableName)); QStringList oldIndexes; while (m_db->next()) { const QString indexName = m_db->value(1).toString(); if (!indexName.startsWith(IndexNamePrefix)) continue; if (indexName.endsWith(IndexNameVersion)) return; oldIndexes << indexName; } // Drop old indexes for (const QString &oldIndexName : std::as_const(oldIndexes)) { m_db->execute(indexDropQuery.arg(oldIndexName)); } m_db->execute(indexCreateQuery.arg(IndexNamePrefix).arg(IndexNameVersion).arg(tableName).arg(columnName)); } void Docset::createView() { static const QString viewCreateQuery = QStringLiteral("CREATE VIEW IF NOT EXISTS searchIndex AS" " SELECT" " ztokenname AS name," " ztypename AS type," " zpath AS path," " zanchor AS fragment" " FROM ztoken" " INNER JOIN ztokenmetainformation" " ON ztoken.zmetainformation = ztokenmetainformation.z_pk" " INNER JOIN zfilepath" " ON ztokenmetainformation.zfile = zfilepath.z_pk" " INNER JOIN ztokentype" " ON ztoken.ztokentype = ztokentype.z_pk"); m_db->execute(viewCreateQuery); } QUrl Docset::createPageUrl(const QString &path, const QString &fragment) const { QString realPath; QString realFragment; if (fragment.isEmpty()) { const QStringList urlParts = path.split(QLatin1Char('#')); realPath = urlParts[0]; if (urlParts.size() > 1) realFragment = urlParts[1]; } else { realPath = path; realFragment = fragment; } static const QRegularExpression dashEntryRegExp(QStringLiteral("")); realPath.remove(dashEntryRegExp); realFragment.remove(dashEntryRegExp); QUrl url = m_baseUrl; url.setPath(m_baseUrl.path() + "/" + realPath, QUrl::TolerantMode); if (!realFragment.isEmpty()) { if (realFragment.startsWith(QLatin1String("//apple_ref")) || realFragment.startsWith(QLatin1String("//dash_ref"))) { url.setFragment(realFragment, QUrl::DecodedMode); } else { url.setFragment(realFragment); } } return url; } QString Docset::parseSymbolType(const QString &str) { // Dash symbol aliases const static QHash aliases = { // Attribute {QStringLiteral("Package Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("Private Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("Protected Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("Public Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("Static Package Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("Static Private Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("Static Protected Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("Static Public Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("XML Attributes"), QStringLiteral("Attribute")}, // Binding {QStringLiteral("binding"), QStringLiteral("Binding")}, // Category {QStringLiteral("cat"), QStringLiteral("Category")}, {QStringLiteral("Groups"), QStringLiteral("Category")}, {QStringLiteral("Pages"), QStringLiteral("Category")}, // Class {QStringLiteral("cl"), QStringLiteral("Class")}, {QStringLiteral("specialization"), QStringLiteral("Class")}, {QStringLiteral("tmplt"), QStringLiteral("Class")}, // Constant {QStringLiteral("data"), QStringLiteral("Constant")}, {QStringLiteral("econst"), QStringLiteral("Constant")}, {QStringLiteral("enumdata"), QStringLiteral("Constant")}, {QStringLiteral("enumelt"), QStringLiteral("Constant")}, {QStringLiteral("clconst"), QStringLiteral("Constant")}, {QStringLiteral("structdata"), QStringLiteral("Constant")}, {QStringLiteral("writerid"), QStringLiteral("Constant")}, {QStringLiteral("Notifications"), QStringLiteral("Constant")}, // Constructor {QStringLiteral("structctr"), QStringLiteral("Constructor")}, {QStringLiteral("Public Constructors"), QStringLiteral("Constructor")}, // Enumeration {QStringLiteral("enum"), QStringLiteral("Enumeration")}, {QStringLiteral("Enum"), QStringLiteral("Enumeration")}, {QStringLiteral("Enumerations"), QStringLiteral("Enumeration")}, // Event {QStringLiteral("event"), QStringLiteral("Event")}, {QStringLiteral("Public Events"), QStringLiteral("Event")}, {QStringLiteral("Inherited Events"), QStringLiteral("Event")}, {QStringLiteral("Private Events"), QStringLiteral("Event")}, // Field {QStringLiteral("Data Fields"), QStringLiteral("Field")}, // Function {QStringLiteral("dcop"), QStringLiteral("Function")}, {QStringLiteral("func"), QStringLiteral("Function")}, {QStringLiteral("ffunc"), QStringLiteral("Function")}, {QStringLiteral("signal"), QStringLiteral("Function")}, {QStringLiteral("slot"), QStringLiteral("Function")}, {QStringLiteral("grammar"), QStringLiteral("Function")}, {QStringLiteral("Function Prototypes"), QStringLiteral("Function")}, {QStringLiteral("Functions/Subroutines"), QStringLiteral("Function")}, {QStringLiteral("Members"), QStringLiteral("Function")}, {QStringLiteral("Package Functions"), QStringLiteral("Function")}, {QStringLiteral("Private Member Functions"), QStringLiteral("Function")}, {QStringLiteral("Private Slots"), QStringLiteral("Function")}, {QStringLiteral("Protected Member Functions"), QStringLiteral("Function")}, {QStringLiteral("Protected Slots"), QStringLiteral("Function")}, {QStringLiteral("Public Member Functions"), QStringLiteral("Function")}, {QStringLiteral("Public Slots"), QStringLiteral("Function")}, {QStringLiteral("Signals"), QStringLiteral("Function")}, {QStringLiteral("Static Package Functions"), QStringLiteral("Function")}, {QStringLiteral("Static Private Member Functions"), QStringLiteral("Function")}, {QStringLiteral("Static Protected Member Functions"), QStringLiteral("Function")}, {QStringLiteral("Static Public Member Functions"), QStringLiteral("Function")}, // Guide {QStringLiteral("doc"), QStringLiteral("Guide")}, // Namespace {QStringLiteral("ns"), QStringLiteral("Namespace")}, // Macro {QStringLiteral("macro"), QStringLiteral("Macro")}, // Method {QStringLiteral("clm"), QStringLiteral("Method")}, {QStringLiteral("enumcm"), QStringLiteral("Method")}, {QStringLiteral("enumctr"), QStringLiteral("Method")}, {QStringLiteral("enumm"), QStringLiteral("Method")}, {QStringLiteral("intfctr"), QStringLiteral("Method")}, {QStringLiteral("intfcm"), QStringLiteral("Method")}, {QStringLiteral("intfm"), QStringLiteral("Method")}, {QStringLiteral("intfsub"), QStringLiteral("Method")}, {QStringLiteral("instsub"), QStringLiteral("Method")}, {QStringLiteral("instctr"), QStringLiteral("Method")}, {QStringLiteral("instm"), QStringLiteral("Method")}, {QStringLiteral("structcm"), QStringLiteral("Method")}, {QStringLiteral("structm"), QStringLiteral("Method")}, {QStringLiteral("structsub"), QStringLiteral("Method")}, {QStringLiteral("Class Methods"), QStringLiteral("Method")}, {QStringLiteral("Inherited Methods"), QStringLiteral("Method")}, {QStringLiteral("Instance Methods"), QStringLiteral("Method")}, {QStringLiteral("Private Methods"), QStringLiteral("Method")}, {QStringLiteral("Protected Methods"), QStringLiteral("Method")}, {QStringLiteral("Public Methods"), QStringLiteral("Method")}, // Operator {QStringLiteral("intfopfunc"), QStringLiteral("Operator")}, {QStringLiteral("opfunc"), QStringLiteral("Operator")}, // Property {QStringLiteral("enump"), QStringLiteral("Property")}, {QStringLiteral("intfdata"), QStringLiteral("Property")}, {QStringLiteral("intfp"), QStringLiteral("Property")}, {QStringLiteral("instp"), QStringLiteral("Property")}, {QStringLiteral("structp"), QStringLiteral("Property")}, {QStringLiteral("Inherited Properties"), QStringLiteral("Property")}, {QStringLiteral("Private Properties"), QStringLiteral("Property")}, {QStringLiteral("Protected Properties"), QStringLiteral("Property")}, {QStringLiteral("Public Properties"), QStringLiteral("Property")}, // Protocol {QStringLiteral("intf"), QStringLiteral("Protocol")}, // Structure {QStringLiteral("_Struct"), QStringLiteral("Structure")}, {QStringLiteral("_Structs"), QStringLiteral("Structure")}, {QStringLiteral("struct"), QStringLiteral("Structure")}, {QStringLiteral("Control Structure"), QStringLiteral("Structure")}, {QStringLiteral("Data Structures"), QStringLiteral("Structure")}, {QStringLiteral("Struct"), QStringLiteral("Structure")}, // Type {QStringLiteral("tag"), QStringLiteral("Type")}, {QStringLiteral("tdef"), QStringLiteral("Type")}, {QStringLiteral("Data Types"), QStringLiteral("Type")}, {QStringLiteral("Package Types"), QStringLiteral("Type")}, {QStringLiteral("Private Types"), QStringLiteral("Type")}, {QStringLiteral("Protected Types"), QStringLiteral("Type")}, {QStringLiteral("Public Types"), QStringLiteral("Type")}, {QStringLiteral("Typedefs"), QStringLiteral("Type")}, // Variable {QStringLiteral("var"), QStringLiteral("Variable")} }; return aliases.value(str, str); } QUrl Docset::baseUrl() const { return m_baseUrl; } void Docset::setBaseUrl(const QUrl &baseUrl) { m_baseUrl = baseUrl; if (!m_indexFilePath.isEmpty()) { m_indexFileUrl = createPageUrl(m_indexFilePath); } } bool Docset::isFuzzySearchEnabled() const { return m_isFuzzySearchEnabled; } void Docset::setFuzzySearchEnabled(bool enabled) { m_isFuzzySearchEnabled = enabled; } bool Docset::isJavaScriptEnabled() const { return m_isJavaScriptEnabled; } static void sqliteScoreFunction(sqlite3_context *context, int argc, sqlite3_value **argv) { Q_UNUSED(argc) auto needle = reinterpret_cast(sqlite3_value_text(argv[0])); auto haystack = reinterpret_cast(sqlite3_value_text(argv[1])); sqlite3_result_double(context, Zeal::Util::Fuzzy::scoreFunction(needle, haystack)); } ================================================ FILE: src/libs/registry/docset.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_REGISTRY_DOCSET_H #define ZEAL_REGISTRY_DOCSET_H #include #include #include #include #include namespace Zeal { namespace Util { class SQLiteDatabase; } namespace Registry { class CancellationToken; struct SearchResult; class Docset final { Q_DISABLE_COPY_MOVE(Docset) public: explicit Docset(QString path); virtual ~Docset(); bool isValid() const; QString name() const; QString title() const; QStringList keywords() const; QString version() const; int revision() const; QString feedUrl() const; QString path() const; QString documentPath() const; QIcon icon() const; QIcon symbolTypeIcon(const QString &symbolType) const; QUrl indexFileUrl() const; QMap symbolCounts() const; int symbolCount(const QString &symbolType) const; const QMultiMap &symbols(const QString &symbolType) const; QList search(const QString &query, const CancellationToken &token) const; QList relatedLinks(const QUrl &url) const; // FIXME: This a temporary solution to create URL on demand. QUrl searchResultUrl(const SearchResult &result) const; // FIXME: This is an ugly workaround before we have a proper docset sources implementation bool hasUpdate = false; QUrl baseUrl() const; void setBaseUrl(const QUrl &baseUrl); bool isFuzzySearchEnabled() const; void setFuzzySearchEnabled(bool enabled); bool isJavaScriptEnabled() const; private: enum class Type { Invalid, Dash, ZDash }; void loadMetadata(); void countSymbols(); void loadSymbols(const QString &symbolType) const; void loadSymbols(const QString &symbolType, const QString &symbolString) const; void createIndex(); void createView(); QUrl createPageUrl(const QString &path, const QString &fragment = QString()) const; static QString parseSymbolType(const QString &str); QString m_name; QString m_title; QStringList m_keywords; QString m_version; int m_revision = 0; QString m_feedUrl; Docset::Type m_type = Type::Invalid; QString m_path; QIcon m_icon; QUrl m_indexFileUrl; QString m_indexFilePath; QMultiMap m_symbolStrings; QMap m_symbolCounts; mutable QMap> m_symbols; Util::SQLiteDatabase *m_db = nullptr; bool m_isFuzzySearchEnabled = false; bool m_isJavaScriptEnabled = false; QUrl m_baseUrl; }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_DOCSET_H ================================================ FILE: src/libs/registry/docsetmetadata.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "docsetmetadata.h" #include #include #include #include #include #include #include using namespace Zeal::Registry; DocsetMetadata::DocsetMetadata(const QJsonObject &jsonObject) { m_name = jsonObject[QStringLiteral("name")].toString(); m_title = jsonObject[QStringLiteral("title")].toString(); m_rawIcon = QByteArray::fromBase64(jsonObject[QStringLiteral("icon")].toString().toLocal8Bit()); m_icon.addPixmap(QPixmap::fromImage(QImage::fromData(m_rawIcon))); m_rawIcon2x = QByteArray::fromBase64(jsonObject[QStringLiteral("icon2x")].toString() .toLocal8Bit()); if (qApp->devicePixelRatio() > 1.0) { QPixmap pixmap = QPixmap::fromImage(QImage::fromData(m_rawIcon2x)); pixmap.setDevicePixelRatio(2.0); m_icon.addPixmap(pixmap); } for (const QJsonValueRef vv : jsonObject[QStringLiteral("aliases")].toArray()) { m_aliases << vv.toString(); } for (const QJsonValueRef vv : jsonObject[QStringLiteral("versions")].toArray()) { m_versions << vv.toString(); } // Unfortunately, API returns revision as a string, so it needs to be converted to integer // for comparison to work properly. m_revision = jsonObject[QStringLiteral("revision")].toString().toInt(); m_feedUrl = QUrl(jsonObject[QStringLiteral("feed_url")].toString()); for (const QJsonValueRef vv : jsonObject[QStringLiteral("urls")].toArray()) { m_urls.append(QUrl(vv.toString())); } m_extra = jsonObject[QStringLiteral("extra")].toObject(); } /*! Creates meta.json for specified docset \a version in the \a path. */ void DocsetMetadata::save(const QString &path, const QString &version) { QScopedPointer file(new QFile(path + QLatin1String("/meta.json"))); if (!file->open(QIODevice::WriteOnly)) return; QJsonObject jsonObject; jsonObject[QStringLiteral("name")] = m_name; jsonObject[QStringLiteral("title")] = m_title; if (!version.isEmpty()) jsonObject[QStringLiteral("version")] = version; if (version == latestVersion() && m_revision > 0) jsonObject[QStringLiteral("revision")] = QString::number(m_revision); if (!m_feedUrl.isEmpty()) jsonObject[QStringLiteral("feed_url")] = m_feedUrl.toString(); if (!m_urls.isEmpty()) { QJsonArray urls; for (const QUrl &url : std::as_const(m_urls)) { urls.append(url.toString()); } jsonObject[QStringLiteral("urls")] = urls; } if (!m_extra.isEmpty()) jsonObject[QStringLiteral("extra")] = m_extra; file->write(QJsonDocument(jsonObject).toJson()); file->close(); if (m_rawIcon.isEmpty()) return; file->setFileName(path + QLatin1String("/icon.png")); if (file->open(QIODevice::WriteOnly)) file->write(m_rawIcon); file->close(); if (m_rawIcon2x.isEmpty()) return; file->setFileName(path + QLatin1String("/icon@2x.png")); if (file->open(QIODevice::WriteOnly)) file->write(m_rawIcon2x); file->close(); } QString DocsetMetadata::name() const { return m_name; } QIcon DocsetMetadata::icon() const { return m_icon; } QString DocsetMetadata::title() const { return m_title; } QStringList DocsetMetadata::aliases() const { return m_aliases; } QStringList DocsetMetadata::versions() const { return m_versions; } QString DocsetMetadata::latestVersion() const { return m_versions.isEmpty() ? QString() : m_versions.first(); } int DocsetMetadata::revision() const { return m_revision; } QUrl DocsetMetadata::feedUrl() const { return m_feedUrl; } QUrl DocsetMetadata::url() const { return m_urls.at(QRandomGenerator::global()->bounded(m_urls.size())); } QList DocsetMetadata::urls() const { return m_urls; } DocsetMetadata DocsetMetadata::fromDashFeed(const QUrl &feedUrl, const QByteArray &data) { DocsetMetadata metadata; metadata.m_name = feedUrl.fileName(); // Strip ".xml" extension if any. if (metadata.m_name.endsWith(QLatin1String(".xml"))) { metadata.m_name.chop(4); } metadata.m_title = metadata.m_name; metadata.m_title.replace(QLatin1Char('_'), QLatin1Char(' ')); metadata.m_feedUrl = feedUrl; QXmlStreamReader xml(data); while (!xml.atEnd()) { const QXmlStreamReader::TokenType token = xml.readNext(); if (token != QXmlStreamReader::StartElement) continue; // Try to pull out the relevant data if (xml.name() == QLatin1String("version")) { if (xml.readNext() != QXmlStreamReader::Characters) continue; metadata.m_versions << xml.text().toString(); } else if (xml.name() == QLatin1String("url")) { if (xml.readNext() != QXmlStreamReader::Characters) continue; metadata.m_urls.append(QUrl(xml.text().toString())); } } return metadata; } ================================================ FILE: src/libs/registry/docsetmetadata.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_REGISTRY_DOCSETMETADATA_H #define ZEAL_REGISTRY_DOCSETMETADATA_H #include #include #include #include namespace Zeal { namespace Registry { class DocsetMetadata { public: explicit DocsetMetadata() = default; explicit DocsetMetadata(const QJsonObject &jsonObject); void save(const QString &path, const QString &version); QString name() const; QString title() const; QStringList aliases() const; QStringList versions() const; QString latestVersion() const; int revision() const; QIcon icon() const; QUrl feedUrl() const; QUrl url() const; QList urls() const; static DocsetMetadata fromDashFeed(const QUrl &feedUrl, const QByteArray &data); private: QString m_name; QString m_title; QStringList m_aliases; QStringList m_versions; int m_revision = 0; QByteArray m_rawIcon; QByteArray m_rawIcon2x; QIcon m_icon; QJsonObject m_extra; QUrl m_feedUrl; QList m_urls; }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_DOCSETMETADATA_H ================================================ FILE: src/libs/registry/docsetregistry.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "docsetregistry.h" #include "docset.h" #include "listmodel.h" #include "searchquery.h" #include "searchresult.h" #include #include #include #include #include #include #include #include using namespace Zeal::Registry; static Q_LOGGING_CATEGORY(log, "zeal.registry.docsetregistry") void MergeQueryResults(QList &finalResult, const QList &partial) { finalResult << partial; } DocsetRegistry::DocsetRegistry(QObject *parent) : QObject(parent) , m_model(new ListModel(this)) , m_thread(new QThread(this)) { // Register for use in signal connections. qRegisterMetaType>("QList"); // FIXME: Only search should be performed in a separate thread moveToThread(m_thread); m_thread->start(); } DocsetRegistry::~DocsetRegistry() { m_thread->exit(); m_thread->wait(); qDeleteAll(m_docsets); } QAbstractItemModel *DocsetRegistry::model() const { return m_model; } QString DocsetRegistry::storagePath() const { return m_storagePath; } void DocsetRegistry::setStoragePath(const QString &path) { if (path == m_storagePath) { return; } QMetaObject::invokeMethod(this, [this, path](){ unloadAllDocsets(); addDocsetsFromFolder(path); m_storagePath = path; }); } bool DocsetRegistry::isFuzzySearchEnabled() const { return m_isFuzzySearchEnabled; } void DocsetRegistry::setFuzzySearchEnabled(bool enabled) { if (enabled == m_isFuzzySearchEnabled) { return; } m_isFuzzySearchEnabled = enabled; for (Docset *docset : std::as_const(m_docsets)) { docset->setFuzzySearchEnabled(enabled); } } int DocsetRegistry::count() const { return m_docsets.count(); } bool DocsetRegistry::contains(const QString &name) const { return m_docsets.contains(name); } QStringList DocsetRegistry::names() const { return m_docsets.keys(); } void DocsetRegistry::loadDocset(const QString &path) { std::future f = std::async(std::launch::async, [path](){ return new Docset(path); }); f.wait(); Docset *docset = f.get(); // TODO: Emit error if (!docset->isValid()) { qCWarning(log, "Could not load docset '%s' from '%s'. Reinstall the docset.", qPrintable(docset->name()), qPrintable(docset->path())); delete docset; return; } docset->setFuzzySearchEnabled(m_isFuzzySearchEnabled); const QString name = docset->name(); if (m_docsets.contains(name)) { unloadDocset(name); } // Setup HTTP mount. QUrl url = Core::Application::instance()->httpServer()->mount(name, docset->documentPath()); if (url.isEmpty()) { qCWarning(log, "Could not enable docset '%s' from '%s'. Reinstall the docset.", qPrintable(docset->name()), qPrintable(docset->path())); delete docset; return; } docset->setBaseUrl(url); m_docsets[name] = docset; emit docsetLoaded(name); } void DocsetRegistry::unloadDocset(const QString &name) { emit docsetAboutToBeUnloaded(name); Core::Application::instance()->httpServer()->unmount(name); delete m_docsets.take(name); emit docsetUnloaded(name); } void DocsetRegistry::unloadAllDocsets() { const auto keys = m_docsets.keys(); for (const QString &name : keys) { unloadDocset(name); } } Docset *DocsetRegistry::docset(const QString &name) const { return m_docsets[name]; } Docset *DocsetRegistry::docset(int index) const { if (index < 0 || index >= m_docsets.size()) return nullptr; auto it = m_docsets.cbegin(); std::advance(it, index); return *it; } Docset *DocsetRegistry::docsetForUrl(const QUrl &url) { for (Docset *docset : std::as_const(m_docsets)) { if (docset->baseUrl().isParentOf(url)) return docset; } return nullptr; } QList DocsetRegistry::docsets() const { return m_docsets.values(); } void DocsetRegistry::search(const QString &query) { m_cancellationToken.cancel(); if (query.isEmpty()) { emit searchCompleted({}); return; } QMetaObject::invokeMethod(this, "_runQuery", Qt::QueuedConnection, Q_ARG(QString, query)); } void DocsetRegistry::_runQuery(const QString &query) { m_cancellationToken.reset(); QList enabledDocsets; const SearchQuery searchQuery = SearchQuery::fromString(query); if (searchQuery.hasKeywords()) { for (Docset *docset : std::as_const(m_docsets)) { if (searchQuery.hasKeywords(docset->keywords())) enabledDocsets << docset; } } else { enabledDocsets = docsets(); } QFuture> queryResultsFuture = QtConcurrent::mappedReduced(enabledDocsets, std::bind(&Docset::search, std::placeholders::_1, searchQuery.query(), std::ref(m_cancellationToken)), &MergeQueryResults); QList results = queryResultsFuture.result(); if (m_cancellationToken.isCanceled()) return; std::sort(results.begin(), results.end()); if (m_cancellationToken.isCanceled()) return; emit searchCompleted(results); } // Recursively finds and adds all docsets in a given directory. void DocsetRegistry::addDocsetsFromFolder(const QString &path) { const QDir dir(path); const auto subDirectories = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllDirs); for (const QFileInfo &subdir : subDirectories) { if (subdir.suffix() == QLatin1String("docset")) loadDocset(subdir.filePath()); else addDocsetsFromFolder(subdir.filePath()); } } ================================================ FILE: src/libs/registry/docsetregistry.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_REGISTRY_DOCSETREGISTRY_H #define ZEAL_REGISTRY_DOCSETREGISTRY_H #include "cancellationtoken.h" #include "searchresult.h" #include #include class QAbstractItemModel; class QThread; namespace Zeal { namespace Registry { class Docset; class DocsetRegistry final : public QObject { Q_OBJECT Q_DISABLE_COPY_MOVE(DocsetRegistry) public: explicit DocsetRegistry(QObject *parent = nullptr); ~DocsetRegistry() override; QAbstractItemModel *model() const; QString storagePath() const; void setStoragePath(const QString &path); bool isFuzzySearchEnabled() const; void setFuzzySearchEnabled(bool enabled); int count() const; bool contains(const QString &name) const; QStringList names() const; void loadDocset(const QString &path); void unloadDocset(const QString &name); void unloadAllDocsets(); Docset *docset(const QString &name) const; Docset *docset(int index) const; Docset *docsetForUrl(const QUrl &url); QList docsets() const; void search(const QString &query); const QList &queryResults(); signals: void docsetLoaded(const QString &name); void docsetAboutToBeUnloaded(const QString &name); void docsetUnloaded(const QString &name); void searchCompleted(const QList &results); private slots: void _runQuery(const QString &query); private: void addDocsetsFromFolder(const QString &path); QAbstractItemModel *m_model = nullptr; QString m_storagePath; bool m_isFuzzySearchEnabled = false; QThread *m_thread = nullptr; QMap m_docsets; CancellationToken m_cancellationToken; }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_DOCSETREGISTRY_H ================================================ FILE: src/libs/registry/itemdatarole.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_REGISTRY_ITEMDATAROLE_H #define ZEAL_REGISTRY_ITEMDATAROLE_H #include namespace Zeal { namespace Registry { enum ItemDataRole { DocsetIconRole = Qt::UserRole, DocsetNameRole, MatchPositionsRole, UpdateAvailableRole, UrlRole }; enum SectionIndex { Name, SearchPrefix, Actions }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_ITEMDATAROLE_H ================================================ FILE: src/libs/registry/listmodel.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "listmodel.h" #include "docset.h" #include "docsetregistry.h" #include "itemdatarole.h" #include using namespace Zeal::Registry; ListModel::ListModel(DocsetRegistry *docsetRegistry) : QAbstractItemModel(docsetRegistry) , m_docsetRegistry(docsetRegistry) { connect(m_docsetRegistry, &DocsetRegistry::docsetLoaded, this, &ListModel::addDocset); connect(m_docsetRegistry, &DocsetRegistry::docsetAboutToBeUnloaded, this, &ListModel::removeDocset); } ListModel::~ListModel() { for (auto &kv : m_docsetItems) { qDeleteAll(kv.second->groups); delete kv.second; } } QVariant ListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal || role != Qt::DisplayRole) { return QAbstractItemModel::headerData(section, orientation, role); } switch (section) { case SectionIndex::Name: return tr("Name"); case SectionIndex::SearchPrefix: return tr("Search prefix"); default: return QLatin1String(); } } QVariant ListModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } switch (role) { case Qt::DecorationRole: if (index.column() != SectionIndex::Name) { return QVariant(); } switch (indexLevel(index)) { case IndexLevel::Docset: return itemInRow(index.row())->docset->icon(); case IndexLevel::Group: { auto docsetItem = static_cast(index.internalPointer()); const QString symbolType = docsetItem->groups.at(index.row())->symbolType; return docsetItem->docset->symbolTypeIcon(symbolType); } case IndexLevel::Symbol: { auto groupItem = static_cast(index.internalPointer()); return groupItem->docsetItem->docset->symbolTypeIcon(groupItem->symbolType); } default: return QVariant(); } case Qt::DisplayRole: switch (indexLevel(index)) { case IndexLevel::Docset: switch (index.column()) { case SectionIndex::Name: return itemInRow(index.row())->docset->title(); case SectionIndex::SearchPrefix: return itemInRow(index.row())->docset->keywords().join(QLatin1String(", ")); default: return QVariant(); } case IndexLevel::Group: { auto docsetItem = static_cast(index.internalPointer()); const QString symbolType = docsetItem->groups.at(index.row())->symbolType; return QStringLiteral("%1 (%2)").arg(pluralize(symbolType), QString::number(docsetItem->docset->symbolCount(symbolType))); } case IndexLevel::Symbol: { auto groupItem = static_cast(index.internalPointer()); auto it = groupItem->docsetItem->docset->symbols(groupItem->symbolType).cbegin(); std::advance(it, index.row()); return it.key(); } default: return QVariant(); } case Qt::ToolTipRole: if (index.column() != SectionIndex::Name) { return QVariant(); } switch (indexLevel(index)) { case IndexLevel::Docset: { const auto docset = itemInRow(index.row())->docset; return tr("Version: %1r%2").arg(docset->version()).arg(docset->revision()); } default: return QVariant(); } case ItemDataRole::UrlRole: switch (indexLevel(index)) { case IndexLevel::Docset: return itemInRow(index.row())->docset->indexFileUrl(); case IndexLevel::Symbol: { auto groupItem = static_cast(index.internalPointer()); auto it = groupItem->docsetItem->docset->symbols(groupItem->symbolType).cbegin(); std::advance(it, index.row()); return it.value(); } default: return QVariant(); } case ItemDataRole::DocsetNameRole: if (index.parent().isValid()) return QVariant(); return itemInRow(index.row())->docset->name(); case ItemDataRole::UpdateAvailableRole: if (index.parent().isValid()) return QVariant(); return itemInRow(index.row())->docset->hasUpdate; default: return QVariant(); } } QModelIndex ListModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) { return {}; } switch (indexLevel(parent)) { case IndexLevel::Root: return createIndex(row, column); case IndexLevel::Docset: return createIndex(row, column, static_cast(itemInRow(parent.row()))); case IndexLevel::Group: { auto docsetItem = static_cast(parent.internalPointer()); return createIndex(row, column, docsetItem->groups.at(parent.row())); } default: return {}; } } QModelIndex ListModel::parent(const QModelIndex &child) const { switch (indexLevel(child)) { case IndexLevel::Group: { auto item = static_cast(child.internalPointer()); auto it = std::find_if(m_docsetItems.cbegin(), m_docsetItems.cend(), [item](const auto &pair) { return pair.second == item; }); if (it == m_docsetItems.cend()) { // TODO: Report error, this should never happen. return {}; } const int row = static_cast(std::distance(m_docsetItems.begin(), it)); return createIndex(row, 0); } case IndexLevel::Symbol: { auto item = static_cast(child.internalPointer()); return createIndex(item->docsetItem->groups.indexOf(item), 0, item->docsetItem); } default: return {}; } } int ListModel::columnCount(const QModelIndex &parent) const { if (indexLevel(parent) == IndexLevel::Root) { return 3; } return 1; } int ListModel::rowCount(const QModelIndex &parent) const { if (parent.column() > 0) { return 0; } switch (indexLevel(parent)) { case IndexLevel::Root: return static_cast(m_docsetItems.size()); case IndexLevel::Docset: return itemInRow(parent.row())->docset->symbolCounts().count(); case IndexLevel::Group: { auto docsetItem = static_cast(parent.internalPointer()); return docsetItem->docset->symbolCount(docsetItem->groups.at(parent.row())->symbolType); } default: return 0; } } void ListModel::addDocset(const QString &name) { const int row = static_cast(std::distance(m_docsetItems.begin(), m_docsetItems.upper_bound(name))); beginInsertRows(QModelIndex(), row, row); auto docsetItem = new DocsetItem(); docsetItem->docset = m_docsetRegistry->docset(name); const auto keys = docsetItem->docset->symbolCounts().keys(); for (const QString &symbolType : keys) { auto groupItem = new GroupItem(); groupItem->docsetItem = docsetItem; groupItem->symbolType = symbolType; docsetItem->groups.append(groupItem); } m_docsetItems.insert({name, docsetItem}); endInsertRows(); } void ListModel::removeDocset(const QString &name) { auto it = m_docsetItems.find(name); if (it == m_docsetItems.cend()) { // TODO: Investigate why this can happen (see #420) return; } const int row = static_cast(std::distance(m_docsetItems.begin(), it)); beginRemoveRows(QModelIndex(), row, row); qDeleteAll(it->second->groups); delete it->second; m_docsetItems.erase(it); endRemoveRows(); } QString ListModel::pluralize(const QString &s) { if (s.endsWith(QLatin1String("y"))) { return s.left(s.length() - 1) + QLatin1String("ies"); } return s + (s.endsWith('s') ? QLatin1String("es") : QLatin1String("s")); } ListModel::IndexLevel ListModel::indexLevel(const QModelIndex &index) { if (!index.isValid()) { return IndexLevel::Root; } if (!index.internalPointer()) { return IndexLevel::Docset; } if (*static_cast(index.internalPointer()) == IndexLevel::Docset) { return IndexLevel::Group; } return IndexLevel::Symbol; } ListModel::DocsetItem *ListModel::itemInRow(int row) const { auto it = m_docsetItems.cbegin(); std::advance(it, row); return it->second; } ================================================ FILE: src/libs/registry/listmodel.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_REGISTRY_LISTMODEL_H #define ZEAL_REGISTRY_LISTMODEL_H #include #include namespace Zeal { namespace Registry { class Docset; class DocsetRegistry; class ListModel final : public QAbstractItemModel { Q_OBJECT Q_DISABLE_COPY_MOVE(ListModel) public: ~ListModel() override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QModelIndex index(int row, int column, const QModelIndex &parent) const override; QModelIndex parent(const QModelIndex &child) const override; int columnCount(const QModelIndex &parent) const override; int rowCount(const QModelIndex &parent) const override; private slots: void addDocset(const QString &name); void removeDocset(const QString &name); private: friend class DocsetRegistry; enum class IndexLevel { Root, Docset, Group, Symbol }; explicit ListModel(DocsetRegistry *docsetRegistry); inline static QString pluralize(const QString &s); inline static IndexLevel indexLevel(const QModelIndex &index); DocsetRegistry *m_docsetRegistry = nullptr; struct DocsetItem; struct GroupItem { const IndexLevel level = IndexLevel::Group; DocsetItem *docsetItem = nullptr; QString symbolType; }; struct DocsetItem { const IndexLevel level = IndexLevel::Docset; Docset *docset = nullptr; QList groups; }; inline DocsetItem *itemInRow(int row) const; Util::CaseInsensitiveMap m_docsetItems; }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_LISTMODEL_H ================================================ FILE: src/libs/registry/searchmodel.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "searchmodel.h" #include "docset.h" #include "itemdatarole.h" using namespace Zeal::Registry; SearchModel::SearchModel(QObject *parent) : QAbstractListModel(parent) { } SearchModel *SearchModel::clone(QObject *parent) { auto model = new SearchModel(parent); model->m_dataList = m_dataList; return model; } bool SearchModel::isEmpty() const { return m_dataList.isEmpty(); } QVariant SearchModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); auto item = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: return item->name; case Qt::DecorationRole: return item->docset->symbolTypeIcon(item->type); case ItemDataRole::DocsetIconRole: return item->docset->icon(); case ItemDataRole::MatchPositionsRole: return QVariant::fromValue(item->matchPositions); case ItemDataRole::UrlRole: return item->docset->searchResultUrl(*item); default: return QVariant(); } } QModelIndex SearchModel::index(int row, int column, const QModelIndex &parent) const { if (parent.isValid() || m_dataList.count() <= row || column > 1) return {}; // FIXME: const_cast auto item = const_cast(&m_dataList.at(row)); return createIndex(row, column, item); } int SearchModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) return m_dataList.count(); return 0; } bool SearchModel::removeRows(int row, int count, const QModelIndex &parent) { if (row + count <= m_dataList.size() && !parent.isValid()) { beginRemoveRows(parent, row, row + count - 1); while (count) { m_dataList.removeAt(row); --count; } endRemoveRows(); return true; } return false; } void SearchModel::removeSearchResultWithName(const QString &name) { QMutableListIterator iterator(m_dataList); int rowNum = 0; while (iterator.hasNext()) { if (iterator.next().docset->name() == name) { beginRemoveRows(QModelIndex(), rowNum, rowNum); iterator.remove(); rowNum -= 1; endRemoveRows(); } rowNum += 1; } } void SearchModel::setResults(const QList &results) { beginResetModel(); m_dataList = results; endResetModel(); emit updated(); } ================================================ FILE: src/libs/registry/searchmodel.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_REGISTRY_SEARCHMODEL_H #define ZEAL_REGISTRY_SEARCHMODEL_H #include "searchresult.h" #include namespace Zeal { namespace Registry { class SearchModel final : public QAbstractListModel { Q_OBJECT Q_DISABLE_COPY_MOVE(SearchModel) public: explicit SearchModel(QObject *parent = nullptr); SearchModel *clone(QObject *parent = nullptr); bool isEmpty() const; QVariant data(const QModelIndex &index, int role) const override; QModelIndex index(int row, int column, const QModelIndex &parent) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; void removeSearchResultWithName(const QString &name); public slots: void setResults(const QList &results = QList()); signals: void updated(); private: QList m_dataList; }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_SEARCHMODEL_H ================================================ FILE: src/libs/registry/searchquery.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "searchquery.h" #include using namespace Zeal::Registry; namespace { const char prefixSeparator = ':'; const char keywordSeparator = ','; } // namespace SearchQuery::SearchQuery(QString query, const QStringList &keywords) : m_query(std::move(query)) { setKeywords(keywords); } SearchQuery SearchQuery::fromString(const QString &str) { const int sepAt = str.indexOf(prefixSeparator); const int next = sepAt + 1; QString query; QStringList keywords; if (sepAt > 0 && (next >= str.size() || str.at(next) != prefixSeparator)) { query = str.mid(next).trimmed(); const QString keywordStr = str.left(sepAt).trimmed(); keywords = keywordStr.split(keywordSeparator); } else { query = str.trimmed(); } return SearchQuery(query, keywords); } QString SearchQuery::toString() const { if (m_keywords.isEmpty()) { return m_query; } return m_keywordPrefix + m_query; } bool SearchQuery::isEmpty() const { return m_query.isEmpty() && m_keywords.isEmpty(); } QStringList SearchQuery::keywords() const { return m_keywords; } void SearchQuery::setKeywords(const QStringList &list) { if (list.isEmpty()) return; m_keywords = list; m_keywordPrefix = list.join(keywordSeparator) + prefixSeparator; } bool SearchQuery::hasKeywords() const { return !m_keywords.isEmpty(); } bool SearchQuery::hasKeywords(const QStringList &keywords) const { for (const QString &keyword : keywords) { if (m_keywords.contains(keyword, Qt::CaseInsensitive)) { return true; } } return false; } int SearchQuery::keywordPrefixSize() const { return m_keywordPrefix.size(); } QString SearchQuery::query() const { return m_query; } void SearchQuery::setQuery(const QString &str) { m_query = str; } QDataStream &operator<<(QDataStream &out, const Zeal::Registry::SearchQuery &query) { out << query.toString(); return out; } QDataStream &operator>>(QDataStream &in, Zeal::Registry::SearchQuery &query) { QString str; in >> str; query = SearchQuery::fromString(str); return in; } ================================================ FILE: src/libs/registry/searchquery.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_REGISTRY_SEARCHQUERY_H #define ZEAL_REGISTRY_SEARCHQUERY_H #include #include namespace Zeal { namespace Registry { /** * @short The search query model. */ class SearchQuery { public: explicit SearchQuery() = default; explicit SearchQuery(QString query, const QStringList &keywords = QStringList()); /// Creates a search query from a string. Single separator will be /// used to contstruct docset filter, but separator repeated twice /// will be left inside coreQuery part since double semicolon is /// used inside qualified symbol names in popular programming /// languages (c++, ruby, perl, etc.). /// /// Examples: /// "android:setTypeFa" #=> docsetFilters = ["android"], coreQuery = "setTypeFa" /// "noprefix" #=> docsetFilters = [], coreQuery = "noprefix" /// ":find" #=> docsetFilters = [], coreQuery = ":find" /// "std::string" #=> docsetFilters = [], coreQuery = "std::string" /// "c++:std::string" #=> docsetFilters = ["c++"], coreQuery = "std::string" /// /// Multiple docsets are supported using the ',' character: /// "java,android:setTypeFa #=> docsetFilters = ["java", "android"], coreQuery = "setTypeFa" static SearchQuery fromString(const QString &str); QString toString() const; bool isEmpty() const; QStringList keywords() const; void setKeywords(const QStringList &list); /// Returns true if there's a docset filter for the given query bool hasKeywords() const; /// Returns true if one the query contains one of the @c keywords. bool hasKeywords(const QStringList &keywords) const; /// Returns the docset filter raw size for the given query int keywordPrefixSize() const; QString query() const; void setQuery(const QString &str); private: QString m_query; QStringList m_keywords; QString m_keywordPrefix; }; } // namespace Registry } // namespace Zeal QDataStream &operator<<(QDataStream &out, const Zeal::Registry::SearchQuery &query); QDataStream &operator>>(QDataStream &in, Zeal::Registry::SearchQuery &query); #endif // ZEAL_REGISTRY_SEARCHQUERY_H ================================================ FILE: src/libs/registry/searchresult.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_REGISTRY_SEARCHRESULT_H #define ZEAL_REGISTRY_SEARCHRESULT_H #include #include #include namespace Zeal { namespace Registry { class Docset; struct SearchResult { QString name; QString type; QString urlPath; QString urlFragment; Docset *docset; double score; QVector matchPositions; inline bool operator<(const SearchResult &other) const { if (score == other.score) return QString::compare(name, other.name, Qt::CaseInsensitive) < 0; return score > other.score; } }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_SEARCHRESULT_H ================================================ FILE: src/libs/sidebar/CMakeLists.txt ================================================ add_library(Sidebar STATIC container.cpp proxyview.cpp view.cpp viewprovider.cpp ) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED) target_link_libraries(Sidebar PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) ================================================ FILE: src/libs/sidebar/container.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "container.h" #include "view.h" #include #include #include #include #include using namespace Zeal; using namespace Zeal::Sidebar; Container::Container(QWidget *parent) : QWidget(parent) { setMinimumWidth(150); // Setup splitter. m_splitter = new QSplitter(); m_splitter->setOrientation(Qt::Vertical); connect(m_splitter, &QSplitter::splitterMoved, this, [this]() { Core::Application::instance()->settings()->tocSplitterState = m_splitter->saveState(); }); // Setup main layout. auto layout = WidgetUi::LayoutHelper::createBorderlessLayout(); layout->addWidget(m_splitter); setLayout(layout); } Container::~Container() = default; void Container::addView(View *view) { if (m_views.contains(view)) return; m_views.append(view); m_splitter->addWidget(view); } ================================================ FILE: src/libs/sidebar/container.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_SIDEBAR_CONTAINER_H #define ZEAL_SIDEBAR_CONTAINER_H #include class QSplitter; namespace Zeal { namespace Sidebar { class View; // TODO: Implement view groups (alt. naming: tabs, pages) (move splitter into a group?). class Container : public QWidget { Q_OBJECT Q_DISABLE_COPY_MOVE(Container) public: explicit Container(QWidget *parent = nullptr); ~Container() override; void addView(View *view); private: QSplitter *m_splitter = nullptr; QList m_views; }; } // namespace Sidebar } // namespace Zeal #endif // ZEAL_SIDEBAR_CONTAINER_H ================================================ FILE: src/libs/sidebar/proxyview.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "proxyview.h" #include "viewprovider.h" #include #include #include #include using namespace Zeal; using namespace Zeal::Sidebar; static Q_LOGGING_CATEGORY(log, "zeal.sidebar.proxyview") ProxyView::ProxyView(ViewProvider *provider, QString id, QWidget *parent) : View(parent) , m_viewProvider(provider) , m_viewId(std::move(id)) { setLayout(WidgetUi::LayoutHelper::createBorderlessLayout()); connect(m_viewProvider, &ViewProvider::viewChanged, this, [this]() { auto view = m_viewProvider->view(m_viewId); if (view == nullptr) { qCWarning(log, "ViewProvider returned invalid view for id '%s'.", qPrintable(m_viewId)); return; } if (m_view == view) return; clearCurrentView(); layout()->addWidget(view); view->show(); m_view = view; }); } ProxyView::~ProxyView() { clearCurrentView(); } void ProxyView::clearCurrentView() { // Unparent the view, because we don't own it. QLayout *l = layout(); if (l->isEmpty() || m_view == nullptr) { return; } m_view->hide(); l->removeWidget(m_view); m_view->setParent(nullptr); m_view = nullptr; } ================================================ FILE: src/libs/sidebar/proxyview.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_SIDEBAR_PROXYVIEW_H #define ZEAL_SIDEBAR_PROXYVIEW_H #include "view.h" namespace Zeal { namespace Sidebar { class ViewProvider; class ProxyView final : public View { Q_OBJECT Q_DISABLE_COPY_MOVE(ProxyView) public: explicit ProxyView(ViewProvider *provider, QString id = QString(), QWidget *parent = nullptr); ~ProxyView() override; private: void clearCurrentView(); ViewProvider *m_viewProvider = nullptr; QString m_viewId; View *m_view = nullptr; }; } // namespace Sidebar } // namespace Zeal #endif // ZEAL_SIDEBAR_PROXYVIEW_H ================================================ FILE: src/libs/sidebar/view.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "view.h" using namespace Zeal::Sidebar; View::View(QWidget *parent) : QWidget(parent) { } ================================================ FILE: src/libs/sidebar/view.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_SIDEBAR_VIEW_H #define ZEAL_SIDEBAR_VIEW_H #include namespace Zeal { namespace Sidebar { class View : public QWidget { Q_OBJECT Q_DISABLE_COPY_MOVE(View) public: explicit View(QWidget *parent = nullptr); }; } // namespace Sidebar } // namespace Zeal #endif // ZEAL_SIDEBAR_VIEW_H ================================================ FILE: src/libs/sidebar/viewprovider.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "viewprovider.h" using namespace Zeal::Sidebar; ViewProvider::ViewProvider(QObject *parent) : QObject(parent) { } ================================================ FILE: src/libs/sidebar/viewprovider.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_SIDEBAR_VIEWPROVIDER_H #define ZEAL_SIDEBAR_VIEWPROVIDER_H #include namespace Zeal { namespace Sidebar { class View; class ViewProvider : public QObject { Q_OBJECT Q_DISABLE_COPY_MOVE(ViewProvider) public: explicit ViewProvider(QObject *parent = nullptr); virtual View *view(const QString &id = QString()) const = 0; signals: void viewChanged(); }; } // namespace Sidebar } // namespace Zeal #endif // ZEAL_SIDEBAR_VIEWPROVIDER_H ================================================ FILE: src/libs/ui/CMakeLists.txt ================================================ add_subdirectory(qxtglobalshortcut) add_subdirectory(widgets) set(Ui_FORMS aboutdialog.ui docsetsdialog.ui settingsdialog.ui ) add_library(Ui STATIC aboutdialog.cpp browsertab.cpp docsetlistitemdelegate.cpp docsetsdialog.cpp mainwindow.cpp searchitemdelegate.cpp searchsidebar.cpp settingsdialog.cpp sidebarviewprovider.cpp ${Ui_FORMS} # For Qt Creator. ) target_link_libraries(Ui PRIVATE Browser Sidebar QxtGlobalShortcut Widgets Registry Util) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS WebEngineWidgets REQUIRED) target_link_libraries(Ui PRIVATE Qt${QT_VERSION_MAJOR}::WebEngineWidgets) ================================================ FILE: src/libs/ui/aboutdialog.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "aboutdialog.h" #include "ui_aboutdialog.h" #include using namespace Zeal::WidgetUi; AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent) , ui(new Ui::AboutDialog) { ui->setupUi(this); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); ui->versionLabel->setText(Core::Application::versionString()); ui->buttonBox->setFocus(Qt::OtherFocusReason); } AboutDialog::~AboutDialog() { delete ui; } ================================================ FILE: src/libs/ui/aboutdialog.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_WIDGETUI_ABOUTDIALOG_H #define ZEAL_WIDGETUI_ABOUTDIALOG_H #include namespace Zeal { namespace WidgetUi { namespace Ui { class AboutDialog; } // namespace Ui class AboutDialog : public QDialog { Q_OBJECT public: explicit AboutDialog(QWidget *parent = nullptr); ~AboutDialog() override; private: Ui::AboutDialog *ui; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_ABOUTDIALOG_H ================================================ FILE: src/libs/ui/aboutdialog.ui ================================================ Zeal::WidgetUi::AboutDialog 0 0 420 420 About Zeal QFrame::StyledPanel QFrame::Sunken 10 :/icons/logo/64x64.png 0 <h1>Zeal</h1> Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse 0 About <strong>A simple offline documentation browser</strong> <br><br> Copyright &copy; Oleg Shparber and other contributors, 2013-2026. <br> <a href="https://zealdocs.org">zealdocs.org</a> <br> <a href="ircs://irc.libera.chat:6697/zealdocs">#zealdocs</a> on <a href="https://libera.chat">Libera Chat</a> <br><br> Zeal is an open source software available under the terms of the General Public License version 3 (<a href="https://www.gnu.org/copyleft/gpl.html">GPLv3</a>) or later. <br><br> Docsets are courtesy of <a href="https://kapeli.com/dash">Dash</a>. Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true true Licenses <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Roboto'; font-size:10pt; font-weight:400; font-style:normal;"> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;">Zeal heavily relies on other open source software listed below.</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">cpp-httplib</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://github.com/yhirose/cpp-httplib"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">github.com/yhirose/cpp-httplib</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: MIT License</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">libarchive</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://www.libarchive.org/"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">www.libarchive.org</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: Simplified BSD License</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">LibQxt</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://bitbucket.org/libqxt/libqxt"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">bitbucket.org/libqxt/libqxt</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: New BSD License</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">Lucide</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://lucide.dev/"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">lucide.dev</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: ISC License</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">Oat</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://oat.ink/"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">oat.ink</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: MIT License</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">Qt</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://www.qt.io/"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">www.qt.io</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: GNU LGPL version 3</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">Simple Icons</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://simpleicons.org/"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">simpleicons.org</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: CC0 1.0 Universal</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">SQLite</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://www.sqlite.org/"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">www.sqlite.org</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: Public Domain</span></p></body></html> true QDialogButtonBox::Close buttonBox clicked(QAbstractButton*) Zeal::WidgetUi::AboutDialog close() 248 254 157 274 ================================================ FILE: src/libs/ui/browsertab.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "browsertab.h" #include "searchsidebar.h" #include "widgets/layouthelper.h" #include "widgets/toolbarframe.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Zeal; using namespace Zeal::WidgetUi; namespace { constexpr char WelcomePageUrl[] = "qrc:///browser/welcome.html"; } // namespace BrowserTab::BrowserTab(QWidget *parent) : QWidget(parent) { // Setup WebControl. m_webControl = new Browser::WebControl(this); connect(m_webControl, &Browser::WebControl::titleChanged, this, &BrowserTab::titleChanged); connect(m_webControl, &Browser::WebControl::urlChanged, this, [this](const QUrl &url) { emit iconChanged(docsetIcon(url)); // Only update TOC if the base URL changed. const QUrl baseUrl = url.adjusted(QUrl::RemoveFragment); if (baseUrl != m_baseUrl) { m_baseUrl = baseUrl; Registry::Docset *docset = Core::Application::instance()->docsetRegistry()->docsetForUrl(url); if (docset) { searchSidebar()->pageTocModel()->setResults(docset->relatedLinks(url)); m_webControl->setJavaScriptEnabled(docset->isJavaScriptEnabled()); } else { searchSidebar()->pageTocModel()->setResults(); // Always enable JS outside of docsets. m_webControl->setJavaScriptEnabled(true); } } m_backButton->setEnabled(m_webControl->canGoBack()); m_forwardButton->setEnabled(m_webControl->canGoForward()); }); // Setup navigation toolbar. m_backButton = new QToolButton(); m_backButton->setAutoRaise(true); m_backButton->setIcon(qApp->style()->standardIcon(QStyle::SP_ArrowBack)); m_backButton->setStyleSheet(QStringLiteral("QToolButton::menu-indicator { image: none; }")); m_backButton->setText(QStringLiteral("←")); m_backButton->setToolTip(tr("Go back one page")); auto backMenu = new QMenu(m_backButton); connect(backMenu, &QMenu::aboutToShow, this, [this, backMenu]() { backMenu->clear(); QWebEngineHistory *history = m_webControl->history(); QList items = history->backItems(10); for (auto it = items.crbegin(); it != items.crend(); ++it) { const QIcon icon = docsetIcon(it->url()); const QWebEngineHistoryItem item = *it; backMenu->addAction(icon, it->title(), this, [=](bool) { history->goToItem(item); }); } }); m_backButton->setMenu(backMenu); connect(m_backButton, &QToolButton::clicked, m_webControl, &Browser::WebControl::back); m_forwardButton = new QToolButton(); m_forwardButton->setAutoRaise(true); m_forwardButton->setIcon(qApp->style()->standardIcon(QStyle::SP_ArrowForward)); m_forwardButton->setStyleSheet(QStringLiteral("QToolButton::menu-indicator { image: none; }")); m_forwardButton->setText(QStringLiteral("→")); m_forwardButton->setToolTip(tr("Go forward one page")); auto forwardMenu = new QMenu(m_forwardButton); connect(forwardMenu, &QMenu::aboutToShow, this, [this, forwardMenu]() { forwardMenu->clear(); QWebEngineHistory *history = m_webControl->history(); const auto forwardItems = history->forwardItems(10); for (const QWebEngineHistoryItem &item : forwardItems) { const QIcon icon = docsetIcon(item.url()); forwardMenu->addAction(icon, item.title(), this, [=](bool) { history->goToItem(item); }); } }); m_forwardButton->setMenu(forwardMenu); connect(m_forwardButton, &QToolButton::clicked, m_webControl, &Browser::WebControl::forward); auto label = new QLabel(); label->setAlignment(Qt::AlignCenter); connect(m_webControl, &Browser::WebControl::titleChanged, this, [label](const QString &title) { if (title.isEmpty()) return; label->setText(title); }); auto toolBarLayout = new QHBoxLayout(); toolBarLayout->setContentsMargins(4, 0, 4, 0); toolBarLayout->setSpacing(4); toolBarLayout->addWidget(m_backButton); toolBarLayout->addWidget(m_forwardButton); toolBarLayout->addWidget(label, 1); auto toolBarFrame = new ToolBarFrame(); toolBarFrame->setLayout(toolBarLayout); // Setup main layout. auto layout = LayoutHelper::createBorderlessLayout(); layout->addWidget(toolBarFrame); layout->addWidget(m_webControl); setLayout(layout); auto registry = Core::Application::instance()->docsetRegistry(); using Registry::DocsetRegistry; connect(registry, &DocsetRegistry::docsetAboutToBeUnloaded, this, [this, registry](const QString &name) { Registry::Docset *docset = registry->docsetForUrl(m_webControl->url()); if (docset == nullptr || docset->name() != name) { return; } // TODO: Add custom 'Page has been removed' page. navigateToStartPage(); // TODO: Cleanup history. }); } BrowserTab *BrowserTab::clone(QWidget *parent) const { auto tab = new BrowserTab(parent); if (m_searchSidebar) { tab->m_searchSidebar = m_searchSidebar->clone(); connect(tab->m_searchSidebar, &SearchSidebar::activated, tab->m_webControl, &Browser::WebControl::focus); connect(tab->m_searchSidebar, &SearchSidebar::navigationRequested, tab->m_webControl, &Browser::WebControl::load); } tab->m_webControl->restoreHistory(m_webControl->saveHistory()); tab->m_webControl->setZoomLevel(m_webControl->zoomLevel()); return tab; } BrowserTab::~BrowserTab() { if (m_searchSidebar) { // The sidebar is not in this widget's hierarchy, so direct delete is not safe. m_searchSidebar->deleteLater(); } } Browser::WebControl *BrowserTab::webControl() const { return m_webControl; } SearchSidebar *BrowserTab::searchSidebar() { if (m_searchSidebar == nullptr) { // Create SearchSidebar managed by this tab. m_searchSidebar = new SearchSidebar(); connect(m_searchSidebar, &SearchSidebar::activated, m_webControl, &Browser::WebControl::focus); connect(m_searchSidebar, &SearchSidebar::navigationRequested, m_webControl, &Browser::WebControl::load); } return m_searchSidebar; } void BrowserTab::navigateToStartPage() { m_webControl->load(QUrl(WelcomePageUrl)); } void BrowserTab::search(const Registry::SearchQuery &query) { if (query.isEmpty()) return; m_searchSidebar->search(query); } QIcon BrowserTab::docsetIcon(const QUrl &url) const { Registry::Docset *docset = Core::Application::instance()->docsetRegistry()->docsetForUrl(url); return docset ? docset->icon() : QIcon(QStringLiteral(":/icons/logo/icon.png")); } ================================================ FILE: src/libs/ui/browsertab.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_WIDGETUI_BROWSERTAB_H #define ZEAL_WIDGETUI_BROWSERTAB_H #include #include #include class QToolButton; namespace Zeal { namespace Browser { class WebControl; } // namespace Browser namespace Registry { class SearchQuery; } //namespace Registry namespace WidgetUi { class SearchSidebar; class BrowserTab : public QWidget { Q_OBJECT Q_DISABLE_COPY_MOVE(BrowserTab) public: explicit BrowserTab(QWidget *parent = nullptr); BrowserTab *clone(QWidget *parent = nullptr) const; ~BrowserTab() override; Browser::WebControl *webControl() const; SearchSidebar *searchSidebar(); // TODO: const public slots: void navigateToStartPage(); void search(const Registry::SearchQuery &query); signals: void iconChanged(const QIcon &icon); void titleChanged(const QString &title); private: QIcon docsetIcon(const QUrl &url) const; // Widgets. SearchSidebar *m_searchSidebar = nullptr; Browser::WebControl *m_webControl = nullptr; QToolButton *m_backButton = nullptr; QToolButton *m_forwardButton = nullptr; // State. QUrl m_baseUrl; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_BROWSERTAB_H ================================================ FILE: src/libs/ui/docsetlistitemdelegate.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "docsetlistitemdelegate.h" #include #include #include using namespace Zeal::WidgetUi; namespace { constexpr int ProgressBarWidth = 150; } // namespace DocsetListItemDelegate::DocsetListItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { } void DocsetListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (index.model()->data(index, ShowProgressRole).toBool()) { paintProgressBar(painter, option, index); return; } if (index.column() != Registry::SectionIndex::Actions) { QStyledItemDelegate::paint(painter, option, index); return; } QStyledItemDelegate::paint(painter, option, index); if (!index.model()->data(index, Registry::ItemDataRole::UpdateAvailableRole).toBool()) { return; } const QString text = tr("Update available"); QFont font(painter->font()); font.setItalic(true); const QFontMetrics fontMetrics(font); const int margin = 4; // Random small number QRect textRect = option.rect.adjusted(-margin, 0, -margin, 0); textRect.setLeft(textRect.right() - fontMetrics.horizontalAdvance(text) - 2); textRect = QStyle::visualRect(option.direction, option.rect, textRect); // Constant LeftToRight because we don't need to flip it any further. // Vertically align the text in the middle to match QCommonStyle behavior. const auto alignedRect = QStyle::alignedRect(Qt::LeftToRight, option.displayAlignment, QSize(textRect.size().width(), fontMetrics.height()), textRect); painter->save(); QPalette palette = option.palette; #ifdef Q_OS_WINDOWS // QWindowsVistaStyle overrides highlight color. if (option.widget->style()->objectName() == QLatin1String("windowsvista")) { palette.setColor(QPalette::All, QPalette::HighlightedText, palette.color(QPalette::Active, QPalette::Text)); } #endif const QPalette::ColorGroup cg = (option.state & QStyle::State_Active) ? QPalette::Normal : QPalette::Inactive; if (option.state & QStyle::State_Selected) { painter->setPen(palette.color(cg, QPalette::HighlightedText)); } else { painter->setPen(palette.color(cg, QPalette::Text)); } painter->setFont(font); painter->drawText(alignedRect, text); painter->restore(); } void DocsetListItemDelegate::paintProgressBar(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { bool ok; const int value = index.model()->data(index, ValueRole).toInt(&ok); if (!ok) { QStyledItemDelegate::paint(painter, option, index); return; } // Adjust maximum text width QStyleOptionViewItem styleOption = option; styleOption.rect.setRight(styleOption.rect.right() - ProgressBarWidth); // Size progress bar QScopedPointer renderer(new QProgressBar()); renderer->resize(ProgressBarWidth, styleOption.rect.height()); renderer->setRange(0, 100); renderer->setValue(value); const QString format = index.model()->data(index, FormatRole).toString(); if (!format.isEmpty()) { renderer->setFormat(format); } painter->save(); // Paint progress bar painter->translate(styleOption.rect.topRight()); renderer->render(painter); painter->restore(); QStyledItemDelegate::paint(painter, styleOption, index); } ================================================ FILE: src/libs/ui/docsetlistitemdelegate.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_WIDGETUI_DOCSETLISTITEMDELEGATE_H #define ZEAL_WIDGETUI_DOCSETLISTITEMDELEGATE_H #include namespace Zeal { namespace WidgetUi { class DocsetListItemDelegate : public QStyledItemDelegate { Q_OBJECT public: enum ProgressRoles { ValueRole = Qt::UserRole + 10, FormatRole, ShowProgressRole }; explicit DocsetListItemDelegate(QObject *parent = nullptr); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; private: void paintProgressBar(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_DOCSETLISTITEMDELEGATE_H ================================================ FILE: src/libs/ui/docsetsdialog.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "docsetsdialog.h" #include "ui_docsetsdialog.h" #include "docsetlistitemdelegate.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Zeal; using namespace Zeal::WidgetUi; static Q_LOGGING_CATEGORY(log, "zeal.widgetui.docsetsdialog") #ifdef Q_OS_WINDOWS extern Q_CORE_EXPORT int qt_ntfs_permission_lookup; #endif namespace { constexpr char ApiServerUrl[] = "https://api.zealdocs.org/v1"; constexpr char RedirectServerUrl[] = "https://go.zealdocs.org/d/%1/%2/latest"; // TODO: Each source plugin should have its own cache constexpr char DocsetListCacheFileName[] = "com.kapeli.json"; // TODO: Make the timeout period configurable constexpr int CacheTimeout = 24 * 60 * 60; // 24 hours in seconds // QNetworkReply properties constexpr char DocsetNameProperty[] = "docsetName"; constexpr char DownloadTypeProperty[] = "downloadType"; constexpr char ListItemIndexProperty[] = "listItem"; } // namespace DocsetsDialog::DocsetsDialog(Core::Application *app, QWidget *parent) : QDialog(parent) , ui(new Ui::DocsetsDialog()) , m_application(app) , m_docsetRegistry(app->docsetRegistry()) { ui->setupUi(this); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); loadDocsetList(); m_isStorageReadOnly = !isDirWritable(m_application->settings()->docsetPath); #ifdef Q_OS_MACOS ui->availableDocsetList->setAttribute(Qt::WA_MacShowFocusRect, false); ui->installedDocsetList->setAttribute(Qt::WA_MacShowFocusRect, false); #endif ui->statusLabel->clear(); // Clear text shown in the designer mode. ui->storageStatusLabel->setVisible(m_isStorageReadOnly); const QFileInfo fi(m_application->settings()->docsetPath); ui->storageStatusLabel->setText(fi.exists() ? tr("Docset storage is read only.") : tr("Docset storage does not exist.")); connect(m_application, &Core::Application::extractionCompleted, this, &DocsetsDialog::extractionCompleted); connect(m_application, &Core::Application::extractionError, this, &DocsetsDialog::extractionError); connect(m_application, &Core::Application::extractionProgress, this, &DocsetsDialog::extractionProgress); // Setup signals & slots connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton *button) { if (button == ui->buttonBox->button(QDialogButtonBox::Cancel)) { cancelDownloads(); return; } if (button == ui->buttonBox->button(QDialogButtonBox::Close)) { close(); return; } }); setupInstalledDocsetsTab(); setupAvailableDocsetsTab(); if (m_isStorageReadOnly) { disableControls(); } } DocsetsDialog::~DocsetsDialog() { delete ui; } void DocsetsDialog::addDashFeed() { QString clipboardText = QApplication::clipboard()->text(); if (!clipboardText.startsWith(QLatin1String("dash-feed://"))) clipboardText.clear(); QString feedUrl = QInputDialog::getText(this, QStringLiteral("Zeal"), tr("Feed URL:"), QLineEdit::Normal, clipboardText); feedUrl = feedUrl.trimmed(); if (feedUrl.isEmpty()) return; if (feedUrl.startsWith(QLatin1String("dash-feed://"))) { feedUrl = feedUrl.remove(0, 12); feedUrl = QUrl::fromPercentEncoding(feedUrl.toUtf8()); } QNetworkReply *reply = download(QUrl(feedUrl)); reply->setProperty(DownloadTypeProperty, DownloadDashFeed); } void DocsetsDialog::updateSelectedDocsets() { const auto selectedRows = ui->installedDocsetList->selectionModel()->selectedRows(); for (const QModelIndex &index : selectedRows) { if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) continue; downloadDashDocset(index); } } void DocsetsDialog::updateAllDocsets() { QAbstractItemModel *model = ui->installedDocsetList->model(); for (int i = 0; i < model->rowCount(); ++i) { const QModelIndex index = model->index(i, 0); if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) continue; downloadDashDocset(index); } } void DocsetsDialog::removeSelectedDocsets() { QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel(); if (!selectionModel->hasSelection()) return; int ret; const QModelIndexList selectedIndexes = selectionModel->selectedRows(); if (selectedIndexes.size() == 1) { const QString docsetTitle = selectedIndexes.first().data().toString(); ret = QMessageBox::question(this, QStringLiteral("Zeal"), tr("Remove %1 docset?").arg(docsetTitle)); } else { ret = QMessageBox::question(this, QStringLiteral("Zeal"), tr("Remove %n docset(s)?", nullptr, selectedIndexes.size())); } if (ret == QMessageBox::No) { return; } // Gather names first, because model indicies become invalid when docsets are removed. QStringList names; for (const QModelIndex &index : selectedIndexes) { names.append(index.data(Registry::ItemDataRole::DocsetNameRole).toString()); } for (const QString &name : names) { removeDocset(name); } } void DocsetsDialog::updateDocsetFilter(const QString &filterString) { const bool doSearch = !filterString.simplified().isEmpty(); for (int i = 0; i < ui->availableDocsetList->count(); ++i) { QListWidgetItem *item = ui->availableDocsetList->item(i); // Skip installed docsets if (m_docsetRegistry->contains(item->data(Registry::ItemDataRole::DocsetNameRole).toString())) continue; item->setHidden(doSearch && !item->text().contains(filterString, Qt::CaseInsensitive)); } } void DocsetsDialog::downloadSelectedDocsets() { QItemSelectionModel *selectionModel = ui->availableDocsetList->selectionModel(); const auto selectedRows = selectionModel->selectedRows(); for (const QModelIndex &index : selectedRows) { selectionModel->select(index, QItemSelectionModel::Deselect); // Do nothing if a download is already in progress. if (index.data(DocsetListItemDelegate::ShowProgressRole).toBool()) continue; QAbstractItemModel *model = ui->availableDocsetList->model(); model->setData(index, tr("Downloading: %p%"), DocsetListItemDelegate::FormatRole); model->setData(index, 0, DocsetListItemDelegate::ValueRole); model->setData(index, true, DocsetListItemDelegate::ShowProgressRole); downloadDashDocset(index); } } /*! \internal Should be connected to all \l QNetworkReply::finished signals in order to process possible HTTP-redirects correctly. */ void DocsetsDialog::downloadCompleted() { QScopedPointer reply( qobject_cast(sender())); m_replies.removeOne(reply.data()); if (reply->error() != QNetworkReply::NoError) { if (reply->error() != QNetworkReply::OperationCanceledError) { const QString msg = tr("Download failed!

Error: %1
URL: %2") .arg(reply->errorString(), reply->request().url().toString()); const int ret = QMessageBox::warning(this, QStringLiteral("Zeal"), msg, QMessageBox::Retry | QMessageBox::Cancel); if (ret == QMessageBox::Retry) { QNetworkReply *newReply = download(reply->request().url()); // Copy properties newReply->setProperty(DocsetNameProperty, reply->property(DocsetNameProperty)); newReply->setProperty(DownloadTypeProperty, reply->property(DownloadTypeProperty)); newReply->setProperty(ListItemIndexProperty, reply->property(ListItemIndexProperty)); return; } bool ok; QListWidgetItem *listItem = ui->availableDocsetList->item( reply->property(ListItemIndexProperty).toInt(&ok)); if (ok && listItem) listItem->setData(DocsetListItemDelegate::ShowProgressRole, false); } updateStatus(); return; } switch (reply->property(DownloadTypeProperty).toUInt()) { case DownloadDocsetList: { const QByteArray replyData = reply->readAll(); QScopedPointer file(new QFile(cacheLocation(DocsetListCacheFileName))); if (file->open(QIODevice::WriteOnly)) { file->write(replyData); file->close(); // Flush to ensure timestamp update on all systems. } updateDocsetListDownloadTimeLabel(QFileInfo(file->fileName()).lastModified()); QJsonParseError jsonError; const QJsonDocument jsonDoc = QJsonDocument::fromJson(replyData, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qCWarning(log, "Failed to parse docset list JSON at offset %lld: %s.", static_cast(jsonError.offset), qPrintable(jsonError.errorString())); const QMessageBox::StandardButton rc = QMessageBox::warning(this, QStringLiteral("Zeal"), tr("Server returned a corrupted docset list."), QMessageBox::Retry | QMessageBox::Cancel); if (rc == QMessageBox::Retry) { downloadDocsetList(); } break; } processDocsetList(jsonDoc.array()); break; } case DownloadDashFeed: { Registry::DocsetMetadata metadata = Registry::DocsetMetadata::fromDashFeed(reply->request().url(), reply->readAll()); if (metadata.urls().isEmpty()) { QMessageBox::warning(this, QStringLiteral("Zeal"), tr("Invalid docset feed!")); break; } m_userFeeds[metadata.name()] = metadata; Registry::Docset *docset = m_docsetRegistry->docset(metadata.name()); if (docset == nullptr) { // Fetch docset only on first feed download, // since further downloads are only update checks QNetworkReply *mdReply = download(metadata.url()); mdReply->setProperty(DocsetNameProperty, metadata.name()); mdReply->setProperty(DownloadTypeProperty, DownloadDocset); } else { // Check for feed update if (metadata.latestVersion() != docset->version() || metadata.revision() > docset->revision()) { docset->hasUpdate = true; if (!m_isStorageReadOnly) { ui->updateAllDocsetsButton->setEnabled(true); } ui->installedDocsetList->reset(); } } break; } case DownloadDocset: { const QString docsetName = reply->property(DocsetNameProperty).toString(); const QString docsetDirectoryName = docsetName + QLatin1String(".docset"); if (QDir(m_application->settings()->docsetPath).exists(docsetDirectoryName)) { removeDocset(docsetName); } QTemporaryFile *tmpFile = m_tmpFiles[docsetName]; if (!tmpFile) { tmpFile = new QTemporaryFile(QStringLiteral("%1/%2.XXXXXX.tmp").arg(Core::Application::cacheLocation(), docsetName), this); if (!tmpFile->open()) return; m_tmpFiles.insert(docsetName, tmpFile); } while (reply->bytesAvailable()) { tmpFile->write(reply->read(1024 * 1024)); // Use small chunks. } tmpFile->close(); QListWidgetItem *item = findDocsetListItem(docsetName); if (item) { item->setData(DocsetListItemDelegate::ValueRole, 0); item->setData(DocsetListItemDelegate::FormatRole, tr("Installing: %p%")); } m_application->extract(tmpFile->fileName(), m_application->settings()->docsetPath, docsetDirectoryName); break; } } // If all enqueued downloads have finished executing. updateStatus(); } // creates a total download progress for multiple QNetworkReplies void DocsetsDialog::downloadProgress(qint64 received, qint64 total) { // Don't show progress for non-docset pages if (total == -1 || received < 10240) return; auto reply = qobject_cast(sender()); if (!reply || !reply->isOpen()) return; if (reply->property(DownloadTypeProperty).toInt() == DownloadDocset) { const QString docsetName = reply->property(DocsetNameProperty).toString(); QTemporaryFile *tmpFile = m_tmpFiles[docsetName]; if (!tmpFile) { tmpFile = new QTemporaryFile(QStringLiteral("%1/%2.XXXXXX.tmp").arg(Core::Application::cacheLocation(), docsetName), this); if (!tmpFile->open()) return; m_tmpFiles.insert(docsetName, tmpFile); } tmpFile->write(reply->read(received)); } // Try to get the item associated to the request QListWidgetItem *item = ui->availableDocsetList->item(reply->property(ListItemIndexProperty).toInt()); if (item) { item->setData(DocsetListItemDelegate::ValueRole, percent(received, total)); } } void DocsetsDialog::extractionCompleted(const QString &filePath) { const QString docsetName = docsetNameForTmpFilePath(filePath); const QDir dataDir(m_application->settings()->docsetPath); const QString docsetPath = dataDir.filePath(docsetName + QLatin1String(".docset")); // Write metadata about docset Registry::DocsetMetadata metadata = m_availableDocsets.count(docsetName) ? m_availableDocsets[docsetName] : m_userFeeds[docsetName]; metadata.save(docsetPath, metadata.latestVersion()); m_docsetRegistry->loadDocset(docsetPath); QListWidgetItem *listItem = findDocsetListItem(docsetName); if (listItem) { listItem->setHidden(true); listItem->setData(DocsetListItemDelegate::ShowProgressRole, false); } delete m_tmpFiles.take(docsetName); updateStatus(); } void DocsetsDialog::extractionError(const QString &filePath, const QString &errorString) { const QString docsetName = docsetNameForTmpFilePath(filePath); QMessageBox::warning(this, QStringLiteral("Zeal"), tr("Cannot extract docset %1: %2").arg(docsetName, errorString)); QListWidgetItem *listItem = findDocsetListItem(docsetName); if (listItem) listItem->setData(DocsetListItemDelegate::ShowProgressRole, false); delete m_tmpFiles.take(docsetName); } void DocsetsDialog::extractionProgress(const QString &filePath, qint64 extracted, qint64 total) { const QString docsetName = docsetNameForTmpFilePath(filePath); QListWidgetItem *listItem = findDocsetListItem(docsetName); if (listItem) listItem->setData(DocsetListItemDelegate::ValueRole, percent(extracted, total)); } void DocsetsDialog::loadDocsetList() { loadUserFeedList(); const QFileInfo fi(cacheLocation(DocsetListCacheFileName)); if (!fi.exists()) { downloadDocsetList(); return; } const auto age = fi.lastModified().secsTo(QDateTime::currentDateTime()); if (age < 0 || age >= CacheTimeout) { downloadDocsetList(); return; } QScopedPointer file(new QFile(fi.filePath())); if (!file->open(QIODevice::ReadOnly)) { downloadDocsetList(); return; } QJsonParseError jsonError; const QJsonDocument jsonDoc = QJsonDocument::fromJson(file->readAll(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { downloadDocsetList(); return; } updateDocsetListDownloadTimeLabel(fi.lastModified()); processDocsetList(jsonDoc.array()); } void DocsetsDialog::setupInstalledDocsetsTab() { ui->installedDocsetList->setItemDelegate(new DocsetListItemDelegate(this)); ui->installedDocsetList->setModel(m_docsetRegistry->model()); ui->installedDocsetList->setItemsExpandable(false); ui->installedDocsetList->setRootIsDecorated(false); ui->installedDocsetList->header()->setStretchLastSection(true); ui->installedDocsetList->header()->setSectionsMovable(false); ui->installedDocsetList->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); ui->installedDocsetList->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); if (m_isStorageReadOnly) { return; } connect(ui->installedDocsetList, &QTreeView::activated, this, [this](const QModelIndex &index) { if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) { return; } downloadDashDocset(index); }); QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel(); connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this, selectionModel]() { ui->removeDocsetsButton->setEnabled(selectionModel->hasSelection()); const auto selectedRows = selectionModel->selectedRows(); for (const QModelIndex &index : selectedRows) { if (index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) { ui->updateSelectedDocsetsButton->setEnabled(true); return; } } ui->updateSelectedDocsetsButton->setEnabled(false); }); connect(ui->addFeedButton, &QPushButton::clicked, this, &DocsetsDialog::addDashFeed); connect(ui->updateSelectedDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::updateSelectedDocsets); connect(ui->updateAllDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::updateAllDocsets); connect(ui->removeDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::removeSelectedDocsets); } void DocsetsDialog::setupAvailableDocsetsTab() { using Registry::DocsetRegistry; ui->availableDocsetList->setItemDelegate(new DocsetListItemDelegate(this)); connect(m_docsetRegistry, &DocsetRegistry::docsetUnloaded, this, [this](const QString &name) { QListWidgetItem *item = findDocsetListItem(name); if (!item) return; item->setHidden(false); }); connect(m_docsetRegistry, &DocsetRegistry::docsetLoaded, this, [this](const QString &name) { QListWidgetItem *item = findDocsetListItem(name); if (!item) return; item->setHidden(true); }); connect(ui->refreshButton, &QPushButton::clicked, this, &DocsetsDialog::downloadDocsetList); connect(ui->docsetFilterInput, &QLineEdit::textEdited, this, &DocsetsDialog::updateDocsetFilter); if (m_isStorageReadOnly) { return; } connect(ui->availableDocsetList, &QListView::activated, this, [this](const QModelIndex &index) { // TODO: Cancel download if it's already in progress. if (index.data(DocsetListItemDelegate::ShowProgressRole).toBool()) return; ui->availableDocsetList->selectionModel()->select(index, QItemSelectionModel::Deselect); QAbstractItemModel *model = ui->availableDocsetList->model(); model->setData(index, tr("Downloading: %p%"), DocsetListItemDelegate::FormatRole); model->setData(index, 0, DocsetListItemDelegate::ValueRole); model->setData(index, true, DocsetListItemDelegate::ShowProgressRole); downloadDashDocset(index); }); QItemSelectionModel *selectionModel = ui->availableDocsetList->selectionModel(); connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this, selectionModel]() { const auto selectedRows = selectionModel->selectedRows(); for (const QModelIndex &index : selectedRows) { if (!index.data(DocsetListItemDelegate::ShowProgressRole).toBool()) { ui->downloadDocsetsButton->setEnabled(true); return; } } ui->downloadDocsetsButton->setEnabled(false); }); connect(ui->downloadDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::downloadSelectedDocsets); } void DocsetsDialog::enableControls() { if (m_isStorageReadOnly || !m_replies.isEmpty() || !m_tmpFiles.isEmpty()) { return; } // Dialog buttons. ui->buttonBox->setStandardButtons(QDialogButtonBox::Close); // Available docsets ui->refreshButton->setEnabled(true); // Installed docsets ui->addFeedButton->setEnabled(true); QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel(); bool hasSelectedUpdates = false; const auto selectedRows = selectionModel->selectedRows(); for (const QModelIndex &index : selectedRows) { if (index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) { hasSelectedUpdates = true; break; } } ui->updateSelectedDocsetsButton->setEnabled(hasSelectedUpdates); ui->updateAllDocsetsButton->setEnabled(updatesAvailable()); ui->removeDocsetsButton->setEnabled(selectionModel->hasSelection()); } void DocsetsDialog::disableControls() { // Dialog buttons. if (!m_isStorageReadOnly) { // Always show the close button if storage is read only. ui->buttonBox->setStandardButtons(QDialogButtonBox::Cancel); } // Installed docsets ui->addFeedButton->setEnabled(false); ui->updateSelectedDocsetsButton->setEnabled(false); ui->updateAllDocsetsButton->setEnabled(false); ui->downloadDocsetsButton->setEnabled(false); ui->removeDocsetsButton->setEnabled(false); // Available docsets ui->refreshButton->setEnabled(false); } QListWidgetItem *DocsetsDialog::findDocsetListItem(const QString &name) const { for (int i = 0; i < ui->availableDocsetList->count(); ++i) { QListWidgetItem *item = ui->availableDocsetList->item(i); if (item->data(Registry::ItemDataRole::DocsetNameRole).toString() == name) return item; } return nullptr; } bool DocsetsDialog::updatesAvailable() const { const auto docsets = m_docsetRegistry->docsets(); for (Registry::Docset *docset : docsets) { if (docset->hasUpdate) return true; } return false; } QNetworkReply *DocsetsDialog::download(const QUrl &url) { QNetworkReply *reply = m_application->download(url); connect(reply, &QNetworkReply::downloadProgress, this, &DocsetsDialog::downloadProgress); connect(reply, &QNetworkReply::finished, this, &DocsetsDialog::downloadCompleted); m_replies.append(reply); disableControls(); updateStatus(); return reply; } void DocsetsDialog::cancelDownloads() { for (QNetworkReply *reply : std::as_const(m_replies)) { // Hide progress bar QListWidgetItem *listItem = ui->availableDocsetList->item(reply->property(ListItemIndexProperty).toInt()); if (listItem) listItem->setData(DocsetListItemDelegate::ShowProgressRole, false); if (reply->property(DownloadTypeProperty).toInt() == DownloadDocset) delete m_tmpFiles.take(reply->property(DocsetNameProperty).toString()); reply->abort(); } updateStatus(); } void DocsetsDialog::loadUserFeedList() { const auto docsets = m_docsetRegistry->docsets(); for (Registry::Docset *docset : docsets) { if (!docset->feedUrl().isEmpty()) { QNetworkReply *reply = download(QUrl(docset->feedUrl())); reply->setProperty(DownloadTypeProperty, DownloadDashFeed); } } } void DocsetsDialog::downloadDocsetList() { ui->availableDocsetList->clear(); m_availableDocsets.clear(); QNetworkReply *reply = download(QUrl(ApiServerUrl + QLatin1String("/docsets"))); reply->setProperty(DownloadTypeProperty, DownloadDocsetList); } void DocsetsDialog::processDocsetList(const QJsonArray &list) { for (const QJsonValue &v : list) { QJsonObject docsetJson = v.toObject(); Registry::DocsetMetadata metadata(docsetJson); m_availableDocsets.insert({metadata.name(), metadata}); } // TODO: Move into dedicated method for (const auto &kv : m_availableDocsets) { const auto &metadata = kv.second; auto listItem = new QListWidgetItem(metadata.icon(), metadata.title(), ui->availableDocsetList); listItem->setData(Registry::ItemDataRole::DocsetNameRole, metadata.name()); if (!m_docsetRegistry->contains(metadata.name())) { continue; } listItem->setHidden(true); Registry::Docset *docset = m_docsetRegistry->docset(metadata.name()); if (metadata.latestVersion() != docset->version() || metadata.revision() > docset->revision()) { docset->hasUpdate = true; if (!m_isStorageReadOnly) { ui->updateAllDocsetsButton->setEnabled(true); } } } ui->installedDocsetList->reset(); // Reapply the filter after repopulating the list. updateDocsetFilter(ui->docsetFilterInput->text()); } void DocsetsDialog::updateDocsetListDownloadTimeLabel(const QDateTime &modifiedTime) { if (!modifiedTime.isValid()) { ui->lastUpdatedLabel->clear(); ui->lastUpdatedLabel->setToolTip(QString()); return; } ui->lastUpdatedLabel->setText(tr("Last updated %1.").arg(Util::Humanizer::fromNow(modifiedTime))); const QString updateTime = modifiedTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)); ui->lastUpdatedLabel->setToolTip(updateTime); } void DocsetsDialog::downloadDashDocset(const QModelIndex &index) { const QString name = index.data(Registry::ItemDataRole::DocsetNameRole).toString(); if (m_availableDocsets.count(name) == 0 && !m_userFeeds.contains(name)) return; QUrl url; if (!m_userFeeds.contains(name)) { // No feed present means that this is a Kapeli docset QString urlString = QString(RedirectServerUrl).arg("com.kapeli").arg(name); url = QUrl(urlString); } else { url = m_userFeeds[name].url(); } QNetworkReply *reply = download(url); reply->setProperty(DocsetNameProperty, name); reply->setProperty(DownloadTypeProperty, DownloadDocset); reply->setProperty(ListItemIndexProperty, ui->availableDocsetList->row(findDocsetListItem(name))); } void DocsetsDialog::removeDocset(const QString &name) { if (m_docsetRegistry->contains(name)) { m_docsetRegistry->unloadDocset(name); } const QString docsetPath = QDir(m_application->settings()->docsetPath).filePath(name + QLatin1String(".docset")); if (!m_application->fileManager()->removeRecursively(docsetPath)) { const QString error = tr("Cannot remove directory %1! It might be in use" " by another process.").arg(docsetPath); QMessageBox::warning(this, QStringLiteral("Zeal"), error); return; } QListWidgetItem *listItem = findDocsetListItem(name); if (listItem) { listItem->setHidden(false); } } void DocsetsDialog::updateStatus() { QString text; if (!m_replies.isEmpty()) { text = tr("Downloading: %n.", nullptr, m_replies.size()); } if (!m_tmpFiles.isEmpty()) { text += QLatin1String(" ") + tr("Installing: %n.", nullptr, m_tmpFiles.size()); } ui->statusLabel->setText(text); enableControls(); } QString DocsetsDialog::docsetNameForTmpFilePath(const QString &filePath) const { for (auto it = m_tmpFiles.cbegin(), end = m_tmpFiles.cend(); it != end; ++it) { if (it.value()->fileName() == filePath) { return it.key(); } } return QString(); } int DocsetsDialog::percent(qint64 fraction, qint64 total) { if (!total) return 0; return static_cast(fraction / static_cast(total) * 100); } QString DocsetsDialog::cacheLocation(const QString &fileName) { return QDir(Core::Application::cacheLocation()).filePath(fileName); } bool DocsetsDialog::isDirWritable(const QString &path) { auto file = std::make_unique(path + QLatin1String("/.zeal_writable_check_XXXXXX.tmp")); return file->open(); } ================================================ FILE: src/libs/ui/docsetsdialog.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_WIDGETUI_DOCSETSDIALOG_H #define ZEAL_WIDGETUI_DOCSETSDIALOG_H #include #include #include #include #include class QDateTime; class QListWidgetItem; class QNetworkReply; class QTemporaryFile; class QUrl; namespace Zeal { namespace Registry { class DocsetRegistry; } // namespace Registry namespace Core { class Application; } namespace WidgetUi { namespace Ui { class DocsetsDialog; } // namespace Ui class DocsetsDialog : public QDialog { Q_OBJECT public: explicit DocsetsDialog(Core::Application *app, QWidget *parent = nullptr); ~DocsetsDialog() override; private slots: void addDashFeed(); void updateSelectedDocsets(); void updateAllDocsets(); void removeSelectedDocsets(); void updateDocsetFilter(const QString &filterString); void downloadSelectedDocsets(); void downloadCompleted(); void downloadProgress(qint64 received, qint64 total); void extractionCompleted(const QString &filePath); void extractionError(const QString &filePath, const QString &errorString); void extractionProgress(const QString &filePath, qint64 extracted, qint64 total); void loadDocsetList(); private: enum DownloadType { DownloadDashFeed, DownloadDocset, DownloadDocsetList }; Ui::DocsetsDialog *ui = nullptr; Core::Application *m_application = nullptr; Registry::DocsetRegistry *m_docsetRegistry = nullptr; bool m_isStorageReadOnly = false; QList m_replies; // TODO: Create a special model Util::CaseInsensitiveMap m_availableDocsets; QMap m_userFeeds; QHash m_tmpFiles; void setupInstalledDocsetsTab(); void setupAvailableDocsetsTab(); void enableControls(); void disableControls(); QListWidgetItem *findDocsetListItem(const QString &name) const; bool updatesAvailable() const; QNetworkReply *download(const QUrl &url); void cancelDownloads(); void loadUserFeedList(); void downloadDocsetList(); void processDocsetList(const QJsonArray &list); void updateDocsetListDownloadTimeLabel(const QDateTime &modifiedTime); void downloadDashDocset(const QModelIndex &index); void removeDocset(const QString &name); void updateStatus(); // FIXME: Come up with a better approach QString docsetNameForTmpFilePath(const QString &filePath) const; static inline int percent(qint64 fraction, qint64 total); static QString cacheLocation(const QString &fileName); static bool isDirWritable(const QString &path); }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_DOCSETSDIALOG_H ================================================ FILE: src/libs/ui/docsetsdialog.ui ================================================ Zeal::WidgetUi::DocsetsDialog Qt::ApplicationModal 0 0 600 500 Docsets 0 Installed QAbstractItemView::ExtendedSelection QAbstractItemView::SelectRows 16 16 Add feed Qt::Horizontal 40 20 false Update false Update all false Remove Available Filter docsets true QAbstractItemView::ExtendedSelection QAbstractItemView::SelectRows 16 16 Refresh Qt::Horizontal 40 20 false Download <i>Docsets are provided by <a href="https://kapeli.com/dash">Dash</a>.</i> Qt::RichText true Downloading: 1. Installing: 5. Qt::Horizontal 40 20 0 0 QDialogButtonBox::Close ================================================ FILE: src/libs/ui/mainwindow.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "mainwindow.h" #include "aboutdialog.h" #include "browsertab.h" #include "docsetsdialog.h" #include "searchsidebar.h" #include "settingsdialog.h" #include "sidebarviewprovider.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Zeal; using namespace Zeal::WidgetUi; MainWindow::MainWindow(Core::Application *app, QWidget *parent) : QMainWindow(parent) , m_application(app) , m_settings(app->settings()) { #ifndef PORTABLE_BUILD setWindowTitle(tr("Zeal")); #else setWindowTitle(tr("Zeal Portable")); #endif resize(900, 600); // Default size. May be overridden by restoreGeometry. setupMainMenu(); setupShortcuts(); setupTabBar(); // Setup central widget. auto centralWidget = new QWidget(this); auto centralWidgetLayout = new QVBoxLayout(centralWidget); centralWidgetLayout->setContentsMargins(0, 0, 0, 0); centralWidgetLayout->setSpacing(0); centralWidgetLayout->addWidget(m_tabBar); m_splitter = new QSplitter(Qt::Horizontal, centralWidget); m_splitter->setChildrenCollapsible(false); centralWidgetLayout->addWidget(m_splitter); m_webViewStack = new QStackedWidget(m_splitter); m_webViewStack->setMinimumWidth(400); m_splitter->addWidget(m_webViewStack); setCentralWidget(centralWidget); restoreGeometry(m_settings->windowGeometry); // Update check connect(m_application, &Core::Application::updateCheckError, this, [this](const QString &message) { QMessageBox::warning(this, QStringLiteral("Zeal"), message); }); connect(m_application, &Core::Application::updateCheckDone, this, [this](const QString &version) { if (version.isEmpty()) { QMessageBox::information(this, QStringLiteral("Zeal"), tr("You are using the latest version.")); return; } // TODO: Remove this ugly workaround for #637. qApp->setQuitOnLastWindowClosed(false); const int ret = QMessageBox::information(this, QStringLiteral("Zeal"), tr("Zeal %1 is available. Open download page?").arg(version), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); qApp->setQuitOnLastWindowClosed(true); if (ret == QMessageBox::Yes) { QDesktopServices::openUrl(QUrl(QStringLiteral("https://zealdocs.org/download.html"))); } }); // Setup sidebar. auto sbViewProvider = new SidebarViewProvider(this); auto sbView = new Sidebar::ProxyView(sbViewProvider, QStringLiteral("index")); auto sb = new Sidebar::Container(); sb->addView(sbView); // Setup splitter. m_splitter->insertWidget(0, sb); m_splitter->restoreState(m_settings->verticalSplitterGeometry); // Setup web settings. auto webSettings = new Browser::Settings(m_settings, this); // Setup web bridge. m_webBridge = new Browser::WebBridge(this); connect(m_webBridge, &Browser::WebBridge::actionTriggered, this, [this](const QString &action) { // TODO: In the future connect directly to the ActionManager. if (action == "openDocsetManager") { m_showDocsetManagerAction->trigger(); } else if (action == "openPreferences") { m_showPreferencesAction->trigger(); } }); createTab(); connect(m_settings, &Core::Settings::updated, this, &MainWindow::applySettings); applySettings(); if (m_settings->checkForUpdate) { m_application->checkForUpdates(true); } } MainWindow::~MainWindow() { m_settings->verticalSplitterGeometry = m_splitter->saveState(); m_settings->windowGeometry = saveGeometry(); } void MainWindow::search(const Registry::SearchQuery &query) { if (auto tab = currentTab()) { tab->search(query); } } void MainWindow::closeTab(int index) { if (index == -1) { index = m_tabBar->currentIndex(); } if (index == -1) { return; } BrowserTab *tab = tabAt(index); m_webViewStack->removeWidget(tab); tab->deleteLater(); // Handle the tab bar last to avoid currentChanged signal coming too early. m_tabBar->removeTab(index); if (m_webViewStack->count() == 0) { createTab(); } } void MainWindow::moveTab(int from, int to) { const QSignalBlocker blocker(m_webViewStack); QWidget *w = m_webViewStack->widget(from); m_webViewStack->removeWidget(w); m_webViewStack->insertWidget(to, w); } BrowserTab *MainWindow::createTab() { auto tab = new BrowserTab(); tab->navigateToStartPage(); addTab(tab); return tab; } void MainWindow::duplicateTab(int index) { BrowserTab *tab = tabAt(index); if (tab == nullptr) return; // Add a duplicate next to the `index`. addTab(tab->clone(), index + 1); } void MainWindow::addTab(BrowserTab *tab, int index) { connect(tab, &BrowserTab::iconChanged, this, [this, tab](const QIcon &icon) { const int index = m_webViewStack->indexOf(tab); Q_ASSERT(m_tabBar->tabData(index).value() == tab); m_tabBar->setTabIcon(index, icon); }); connect(tab, &BrowserTab::titleChanged, this, [this, tab](const QString &title) { if (title.isEmpty()) return; #ifndef PORTABLE_BUILD setWindowTitle(QStringLiteral("%1 - Zeal").arg(title)); #else setWindowTitle(QStringLiteral("%1 - Zeal Portable").arg(title)); #endif const int index = m_webViewStack->indexOf(tab); Q_ASSERT(m_tabBar->tabData(index).value() == tab); m_tabBar->setTabText(index, title); m_tabBar->setTabToolTip(index, title); }); tab->webControl()->setWebBridgeObject("zAppBridge", m_webBridge); tab->searchSidebar()->focusSearchEdit(); if (index == -1) { index = m_settings->openNewTabAfterActive ? m_tabBar->currentIndex() + 1 : m_webViewStack->count(); } m_webViewStack->insertWidget(index, tab); m_tabBar->insertTab(index, tr("Loading…")); m_tabBar->setCurrentIndex(index); m_tabBar->setTabData(index, QVariant::fromValue(tab)); } BrowserTab *MainWindow::currentTab() const { return tabAt(m_tabBar->currentIndex()); } BrowserTab *MainWindow::tabAt(int index) const { return qobject_cast(m_webViewStack->widget(index)); } void MainWindow::setupMainMenu() { m_menuBar = new QMenuBar(this); m_menuBar->installEventFilter(this); // TODO: [Qt 6.3] Refactor using addAction(text, shortcut, receiver, member). // TODO: [Qt 6.7] Use QIcon::ThemeIcon. // File Menu. auto menu = m_menuBar->addMenu(tr("&File")); // -> New Tab Action. // Not a standard icon, but it is often provided by GTK themes. auto action = menu->addAction( QIcon::fromTheme(QStringLiteral("tab-new")), tr("New &Tab") ); addAction(action); action->setShortcut(QKeySequence::AddTab); connect(action, &QAction::triggered, this, &MainWindow::createTab); // -> Close Tab Action. action = menu->addAction(tr("&Close Tab")); addAction(action); action->setShortcut(QKeySequence(Qt::ControlModifier | Qt::Key_W)); connect(action, &QAction::triggered, this, [this]() { closeTab(); }); menu->addSeparator(); // -> Quit Action. action = m_quitAction = menu->addAction( QIcon::fromTheme(QStringLiteral("application-exit")), // Follow Windows HIG. #ifdef Q_OS_WINDOWS tr("E&xit"), #else tr("&Quit"), #endif qApp, &QApplication::quit ); addAction(action); action->setMenuRole(QAction::QuitRole); // Some platform plugins do not define QKeySequence::Quit. if (QKeySequence(QKeySequence::Quit).isEmpty()) { action->setShortcut(QStringLiteral("Ctrl+Q")); } else { action->setShortcut(QKeySequence::Quit); } // Edit Menu. menu = m_menuBar->addMenu(tr("&Edit")); // -> Find in Page Action. action = menu->addAction( QIcon::fromTheme(QStringLiteral("edit-find")), tr("&Find in Page") ); addAction(action); action->setShortcut(QKeySequence::Find); connect(action, &QAction::triggered, this, [this]() { if (auto tab = currentTab()) { tab->webControl()->activateSearchBar(); } }); menu->addSeparator(); // -> Preferences Action. // cspell:disable-next-line - cSpell does not like the ampersand. action = m_showPreferencesAction = menu->addAction(tr("Prefere&nces")); addAction(action); action->setMenuRole(QAction::PreferencesRole); if (QKeySequence(QKeySequence::Preferences).isEmpty()) { action->setShortcut(QStringLiteral("Ctrl+,")); } else { action->setShortcut(QKeySequence::Preferences); } connect(action, &QAction::triggered, this, [this]() { if (m_globalShortcut) { m_globalShortcut->setEnabled(false); } QScopedPointer dialog(new SettingsDialog(this)); dialog->exec(); if (m_globalShortcut) { m_globalShortcut->setEnabled(true); } }); // Menu bar is global on MacOS, so it should always be visible. #ifndef Q_OS_MACOS // View Menu. menu = m_menuBar->addMenu(tr("&View")); // -> Toolbars Submenu. auto subMenu = menu->addMenu(tr("&Toolbars")); // -> Toggle Toolbar Action. action = m_showMenuBarAction = subMenu->addAction(tr("&Menu Bar")); addAction(action); action->setCheckable(true); action->setChecked(!m_settings->hideMenuBar); action->setShortcut(QKeySequence(QStringLiteral("Ctrl+M"))); connect(action, &QAction::toggled, this, [this](bool checked) { m_menuBar->setVisible(checked); m_settings->hideMenuBar = !checked; m_settings->save(); }); // Set menu bar visibility. m_menuBar->setVisible(m_showMenuBarAction->isChecked()); // Show and focus menu bar on F10. auto focusMenu = new QShortcut(Qt::Key_F10, this); connect(focusMenu, &QShortcut::activated, this, [this]() { m_menuBar->setVisible(true); m_menuBar->setFocus(); if (!m_menuBar->actions().isEmpty()) { m_menuBar->setActiveAction(m_menuBar->actions().first()); } }); #endif // Tools Menu. menu = m_menuBar->addMenu(tr("&Tools")); // -> Docsets Action. m_showDocsetManagerAction = menu->addAction( tr("&Docsets…"), this, [this]() { QScopedPointer dialog(new DocsetsDialog(m_application, this)); dialog->exec(); } ); // Help Menu. menu = m_menuBar->addMenu(tr("&Help")); // -> Submit Feedback Action. menu->addAction( tr("&Submit Feedback…"), this, [this]() { QDesktopServices::openUrl(QUrl(QStringLiteral("https://go.zealdocs.org/l/report-bug"))); } ); // -> Check for Updates Action. menu->addAction(tr("&Check for Updates…"), this, [this]() { m_application->checkForUpdates(); }); menu->addSeparator(); // -> About Action. action = menu->addAction( QIcon::fromTheme(QStringLiteral("help-about")), tr("&About Zeal"), this, [this]() { QScopedPointer dialog(new AboutDialog(this)); dialog->exec(); } ); addAction(action); action->setMenuRole(QAction::AboutRole); setMenuBar(m_menuBar); } void MainWindow::setupShortcuts() { // Initialize the global shortcut handler if supported. if (QxtGlobalShortcut::isSupported()) { m_globalShortcut = new QxtGlobalShortcut(m_settings->showShortcut, this); connect(m_globalShortcut, &QxtGlobalShortcut::activated, this, &MainWindow::toggleWindow); } // Focus search bar. auto shortcut = new QShortcut(QStringLiteral("Ctrl+K"), this); connect(shortcut, &QShortcut::activated, this, [this]() { if (auto tab = currentTab()) { tab->searchSidebar()->focusSearchEdit(); } }); shortcut = new QShortcut(QStringLiteral("Ctrl+L"), this); connect(shortcut, &QShortcut::activated, this, [this]() { if (auto tab = currentTab()) { tab->searchSidebar()->focusSearchEdit(); } }); // Duplicate current tab. shortcut = new QShortcut(QStringLiteral("Ctrl+Alt+T"), this); connect(shortcut, &QShortcut::activated, this, [this]() { duplicateTab(m_tabBar->currentIndex()); }); // Hide/show sidebar. // TODO: Move to the View menu. shortcut = new QShortcut(QStringLiteral("Ctrl+B"), this); connect(shortcut, &QShortcut::activated, this, [this]() { auto sb = m_splitter->widget(0); if (sb == nullptr) { // This should not really happen. return; } sb->setVisible(!sb->isVisible()); }); // Browser Shortcuts. shortcut = new QShortcut(QKeySequence::Back, this); connect(shortcut, &QShortcut::activated, this, [this]() { if (auto tab = currentTab()) { tab->webControl()->back(); } }); shortcut = new QShortcut(QKeySequence::Forward, this); connect(shortcut, &QShortcut::activated, this, [this]() { if (auto tab = currentTab()) { tab->webControl()->forward(); } }); shortcut = new QShortcut(QKeySequence::ZoomIn, this); connect(shortcut, &QShortcut::activated, this, [this]() { if (auto tab = currentTab()) { tab->webControl()->zoomIn(); } }); shortcut = new QShortcut(QStringLiteral("Ctrl+="), this); connect(shortcut, &QShortcut::activated, this, [this]() { if (auto tab = currentTab()) { tab->webControl()->zoomIn(); } }); shortcut = new QShortcut(QKeySequence::ZoomOut, this); connect(shortcut, &QShortcut::activated, this, [this]() { if (auto tab = currentTab()) { tab->webControl()->zoomOut(); } }); shortcut = new QShortcut(QStringLiteral("Ctrl+0"), this); connect(shortcut, &QShortcut::activated, this, [this]() { if (auto tab = currentTab()) { tab->webControl()->resetZoom(); } }); // TODO: Use QKeySequence::NextChild, when QTBUG-112193 is fixed. QAction *action = new QAction(this); addAction(action); action->setShortcuts({QKeySequence(Qt::ControlModifier | Qt::Key_Tab), QKeySequence(Qt::ControlModifier | Qt::Key_PageDown)}); connect(action, &QAction::triggered, this, [this]() { const int count = m_tabBar->count(); if (count > 0) { m_tabBar->setCurrentIndex((m_tabBar->currentIndex() + 1) % count); } }); // TODO: Use QKeySequence::PreviousChild, when QTBUG-15746 and QTBUG-112193 are fixed. action = new QAction(this); addAction(action); action->setShortcuts({QKeySequence(Qt::ControlModifier | Qt::ShiftModifier | Qt::Key_Tab), QKeySequence(Qt::ControlModifier | Qt::Key_PageUp)}); connect(action, &QAction::triggered, this, [this]() { const int count = m_tabBar->count(); if (count > 0) { m_tabBar->setCurrentIndex((m_tabBar->currentIndex() - 1 + count) % count); } }); } void MainWindow::setupTabBar() { m_tabBar = new QTabBar(this); m_tabBar->installEventFilter(this); m_tabBar->setDocumentMode(true); m_tabBar->setElideMode(Qt::ElideRight); m_tabBar->setExpanding(false); m_tabBar->setMovable(true); m_tabBar->setSelectionBehaviorOnRemove(QTabBar::SelectPreviousTab); m_tabBar->setStyleSheet(QStringLiteral("QTabBar::tab { width: 150px; }")); m_tabBar->setTabsClosable(true); m_tabBar->setUsesScrollButtons(true); connect(m_tabBar, &QTabBar::currentChanged, this, [this](int index) { if (index == -1) { return; } BrowserTab *tab = tabAt(index); #ifndef PORTABLE_BUILD setWindowTitle(QStringLiteral("%1 - Zeal").arg(tab->webControl()->title())); #else setWindowTitle(QStringLiteral("%1 - Zeal Portable").arg(tab->webControl()->title())); #endif m_webViewStack->setCurrentIndex(index); emit currentTabChanged(); }); connect(m_tabBar, &QTabBar::tabCloseRequested, this, &MainWindow::closeTab); connect(m_tabBar, &QTabBar::tabMoved, this, &MainWindow::moveTab); for (int i = 1; i < 10; i++) { auto action = new QAction(m_tabBar); #ifdef Q_OS_LINUX action->setShortcut(QStringLiteral("Alt+%1").arg(i)); #else action->setShortcut(QStringLiteral("Ctrl+%1").arg(i)); #endif if (i == 9) { connect(action, &QAction::triggered, this, [=]() { m_tabBar->setCurrentIndex(m_tabBar->count() - 1); }); } else { connect(action, &QAction::triggered, this, [=]() { m_tabBar->setCurrentIndex(i - 1); }); } addAction(action); } connect(m_tabBar, &QTabBar::tabBarDoubleClicked, this, [this](int index) { if (index == -1) { createTab(); } }); } void MainWindow::createTrayIcon() { if (m_trayIcon) return; m_trayIcon = new QSystemTrayIcon(this); m_trayIcon->setIcon(QIcon::fromTheme(QStringLiteral("zeal-tray"), windowIcon())); m_trayIcon->setToolTip(QStringLiteral("Zeal")); connect(m_trayIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) { if (reason != QSystemTrayIcon::Trigger && reason != QSystemTrayIcon::DoubleClick) return; toggleWindow(); }); auto trayIconMenu = new QMenu(this); QAction *toggleAction = trayIconMenu->addAction(tr("Show Zeal"), this, &MainWindow::toggleWindow); connect(trayIconMenu, &QMenu::aboutToShow, this, [this, toggleAction]() { toggleAction->setText(isVisible() ? tr("Minimize to Tray") : tr("Show Zeal")); }); trayIconMenu->addSeparator(); trayIconMenu->addAction(m_quitAction); m_trayIcon->setContextMenu(trayIconMenu); m_trayIcon->show(); } void MainWindow::removeTrayIcon() { if (!m_trayIcon) return; QMenu *trayIconMenu = m_trayIcon->contextMenu(); delete m_trayIcon; m_trayIcon = nullptr; delete trayIconMenu; } void MainWindow::bringToFront() { show(); setWindowState((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); raise(); activateWindow(); if (auto tab = currentTab()) { tab->searchSidebar()->focusSearchEdit(); } } void MainWindow::changeEvent(QEvent *event) { if (m_settings->showSystrayIcon && m_settings->minimizeToSystray && event->type() == QEvent::WindowStateChange && isMinimized()) { hide(); } QMainWindow::changeEvent(event); } void MainWindow::closeEvent(QCloseEvent *event) { if (m_settings->showSystrayIcon && m_settings->hideOnClose) { event->ignore(); toggleWindow(); } } bool MainWindow::eventFilter(QObject *object, QEvent *event) { if (object == m_tabBar) { switch (event->type()) { case QEvent::MouseButtonRelease: { auto e = static_cast(event); if (e->button() == Qt::MiddleButton) { const int index = m_tabBar->tabAt(e->pos()); if (index != -1) { closeTab(index); return true; } } break; } case QEvent::Wheel: // TODO: Remove in case QTBUG-8428 is fixed on all platforms return true; default: break; } } #ifndef Q_OS_MACOS if (object == m_menuBar && m_menuBar->isVisible() && m_showMenuBarAction != nullptr && !m_showMenuBarAction->isChecked()) { switch (event->type()) { // Hide menu bar when it loses focus. case QEvent::FocusOut: { auto e = static_cast(event); if (e->reason() != Qt::PopupFocusReason) { m_menuBar->hide(); } break; } // Hide menu bar on Escape key press. case QEvent::KeyPress: { auto e = static_cast(event); if (e->key() == Qt::Key_Escape) { m_menuBar->hide(); } break; } default: break; } } #endif return QMainWindow::eventFilter(object, event); } // Captures global events in order to pass them to the search bar. void MainWindow::keyPressEvent(QKeyEvent *keyEvent) { switch (keyEvent->key()) { case Qt::Key_Escape: if (auto tab = currentTab()) { tab->searchSidebar()->focusSearchEdit(true); } break; case Qt::Key_Question: if (auto tab = currentTab()) { tab->searchSidebar()->focusSearchEdit(); } break; default: QMainWindow::keyPressEvent(keyEvent); break; } } void MainWindow::applySettings() { if (m_globalShortcut) { m_globalShortcut->setShortcut(m_settings->showShortcut); } if (m_settings->showSystrayIcon) createTrayIcon(); else removeTrayIcon(); } void MainWindow::toggleWindow() { const bool checkActive = m_globalShortcut && sender() == m_globalShortcut; if (!isVisible() || (checkActive && !isActiveWindow())) { bringToFront(); } else { if (m_trayIcon) { hide(); } else { showMinimized(); } } } ================================================ FILE: src/libs/ui/mainwindow.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_WIDGETUI_MAINWINDOW_H #define ZEAL_WIDGETUI_MAINWINDOW_H #include class QxtGlobalShortcut; class QAction; class QMenuBar; class QSplitter; class QStackedWidget; class QSystemTrayIcon; class QTabBar; namespace Zeal { namespace Browser { class WebBridge; } // namespace Browser namespace Core { class Application; class Settings; } // namespace Core namespace Registry { class SearchQuery; } //namespace Registry namespace WidgetUi { class BrowserTab; class SidebarViewProvider; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(Core::Application *app, QWidget *parent = nullptr); ~MainWindow() override; void search(const Registry::SearchQuery &query); void bringToFront(); BrowserTab *createTab(); public slots: void toggleWindow(); signals: void currentTabChanged(); protected: void changeEvent(QEvent *event) override; void closeEvent(QCloseEvent *event) override; bool eventFilter(QObject *object, QEvent *event) override; void keyPressEvent(QKeyEvent *keyEvent) override; private slots: void applySettings(); void closeTab(int index = -1); void moveTab(int from, int to); void duplicateTab(int index); private: void setupMainMenu(); void setupShortcuts(); void setupTabBar(); void addTab(BrowserTab *tab, int index = -1); BrowserTab *currentTab() const; BrowserTab *tabAt(int index) const; void createTrayIcon(); void removeTrayIcon(); void syncTabState(BrowserTab *tab); Core::Application *m_application = nullptr; Core::Settings *m_settings = nullptr; Browser::WebBridge *m_webBridge = nullptr; QxtGlobalShortcut *m_globalShortcut = nullptr; QMenuBar *m_menuBar = nullptr; QTabBar *m_tabBar = nullptr; QSplitter *m_splitter = nullptr; QStackedWidget *m_webViewStack = nullptr; // TODO: Replace with proper action manager. QAction *m_quitAction = nullptr; QAction *m_showDocsetManagerAction = nullptr; QAction *m_showPreferencesAction = nullptr; #ifndef Q_OS_MACOS QAction *m_showMenuBarAction = nullptr; #endif friend class SidebarViewProvider; QSystemTrayIcon *m_trayIcon = nullptr; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_MAINWINDOW_H ================================================ FILE: src/libs/ui/qxtglobalshortcut/CMakeLists.txt ================================================ list(APPEND QxtGlobalShortcut_SOURCES qxtglobalshortcut.cpp ) if(APPLE) list(APPEND QxtGlobalShortcut_SOURCES qxtglobalshortcut_mac.cpp ) elseif(UNIX) find_package(X11) if(X11_FOUND) list(APPEND QxtGlobalShortcut_SOURCES qxtglobalshortcut_x11.cpp ) else() list(APPEND QxtGlobalShortcut_SOURCES qxtglobalshortcut_noop.cpp ) endif() elseif(WIN32) list(APPEND QxtGlobalShortcut_SOURCES qxtglobalshortcut_win.cpp ) else() list(APPEND QxtGlobalShortcut_SOURCES qxtglobalshortcut_noop.cpp ) endif() add_library(QxtGlobalShortcut STATIC ${QxtGlobalShortcut_SOURCES}) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Gui REQUIRED) target_link_libraries(QxtGlobalShortcut PRIVATE Qt${QT_VERSION_MAJOR}::Gui) if(APPLE) find_library(CARBON_LIBRARY Carbon) target_link_libraries(QxtGlobalShortcut PRIVATE ${CARBON_LIBRARY}) elseif(UNIX AND X11_FOUND) target_link_libraries(QxtGlobalShortcut PRIVATE ${X11_LIBRARIES}) if(QT_VERSION_MAJOR EQUAL 5) find_package(Qt5 COMPONENTS X11Extras REQUIRED) target_link_libraries(QxtGlobalShortcut PRIVATE Qt5::X11Extras) else() if(Qt6Core_VERSION VERSION_GREATER_EQUAL 6.10) find_package(Qt6 COMPONENTS GuiPrivate REQUIRED) endif() target_link_libraries(QxtGlobalShortcut PRIVATE Qt${QT_VERSION_MAJOR}::GuiPrivate) endif() find_package(ECM REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_FIND_MODULE_DIR}) find_package(XCB COMPONENTS XCB KEYSYMS REQUIRED) target_link_libraries(QxtGlobalShortcut PRIVATE XCB::XCB XCB::KEYSYMS) endif() ================================================ FILE: src/libs/ui/qxtglobalshortcut/qxtglobalshortcut.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later /**************************************************************************** // Copyright (C) 2006 - 2011, the LibQxt project. ** See the Qxt AUTHORS file for a list of authors and copyright holders. ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** * Neither the name of the LibQxt project nor the ** names of its contributors may be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ** DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY ** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; ** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ** *****************************************************************************/ #include "qxtglobalshortcut.h" #include "qxtglobalshortcut_p.h" #include #include static Q_LOGGING_CATEGORY(log, "zeal.ui.qxtglobalshortcut") #ifndef Q_OS_MACOS int QxtGlobalShortcutPrivate::ref = 0; #endif // Q_OS_MACOS QHash, QxtGlobalShortcut *> QxtGlobalShortcutPrivate::shortcuts; QxtGlobalShortcutPrivate::QxtGlobalShortcutPrivate(QxtGlobalShortcut *qq) : q_ptr(qq) { #ifndef Q_OS_MACOS if (ref == 0) QAbstractEventDispatcher::instance()->installNativeEventFilter(this); ++ref; #endif // Q_OS_MACOS } QxtGlobalShortcutPrivate::~QxtGlobalShortcutPrivate() { #ifndef Q_OS_MACOS --ref; if (ref == 0) { QAbstractEventDispatcher *ed = QAbstractEventDispatcher::instance(); if (ed != nullptr) { ed->removeNativeEventFilter(this); } } #endif // Q_OS_MACOS } bool QxtGlobalShortcutPrivate::setShortcut(const QKeySequence &shortcut) { Q_Q(QxtGlobalShortcut); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) const int combination = shortcut[0]; #else const int combination = shortcut[0].toCombined(); #endif key = shortcut.isEmpty() ? Qt::Key(0) : Qt::Key(combination & ~Qt::KeyboardModifierMask); mods = shortcut.isEmpty() ? Qt::NoModifier : Qt::KeyboardModifiers(combination & Qt::KeyboardModifierMask); const quint32 nativeKey = nativeKeycode(key); const quint32 nativeMods = nativeModifiers(mods); const bool res = registerShortcut(nativeKey, nativeMods); if (!res) { qCWarning(log, "Failed to register '%s' shortcut.", qPrintable(QKeySequence(key | mods).toString())); return false; } shortcuts.insert({nativeKey, nativeMods}, q); return true; } bool QxtGlobalShortcutPrivate::unsetShortcut() { Q_Q(QxtGlobalShortcut); const quint32 nativeKey = nativeKeycode(key); const quint32 nativeMods = nativeModifiers(mods); if (shortcuts.value({nativeKey, nativeMods}) != q) { qCWarning(log, "Tried to unregister unowned '%s' shortcut.", qPrintable(QKeySequence(key | mods).toString())); return false; } const bool res = unregisterShortcut(nativeKey, nativeMods); if (!res) { qCWarning(log, "Failed to unregister '%s' shortcut.", qPrintable(QKeySequence(key | mods).toString())); return false; } shortcuts.remove({nativeKey, nativeMods}); key = Qt::Key(0); mods = Qt::KeyboardModifiers(Qt::NoModifier); return true; } bool QxtGlobalShortcutPrivate::activateShortcut(quint32 nativeKey, quint32 nativeMods) { QxtGlobalShortcut *shortcut = shortcuts.value({nativeKey, nativeMods}); if (!shortcut || !shortcut->isEnabled()) return false; emit shortcut->activated(); return true; } /*! \class QxtGlobalShortcut \inmodule QxtWidgets \brief The QxtGlobalShortcut class provides a global shortcut aka "hotkey". A global shortcut triggers even if the application is not active. This makes it easy to implement applications that react to certain shortcuts still if some other application is active or if the application is for example minimized to the system tray. Example usage: \code QxtGlobalShortcut *shortcut = new QxtGlobalShortcut(window); connect(shortcut, SIGNAL(activated()), window, SLOT(toggleVisibility())); shortcut->setShortcut(QKeySequence("Ctrl+Shift+F12")); \endcode \bold {Note:} Since Qxt 0.6 QxtGlobalShortcut no more requires QxtApplication. */ /*! \fn QxtGlobalShortcut::activated() This signal is emitted when the user types the shortcut's key sequence. \sa shortcut */ /*! Constructs a new QxtGlobalShortcut with \a parent. */ QxtGlobalShortcut::QxtGlobalShortcut(QObject *parent) : QObject(parent) , d_ptr(new QxtGlobalShortcutPrivate(this)) { } /*! Constructs a new QxtGlobalShortcut with \a shortcut and \a parent. */ QxtGlobalShortcut::QxtGlobalShortcut(const QKeySequence &shortcut, QObject *parent) : QObject(parent) , d_ptr(new QxtGlobalShortcutPrivate(this)) { setShortcut(shortcut); } /*! Destructs the QxtGlobalShortcut. */ QxtGlobalShortcut::~QxtGlobalShortcut() { Q_D(QxtGlobalShortcut); if (d->key != 0) d->unsetShortcut(); delete d; } /*! \property QxtGlobalShortcut::shortcut \brief the shortcut key sequence \bold {Note:} Notice that corresponding key press and release events are not delivered for registered global shortcuts even if they are disabled. Also, comma separated key sequences are not supported. Only the first part is used: \code qxtShortcut->setShortcut(QKeySequence("Ctrl+Alt+A,Ctrl+Alt+B")); Q_ASSERT(qxtShortcut->shortcut() == QKeySequence("Ctrl+Alt+A")); \endcode */ QKeySequence QxtGlobalShortcut::shortcut() const { Q_D(const QxtGlobalShortcut); return QKeySequence(d->key | d->mods); } bool QxtGlobalShortcut::setShortcut(const QKeySequence &shortcut) { Q_D(QxtGlobalShortcut); if (d->key != 0 && !d->unsetShortcut()) return false; if (shortcut.isEmpty()) return true; return d->setShortcut(shortcut); } /*! \property QxtGlobalShortcut::enabled \brief whether the shortcut is enabled A disabled shortcut does not get activated. The default value is \c true. \sa setDisabled() */ bool QxtGlobalShortcut::isEnabled() const { Q_D(const QxtGlobalShortcut); return d->enabled; } /*! * \brief QxtGlobalShortcut::isSupported checks if the current platform is supported. */ bool QxtGlobalShortcut::isSupported() { return QxtGlobalShortcutPrivate::isSupported(); } void QxtGlobalShortcut::setEnabled(bool enabled) { Q_D(QxtGlobalShortcut); d->enabled = enabled; } ================================================ FILE: src/libs/ui/qxtglobalshortcut/qxtglobalshortcut.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later /**************************************************************************** // Copyright (C) 2006 - 2011, the LibQxt project. ** See the Qxt AUTHORS file for a list of authors and copyright holders. ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** * Neither the name of the LibQxt project nor the ** names of its contributors may be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ** DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY ** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; ** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ** *****************************************************************************/ #ifndef QXTGLOBALSHORTCUT_H #define QXTGLOBALSHORTCUT_H #include #include class QxtGlobalShortcutPrivate; class QxtGlobalShortcut : public QObject { Q_OBJECT QxtGlobalShortcutPrivate *d_ptr = nullptr; Q_DECLARE_PRIVATE(QxtGlobalShortcut) Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled) Q_PROPERTY(QKeySequence shortcut READ shortcut WRITE setShortcut) public: explicit QxtGlobalShortcut(QObject *parent = nullptr); explicit QxtGlobalShortcut(const QKeySequence &shortcut, QObject *parent = nullptr); ~QxtGlobalShortcut() override; QKeySequence shortcut() const; bool setShortcut(const QKeySequence &shortcut); bool isEnabled() const; static bool isSupported(); public slots: void setEnabled(bool enabled); signals: void activated(); }; #endif // QXTGLOBALSHORTCUT_H ================================================ FILE: src/libs/ui/qxtglobalshortcut/qxtglobalshortcut_mac.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later /**************************************************************************** // Copyright (C) 2006 - 2011, the LibQxt project. ** See the Qxt AUTHORS file for a list of authors and copyright holders. ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** * Neither the name of the LibQxt project nor the ** names of its contributors may be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ** DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY ** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; ** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ** *****************************************************************************/ #include "qxtglobalshortcut_p.h" #include #include #include #include namespace { typedef QPair Identifier; static QMap keyRefs; static QHash keyIDs; static quint32 hotKeySerial = 0; static bool qxt_mac_handler_installed = false; OSStatus qxt_mac_handle_hot_key(EventHandlerCallRef nextHandler, EventRef event, void *data) { Q_UNUSED(nextHandler) Q_UNUSED(data) if (GetEventClass(event) == kEventClassKeyboard && GetEventKind(event) == kEventHotKeyPressed) { EventHotKeyID keyID; GetEventParameter(event, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(keyID), NULL, &keyID); Identifier id = keyIDs.key(keyID.id); QxtGlobalShortcutPrivate::activateShortcut(id.second, id.first); } return noErr; } } // namespace bool QxtGlobalShortcutPrivate::isSupported() { return QGuiApplication::platformName() == QLatin1String("cocoa"); } bool QxtGlobalShortcutPrivate::nativeEventFilter(const QByteArray &eventType, void *message, NativeEventFilterResult *result) { Q_UNUSED(eventType) Q_UNUSED(message) Q_UNUSED(result) return false; } quint32 QxtGlobalShortcutPrivate::nativeModifiers(Qt::KeyboardModifiers modifiers) { quint32 native = 0; if (modifiers & Qt::ShiftModifier) native |= shiftKey; if (modifiers & Qt::ControlModifier) native |= cmdKey; if (modifiers & Qt::AltModifier) native |= optionKey; if (modifiers & Qt::MetaModifier) native |= controlKey; if (modifiers & Qt::KeypadModifier) native |= kEventKeyModifierNumLockMask; return native; } quint32 QxtGlobalShortcutPrivate::nativeKeycode(Qt::Key key) { UTF16Char ch; // Constants found in NSEvent.h from AppKit.framework switch (key) { case Qt::Key_Return: return kVK_Return; case Qt::Key_Enter: return kVK_ANSI_KeypadEnter; case Qt::Key_Tab: return kVK_Tab; case Qt::Key_Space: return kVK_Space; case Qt::Key_Backspace: return kVK_Delete; case Qt::Key_Control: return kVK_Command; case Qt::Key_Shift: return kVK_Shift; case Qt::Key_CapsLock: return kVK_CapsLock; case Qt::Key_Option: return kVK_Option; case Qt::Key_Meta: return kVK_Control; case Qt::Key_F17: return kVK_F17; case Qt::Key_VolumeUp: return kVK_VolumeUp; case Qt::Key_VolumeDown: return kVK_VolumeDown; case Qt::Key_F18: return kVK_F18; case Qt::Key_F19: return kVK_F19; case Qt::Key_F20: return kVK_F20; case Qt::Key_F5: return kVK_F5; case Qt::Key_F6: return kVK_F6; case Qt::Key_F7: return kVK_F7; case Qt::Key_F3: return kVK_F3; case Qt::Key_F8: return kVK_F8; case Qt::Key_F9: return kVK_F9; case Qt::Key_F11: return kVK_F11; case Qt::Key_F13: return kVK_F13; case Qt::Key_F16: return kVK_F16; case Qt::Key_F14: return kVK_F14; case Qt::Key_F10: return kVK_F10; case Qt::Key_F12: return kVK_F12; case Qt::Key_F15: return kVK_F15; case Qt::Key_Help: return kVK_Help; case Qt::Key_Home: return kVK_Home; case Qt::Key_PageUp: return kVK_PageUp; case Qt::Key_Delete: return kVK_ForwardDelete; case Qt::Key_F4: return kVK_F4; case Qt::Key_End: return kVK_End; case Qt::Key_F2: return kVK_F2; case Qt::Key_PageDown: return kVK_PageDown; case Qt::Key_F1: return kVK_F1; case Qt::Key_Left: return kVK_LeftArrow; case Qt::Key_Right: return kVK_RightArrow; case Qt::Key_Down: return kVK_DownArrow; case Qt::Key_Up: return kVK_UpArrow; default: ; } if (key == Qt::Key_Escape) ch = 27; else if (key == Qt::Key_Return) ch = 13; else if (key == Qt::Key_Enter) ch = 3; else if (key == Qt::Key_Tab) ch = 9; else ch = key; CFDataRef currentLayoutData; TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource(); if (currentKeyboard == NULL) return 0; currentLayoutData = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData); CFRelease(currentKeyboard); if (currentLayoutData == NULL) return 0; UCKeyboardLayout *header = (UCKeyboardLayout *)CFDataGetBytePtr(currentLayoutData); UCKeyboardTypeHeader *table = header->keyboardTypeList; uint8_t *data = (uint8_t *)header; // God, would a little documentation for this shit kill you... for (quint32 i = 0; i < header->keyboardTypeCount; ++i) { UCKeyStateRecordsIndex *stateRec = 0; if (table[i].keyStateRecordsIndexOffset != 0) { stateRec = reinterpret_cast(data + table[i].keyStateRecordsIndexOffset); if (stateRec->keyStateRecordsIndexFormat != kUCKeyStateRecordsIndexFormat) stateRec = 0; } UCKeyToCharTableIndex *charTable = reinterpret_cast(data + table[i].keyToCharTableIndexOffset); if (charTable->keyToCharTableIndexFormat != kUCKeyToCharTableIndexFormat) continue; for (quint32 j = 0; j < charTable->keyToCharTableCount; ++j) { UCKeyOutput *keyToChar = reinterpret_cast(data + charTable->keyToCharTableOffsets[j]); for (quint32 k = 0; k < charTable->keyToCharTableSize; ++k) { if (keyToChar[k] & kUCKeyOutputTestForIndexMask) { long idx = keyToChar[k] & kUCKeyOutputGetIndexMask; if (stateRec && idx < stateRec->keyStateRecordCount) { UCKeyStateRecord *rec = reinterpret_cast(data + stateRec->keyStateRecordOffsets[idx]); if (rec->stateZeroCharData == ch) return k; } } else if (!(keyToChar[k] & kUCKeyOutputSequenceIndexMask) && keyToChar[k] < 0xFFFE) { if (keyToChar[k] == ch) return k; } } // for k } // for j } // for i return 0; } bool QxtGlobalShortcutPrivate::registerShortcut(quint32 nativeKey, quint32 nativeMods) { if (!qxt_mac_handler_installed) { EventTypeSpec t; t.eventClass = kEventClassKeyboard; t.eventKind = kEventHotKeyPressed; InstallApplicationEventHandler(&qxt_mac_handle_hot_key, 1, &t, NULL, NULL); } EventHotKeyID keyID; keyID.signature = 'cute'; keyID.id = ++hotKeySerial; EventHotKeyRef ref = 0; bool rv = !RegisterEventHotKey(nativeKey, nativeMods, keyID, GetApplicationEventTarget(), 0, &ref); if (rv) { keyIDs.insert(Identifier(nativeMods, nativeKey), keyID.id); keyRefs.insert(keyID.id, ref); } return rv; } bool QxtGlobalShortcutPrivate::unregisterShortcut(quint32 nativeKey, quint32 nativeMods) { Identifier id(nativeMods, nativeKey); if (!keyIDs.contains(id)) return false; EventHotKeyRef ref = keyRefs.take(keyIDs[id]); keyIDs.remove(id); return !UnregisterEventHotKey(ref); } ================================================ FILE: src/libs/ui/qxtglobalshortcut/qxtglobalshortcut_noop.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "qxtglobalshortcut_p.h" bool QxtGlobalShortcutPrivate::isSupported() { return false; } bool QxtGlobalShortcutPrivate::nativeEventFilter(const QByteArray &eventType, void *message, NativeEventFilterResult *result) { Q_UNUSED(eventType) Q_UNUSED(message) Q_UNUSED(result) return false; } quint32 QxtGlobalShortcutPrivate::nativeModifiers(Qt::KeyboardModifiers modifiers) { Q_UNUSED(modifiers) return 0; } quint32 QxtGlobalShortcutPrivate::nativeKeycode(Qt::Key key) { Q_UNUSED(key) return 0; } bool QxtGlobalShortcutPrivate::registerShortcut(quint32 nativeKey, quint32 nativeMods) { Q_UNUSED(nativeKey) Q_UNUSED(nativeMods) return false; } bool QxtGlobalShortcutPrivate::unregisterShortcut(quint32 nativeKey, quint32 nativeMods) { Q_UNUSED(nativeKey) Q_UNUSED(nativeMods) return false; } ================================================ FILE: src/libs/ui/qxtglobalshortcut/qxtglobalshortcut_p.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later /**************************************************************************** // Copyright (C) 2006 - 2011, the LibQxt project. ** See the Qxt AUTHORS file for a list of authors and copyright holders. ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** * Neither the name of the LibQxt project nor the ** names of its contributors may be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ** DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY ** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; ** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ** *****************************************************************************/ #ifndef QXTGLOBALSHORTCUT_P_H #define QXTGLOBALSHORTCUT_P_H #include #include #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #define NativeEventFilterResult long #else #define NativeEventFilterResult qintptr #endif class QKeySequence; class QxtGlobalShortcut; class QxtGlobalShortcutPrivate : public QAbstractNativeEventFilter { QxtGlobalShortcut *q_ptr = nullptr; Q_DECLARE_PUBLIC(QxtGlobalShortcut) public: QxtGlobalShortcutPrivate(QxtGlobalShortcut *qq); ~QxtGlobalShortcutPrivate() override; bool enabled = true; Qt::Key key = Qt::Key(0); Qt::KeyboardModifiers mods = Qt::NoModifier; #ifndef Q_OS_MACOS static int ref; #endif // Q_OS_MACOS bool setShortcut(const QKeySequence &shortcut); bool unsetShortcut(); bool nativeEventFilter(const QByteArray &eventType, void *message, NativeEventFilterResult *result) override; static bool isSupported(); static bool activateShortcut(quint32 nativeKey, quint32 nativeMods); private: static quint32 nativeKeycode(Qt::Key keycode); static quint32 nativeModifiers(Qt::KeyboardModifiers modifiers); static bool registerShortcut(quint32 nativeKey, quint32 nativeMods); static bool unregisterShortcut(quint32 nativeKey, quint32 nativeMods); static QHash, QxtGlobalShortcut *> shortcuts; }; #endif // QXTGLOBALSHORTCUT_P_H ================================================ FILE: src/libs/ui/qxtglobalshortcut/qxtglobalshortcut_win.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later /**************************************************************************** // Copyright (C) 2006 - 2011, the LibQxt project. ** See the Qxt AUTHORS file for a list of authors and copyright holders. ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** * Neither the name of the LibQxt project nor the ** names of its contributors may be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ** DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY ** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; ** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ** *****************************************************************************/ #include "qxtglobalshortcut_p.h" #include #include bool QxtGlobalShortcutPrivate::isSupported() { return QGuiApplication::platformName() == QLatin1String("windows"); } bool QxtGlobalShortcutPrivate::nativeEventFilter(const QByteArray &eventType, void *message, NativeEventFilterResult *result) { Q_UNUSED(eventType) Q_UNUSED(result) MSG *msg = static_cast(message); if (msg->message == WM_HOTKEY) { const quint32 keycode = HIWORD(msg->lParam); const quint32 modifiers = LOWORD(msg->lParam); activateShortcut(keycode, modifiers); } return false; } quint32 QxtGlobalShortcutPrivate::nativeModifiers(Qt::KeyboardModifiers modifiers) { // MOD_ALT, MOD_CONTROL, (MOD_KEYUP), MOD_SHIFT, MOD_WIN quint32 native = 0; if (modifiers & Qt::ShiftModifier) native |= MOD_SHIFT; if (modifiers & Qt::ControlModifier) native |= MOD_CONTROL; if (modifiers & Qt::AltModifier) native |= MOD_ALT; if (modifiers & Qt::MetaModifier) native |= MOD_WIN; // TODO: resolve these? //if (modifiers & Qt::KeypadModifier) //if (modifiers & Qt::GroupSwitchModifier) return native; } quint32 QxtGlobalShortcutPrivate::nativeKeycode(Qt::Key key) { switch (key) { case Qt::Key_Escape: return VK_ESCAPE; case Qt::Key_Tab: case Qt::Key_Backtab: return VK_TAB; case Qt::Key_Backspace: return VK_BACK; case Qt::Key_Return: case Qt::Key_Enter: return VK_RETURN; case Qt::Key_Insert: return VK_INSERT; case Qt::Key_Delete: return VK_DELETE; case Qt::Key_Pause: return VK_PAUSE; case Qt::Key_Print: return VK_PRINT; case Qt::Key_Clear: return VK_CLEAR; case Qt::Key_Home: return VK_HOME; case Qt::Key_End: return VK_END; case Qt::Key_Left: return VK_LEFT; case Qt::Key_Up: return VK_UP; case Qt::Key_Right: return VK_RIGHT; case Qt::Key_Down: return VK_DOWN; case Qt::Key_PageUp: return VK_PRIOR; case Qt::Key_PageDown: return VK_NEXT; case Qt::Key_F1: return VK_F1; case Qt::Key_F2: return VK_F2; case Qt::Key_F3: return VK_F3; case Qt::Key_F4: return VK_F4; case Qt::Key_F5: return VK_F5; case Qt::Key_F6: return VK_F6; case Qt::Key_F7: return VK_F7; case Qt::Key_F8: return VK_F8; case Qt::Key_F9: return VK_F9; case Qt::Key_F10: return VK_F10; case Qt::Key_F11: return VK_F11; case Qt::Key_F12: return VK_F12; case Qt::Key_F13: return VK_F13; case Qt::Key_F14: return VK_F14; case Qt::Key_F15: return VK_F15; case Qt::Key_F16: return VK_F16; case Qt::Key_F17: return VK_F17; case Qt::Key_F18: return VK_F18; case Qt::Key_F19: return VK_F19; case Qt::Key_F20: return VK_F20; case Qt::Key_F21: return VK_F21; case Qt::Key_F22: return VK_F22; case Qt::Key_F23: return VK_F23; case Qt::Key_F24: return VK_F24; case Qt::Key_Space: return VK_SPACE; case Qt::Key_Asterisk: return VK_MULTIPLY; case Qt::Key_Plus: return VK_ADD; case Qt::Key_Comma: return VK_SEPARATOR; case Qt::Key_Minus: return VK_SUBTRACT; case Qt::Key_Slash: return VK_DIVIDE; case Qt::Key_MediaNext: return VK_MEDIA_NEXT_TRACK; case Qt::Key_MediaPrevious: return VK_MEDIA_PREV_TRACK; case Qt::Key_MediaPlay: return VK_MEDIA_PLAY_PAUSE; case Qt::Key_MediaStop: return VK_MEDIA_STOP; // couldn't find those in VK_* //case Qt::Key_MediaLast: //case Qt::Key_MediaRecord: case Qt::Key_VolumeDown: return VK_VOLUME_DOWN; case Qt::Key_VolumeUp: return VK_VOLUME_UP; case Qt::Key_VolumeMute: return VK_VOLUME_MUTE; // numbers case Qt::Key_0: case Qt::Key_1: case Qt::Key_2: case Qt::Key_3: case Qt::Key_4: case Qt::Key_5: case Qt::Key_6: case Qt::Key_7: case Qt::Key_8: case Qt::Key_9: return key; // letters case Qt::Key_A: case Qt::Key_B: case Qt::Key_C: case Qt::Key_D: case Qt::Key_E: case Qt::Key_F: case Qt::Key_G: case Qt::Key_H: case Qt::Key_I: case Qt::Key_J: case Qt::Key_K: case Qt::Key_L: case Qt::Key_M: case Qt::Key_N: case Qt::Key_O: case Qt::Key_P: case Qt::Key_Q: case Qt::Key_R: case Qt::Key_S: case Qt::Key_T: case Qt::Key_U: case Qt::Key_V: case Qt::Key_W: case Qt::Key_X: case Qt::Key_Y: case Qt::Key_Z: return key; default: return 0; } } bool QxtGlobalShortcutPrivate::registerShortcut(quint32 nativeKey, quint32 nativeMods) { return RegisterHotKey(0, nativeMods ^ nativeKey, nativeMods, nativeKey); } bool QxtGlobalShortcutPrivate::unregisterShortcut(quint32 nativeKey, quint32 nativeMods) { return UnregisterHotKey(0, nativeMods ^ nativeKey); } ================================================ FILE: src/libs/ui/qxtglobalshortcut/qxtglobalshortcut_x11.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later /**************************************************************************** // Copyright (C) 2006 - 2011, the LibQxt project. ** See the Qxt AUTHORS file for a list of authors and copyright holders. ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** * Neither the name of the LibQxt project nor the ** names of its contributors may be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ** DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY ** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; ** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ** *****************************************************************************/ #include "qxtglobalshortcut_p.h" #include #include #include #include #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #include #else #include #endif #include #include #include namespace { constexpr quint32 maskModifiers[] = { 0, XCB_MOD_MASK_2, XCB_MOD_MASK_LOCK, (XCB_MOD_MASK_2 | XCB_MOD_MASK_LOCK) }; } // namespace bool QxtGlobalShortcutPrivate::isSupported() { return QGuiApplication::platformName() == QLatin1String("xcb"); } bool QxtGlobalShortcutPrivate::nativeEventFilter(const QByteArray &eventType, void *message, NativeEventFilterResult *result) { Q_UNUSED(result) if (eventType != "xcb_generic_event_t") return false; auto event = static_cast(message); if ((event->response_type & ~0x80) != XCB_KEY_PRESS) return false; auto keyPressEvent = static_cast(message); // Avoid keyboard freeze xcb_connection_t *xcbConnection = QX11Info::connection(); xcb_allow_events(xcbConnection, XCB_ALLOW_REPLAY_KEYBOARD, keyPressEvent->time); xcb_flush(xcbConnection); unsigned int keycode = keyPressEvent->detail; unsigned int keystate = 0; if (keyPressEvent->state & XCB_MOD_MASK_1) keystate |= XCB_MOD_MASK_1; if (keyPressEvent->state & XCB_MOD_MASK_CONTROL) keystate |= XCB_MOD_MASK_CONTROL; if (keyPressEvent->state & XCB_MOD_MASK_4) keystate |= XCB_MOD_MASK_4; if (keyPressEvent->state & XCB_MOD_MASK_SHIFT) keystate |= XCB_MOD_MASK_SHIFT; return activateShortcut(keycode, keystate); } quint32 QxtGlobalShortcutPrivate::nativeModifiers(Qt::KeyboardModifiers modifiers) { quint32 native = 0; if (modifiers & Qt::ShiftModifier) native |= XCB_MOD_MASK_SHIFT; if (modifiers & Qt::ControlModifier) native |= XCB_MOD_MASK_CONTROL; if (modifiers & Qt::AltModifier) native |= XCB_MOD_MASK_1; if (modifiers & Qt::MetaModifier) native |= XCB_MOD_MASK_4; return native; } quint32 QxtGlobalShortcutPrivate::nativeKeycode(Qt::Key key) { quint32 native = 0; KeySym keysym = XStringToKeysym(QKeySequence(key).toString().toLatin1().data()); if (keysym == XCB_NO_SYMBOL) keysym = static_cast(key); xcb_key_symbols_t *xcbKeySymbols = xcb_key_symbols_alloc(QX11Info::connection()); QScopedPointer keycodes( xcb_key_symbols_get_keycode(xcbKeySymbols, keysym)); if (!keycodes.isNull()) native = keycodes.data()[0]; // Use the first keycode xcb_key_symbols_free(xcbKeySymbols); return native; } bool QxtGlobalShortcutPrivate::registerShortcut(quint32 nativeKey, quint32 nativeMods) { xcb_connection_t *xcbConnection = QX11Info::connection(); QList xcbCookies; for (quint32 maskMods : maskModifiers) { xcbCookies << xcb_grab_key_checked(xcbConnection, 1, QX11Info::appRootWindow(), nativeMods | maskMods, nativeKey, XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC); } bool failed = false; for (xcb_void_cookie_t cookie : std::as_const(xcbCookies)) { QScopedPointer error(xcb_request_check(xcbConnection, cookie)); failed = !error.isNull(); } if (failed) unregisterShortcut(nativeKey, nativeMods); return !failed; } bool QxtGlobalShortcutPrivate::unregisterShortcut(quint32 nativeKey, quint32 nativeMods) { xcb_connection_t *xcbConnection = QX11Info::connection(); QList xcbCookies; for (quint32 maskMods : maskModifiers) { xcb_ungrab_key(xcbConnection, nativeKey, QX11Info::appRootWindow(), nativeMods | maskMods); } bool failed = false; for (xcb_void_cookie_t cookie : xcbCookies) { QScopedPointer error(xcb_request_check(xcbConnection, cookie)); failed = !error.isNull(); } return !failed; } ================================================ FILE: src/libs/ui/searchitemdelegate.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "searchitemdelegate.h" #include #include #include #include #include #include #include using namespace Zeal::WidgetUi; SearchItemDelegate::SearchItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { } QList SearchItemDelegate::decorationRoles() const { return m_decorationRoles; } void SearchItemDelegate::setDecorationRoles(const QList &roles) { m_decorationRoles = roles; } int SearchItemDelegate::textHighlightRole() const { return m_textHighlightRole; } void SearchItemDelegate::setTextHighlightRole(int role) { m_textHighlightRole = role; } bool SearchItemDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) { if (event->type() != QEvent::ToolTip) return QStyledItemDelegate::helpEvent(event, view, option, index); if (sizeHint(option, index).width() < view->visualRect(index).width()) { QToolTip::hideText(); return QStyledItemDelegate::helpEvent(event, view, option, index); } QToolTip::showText(event->globalPos(), index.data().toString(), view); return true; } void SearchItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItem opt(option); QStyle *style = opt.widget->style(); // Find decoration roles with data present. QList roles; for (int role : m_decorationRoles) { if (!index.data(role).isNull()) roles.append(role); } // TODO: Implemented via initStyleOption() overload if (!roles.isEmpty()) { opt.features |= QStyleOptionViewItem::HasDecoration; opt.icon = index.data(roles.first()).value(); // Constrain decoration size to the style's small icon size to ensure consistent icon sizing. const int maxSize = style->pixelMetric(QStyle::PM_SmallIconSize, &opt, opt.widget); opt.decorationSize = {std::min(opt.decorationSize.width(), maxSize), std::min(opt.decorationSize.height(), maxSize)}; } style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget); const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &opt, opt.widget) + 1; if (!roles.isEmpty()) { QIcon::Mode mode = QIcon::Normal; if (!(opt.state & QStyle::State_Enabled)) mode = QIcon::Disabled; else if (opt.state & QStyle::State_Selected) mode = QIcon::Selected; const QIcon::State state = (opt.state & QStyle::State_Open) ? QIcon::On : QIcon::Off; // All icons are sized after the first one. QRect iconRect = style->subElementRect(QStyle::SE_ItemViewItemDecoration, &opt, opt.widget); // Undo RTL mirroring iconRect = style->visualRect(opt.direction, opt.rect, iconRect); const int dx = iconRect.width() + margin; for (int i = 1; i < roles.size(); ++i) { opt.decorationSize.rwidth() += dx; iconRect.translate(dx, 0); // Redo RTL mirroring const auto iconVisualRect = style->visualRect(opt.direction, opt.rect, iconRect); const QIcon icon = index.data(roles[i]).value(); icon.paint(painter, iconVisualRect, opt.decorationAlignment, mode, state); } } // This should not happen unless a docset is corrupted. if (index.data().isNull()) return; // Match QCommonStyle behavior. opt.features |= QStyleOptionViewItem::HasDisplay; opt.text = index.data().toString(); const QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &opt, opt.widget) .adjusted(margin, 0, -margin, 0); const QFontMetrics &fm = opt.fontMetrics; const QString elidedText = fm.elidedText(opt.text, opt.textElideMode, textRect.width()); // Get pre-computed match positions from model for highlighting. const QVector matchPositions = index.data(m_textHighlightRole).value>(); if (!matchPositions.isEmpty()) { painter->save(); painter->setRenderHint(QPainter::Antialiasing); painter->setPen(QColor::fromRgb(255, 253, 0)); const QColor highlightColor = (opt.state & (QStyle::State_Selected | QStyle::State_HasFocus)) ? QColor::fromRgb(255, 255, 100, 20) : QColor::fromRgb(255, 255, 100, 120); { // Group consecutive positions to reduce number of highlight rectangles const int matchCount = matchPositions.size(); int startPos = matchPositions[0]; int endPos = matchPositions[0]; for (int i = 1; i <= matchCount; ++i) { const bool isLast = (i == matchCount); const bool isConsecutive = !isLast && (matchPositions[i] == endPos + 1); if (isConsecutive) { endPos = matchPositions[i]; continue; } // Draw highlight for [startPos, endPos] // Use FULL text (opt.text) for position calculation, not elided text const int highlightStart = startPos; const int highlightLen = endPos - startPos + 1; QRect highlightRect = textRect.adjusted(fm.horizontalAdvance(opt.text.left(highlightStart)), 2, 0, -2); highlightRect.setWidth(fm.horizontalAdvance(opt.text.mid(highlightStart, highlightLen))); // Clip highlight to visible text area (handles elided text correctly) highlightRect = highlightRect.intersected(textRect.adjusted(0, 2, 0, -2)); // Only draw if rectangle is valid after clipping if (highlightRect.isValid() && !highlightRect.isEmpty()) { QPainterPath path; path.addRoundedRect(highlightRect, 2, 2); painter->fillPath(path, highlightColor); painter->drawPath(path); } if (!isLast) { startPos = matchPositions[i]; endPos = matchPositions[i]; } } } painter->restore(); } painter->save(); #ifdef Q_OS_WINDOWS // QWindowsVistaStyle overrides highlight color. if (style->objectName() == QLatin1String("windowsvista")) { opt.palette.setColor(QPalette::All, QPalette::HighlightedText, opt.palette.color(QPalette::Active, QPalette::Text)); } #endif const QPalette::ColorGroup cg = (opt.state & QStyle::State_Active) ? QPalette::Normal : QPalette::Inactive; if (opt.state & QStyle::State_Selected) painter->setPen(opt.palette.color(cg, QPalette::HighlightedText)); else painter->setPen(opt.palette.color(cg, QPalette::Text)); // Constant LeftToRight because we don't need to flip it any further. // Vertically align the text in the middle to match QCommonStyle behaviour. const auto alignedRect = QStyle::alignedRect(Qt::LeftToRight, opt.displayAlignment, QSize(textRect.size().width(), fm.height()), textRect); const auto textPoint = QPoint(alignedRect.x(), alignedRect.y() + fm.ascent()); // Force LTR, so that BiDi won't reorder ellipsis to the left. painter->drawText(textPoint, elidedText, Qt::TextFlag::TextForceLeftToRight, 0); painter->restore(); } QSize SearchItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItem opt(option); QStyle *style = opt.widget->style(); // Constrain decoration size to the style's small icon size to ensure consistent icon sizing. const int maxSize = style->pixelMetric(QStyle::PM_SmallIconSize, &opt, opt.widget); opt.decorationSize = {std::min(opt.decorationSize.width(), maxSize), std::min(opt.decorationSize.height(), maxSize)}; QSize size = QStyledItemDelegate::sizeHint(opt, index); size.setWidth(0); const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &opt, opt.widget) + 1; // Find decoration roles with data present. QList roles; for (int role : m_decorationRoles) { if (!index.data(role).isNull()) roles.append(role); } if (!roles.isEmpty()) { size.rwidth() = (opt.decorationSize.width() + margin) * roles.size() + margin; } size.rwidth() += opt.fontMetrics.horizontalAdvance(index.data().toString()) + margin * 2; return size; } ================================================ FILE: src/libs/ui/searchitemdelegate.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_WIDGETUI_SEARCHITEMDELEGATE_H #define ZEAL_WIDGETUI_SEARCHITEMDELEGATE_H #include namespace Zeal { namespace WidgetUi { class SearchItemDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit SearchItemDelegate(QObject *parent = nullptr); QList decorationRoles() const; void setDecorationRoles(const QList &roles); int textHighlightRole() const; void setTextHighlightRole(int role); bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) override; void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; private: QList m_decorationRoles = {Qt::DecorationRole}; int m_textHighlightRole = -1; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_SEARCHITEMDELEGATE_H ================================================ FILE: src/libs/ui/searchsidebar.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "searchsidebar.h" #include "searchitemdelegate.h" #include "widgets/layouthelper.h" #include "widgets/searchedit.h" #include "widgets/toolbarframe.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Zeal; using namespace Zeal::WidgetUi; SearchSidebar::SearchSidebar(QWidget *parent) : SearchSidebar(nullptr, parent) { } SearchSidebar::SearchSidebar(const SearchSidebar *other, QWidget *parent) : Sidebar::View(parent) { // Setup search result view. m_treeView = new QTreeView(); m_treeView->setFrameShape(QFrame::NoFrame); m_treeView->setHeaderHidden(true); m_treeView->setUniformRowHeights(true); // A stylesheet (even an empty one) wraps the widget in QStyleSheetStyle, // which properly re-polishes on dynamic color scheme changes. m_treeView->setStyleSheet(QStringLiteral("QTreeView {}")); #ifdef Q_OS_MACOS m_treeView->setAttribute(Qt::WA_MacShowFocusRect, false); #endif // Save expanded items. connect(m_treeView, &QTreeView::expanded, this, [this](const QModelIndex &index) { if (m_expandedIndexList.indexOf(index) == -1) { m_expandedIndexList.append(index); } }); connect(m_treeView, &QTreeView::collapsed, this, [this](const QModelIndex &index) { m_expandedIndexList.removeOne(index); }); auto delegate = new SearchItemDelegate(m_treeView); delegate->setDecorationRoles({Registry::ItemDataRole::DocsetIconRole, Qt::DecorationRole}); delegate->setTextHighlightRole(Registry::ItemDataRole::MatchPositionsRole); m_treeView->setItemDelegate(delegate); connect(m_treeView, &QTreeView::activated, this, &SearchSidebar::navigateToIndexAndActivate); connect(m_treeView, &QTreeView::clicked, this, &SearchSidebar::navigateToIndex); // Setup Alt+Up, Alt+Down, etc shortcuts. const auto keyList = {Qt::Key_Up, Qt::Key_Down, Qt::Key_PageUp, Qt::Key_PageDown, Qt::Key_Home, Qt::Key_End}; for (const auto key : keyList) { auto shortcut = new QShortcut(key | Qt::AltModifier, this); connect(shortcut, &QShortcut::activated, this, [this, key]() { QKeyEvent event(QKeyEvent::KeyPress, key, Qt::NoModifier); QCoreApplication::sendEvent(m_treeView, &event); }); } // Setup page TOC view. // TODO: Move to a separate Sidebar View. m_pageTocView = new QListView(); m_pageTocView->setFrameShape(QFrame::NoFrame); m_pageTocView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_pageTocView->setItemDelegate(new SearchItemDelegate(m_pageTocView)); m_pageTocView->setStyleSheet(QStringLiteral("QListView {}")); m_pageTocView->setUniformItemSizes(true); m_pageTocView->setVisible(false); #ifdef Q_OS_MACOS m_pageTocView->setAttribute(Qt::WA_MacShowFocusRect, false); #endif m_pageTocModel = new Registry::SearchModel(this); connect(m_pageTocModel, &Registry::SearchModel::updated, this, [this]() { if (m_pageTocModel->isEmpty()) { m_pageTocView->hide(); } else { m_pageTocView->show(); m_splitter->restoreState(Core::Application::instance()->settings()->tocSplitterState); } }); m_pageTocView->setModel(m_pageTocModel); connect(m_pageTocView, &QListView::activated, this, &SearchSidebar::navigateToIndexAndActivate); connect(m_pageTocView, &QListView::clicked, this, &SearchSidebar::navigateToIndex); // Setup search input box. m_searchEdit = new SearchEdit(); m_searchEdit->installEventFilter(this); setupSearchBoxCompletions(); // Clone state if `other` is provided. if (other) { if (other->m_searchEdit->text().isEmpty()) { m_searchModel = new Registry::SearchModel(this); setTreeViewModel(Core::Application::instance()->docsetRegistry()->model(), true); for (const QModelIndex &index : other->m_expandedIndexList) { m_treeView->expand(index); } } else { m_searchEdit->setText(other->m_searchEdit->text()); m_searchModel = other->m_searchModel->clone(this); setTreeViewModel(m_searchModel, false); } // Clone current selection. Signals must be disabled to avoid crash in the event handler. m_treeView->selectionModel()->blockSignals(true); for (const QModelIndex &index : other->m_treeView->selectionModel()->selectedIndexes()) { m_treeView->selectionModel()->select(index, QItemSelectionModel::Select); } m_treeView->selectionModel()->blockSignals(false); // Cannot update position until layout geometry is calculated, so set it in showEvent(). m_pendingVerticalPosition = other->m_treeView->verticalScrollBar()->value(); } else { m_searchModel = new Registry::SearchModel(this); setTreeViewModel(Core::Application::instance()->docsetRegistry()->model(), true); } connect(m_searchEdit, &QLineEdit::textChanged, this, [this](const QString &text) { QItemSelectionModel *oldSelectionModel = m_treeView->selectionModel(); if (text.isEmpty()) { setTreeViewModel(Core::Application::instance()->docsetRegistry()->model(), true); } else { setTreeViewModel(m_searchModel, false); } QItemSelectionModel *newSelectionModel = m_treeView->selectionModel(); if (newSelectionModel != oldSelectionModel) { // TODO: Remove once QTBUG-49966 is addressed. if (oldSelectionModel) { oldSelectionModel->deleteLater(); } // Connect to the new selection model. connect(m_treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SearchSidebar::navigateToSelectionWithDelay); } m_treeView->reset(); Core::Application::instance()->docsetRegistry()->search(text); }); auto toolBarLayout = new QVBoxLayout(); toolBarLayout->setContentsMargins(4, 0, 4, 0); toolBarLayout->setSpacing(4); toolBarLayout->addWidget(m_searchEdit); auto toolBarFrame = new ToolBarFrame(); toolBarFrame->setLayout(toolBarLayout); // Setup splitter. m_splitter = new QSplitter(); m_splitter->setOrientation(Qt::Vertical); m_splitter->addWidget(m_treeView); m_splitter->addWidget(m_pageTocView); connect(m_splitter, &QSplitter::splitterMoved, this, [this]() { Core::Application::instance()->settings()->tocSplitterState = m_splitter->saveState(); }); // Setup main layout. auto layout = LayoutHelper::createBorderlessLayout(); layout->addWidget(toolBarFrame); layout->addWidget(m_splitter); setLayout(layout); // Setup delayed navigation to a page until user makes a pause in typing a search query. m_delayedNavigationTimer = new QTimer(this); m_delayedNavigationTimer->setInterval(400); m_delayedNavigationTimer->setSingleShot(true); connect(m_delayedNavigationTimer, &QTimer::timeout, this, [this]() { const QModelIndex index = m_delayedNavigationTimer->property("index").toModelIndex(); if (!index.isValid()) { return; } navigateToIndex(index); }); // Setup Docset Registry. auto registry = Core::Application::instance()->docsetRegistry(); using Registry::DocsetRegistry; connect(registry, &DocsetRegistry::searchCompleted, this, [this](const QList &results) { if (!isVisible()) { return; } m_delayedNavigationTimer->stop(); m_searchModel->setResults(results); const QModelIndex index = m_searchModel->index(0, 0, QModelIndex()); m_treeView->setCurrentIndex(index); m_delayedNavigationTimer->setProperty("index", index); m_delayedNavigationTimer->start(); }); connect(registry, &DocsetRegistry::docsetAboutToBeUnloaded, this, [this](const QString &name) { if (isVisible()) { // Disable updates because removeSearchResultWithName can // call {begin,end}RemoveRows multiple times, and cause // degradation of UI responsiveness. m_treeView->setUpdatesEnabled(false); m_searchModel->removeSearchResultWithName(name); m_treeView->setUpdatesEnabled(true); } else { m_searchModel->removeSearchResultWithName(name); } setupSearchBoxCompletions(); }); connect(registry, &DocsetRegistry::docsetLoaded, this, [this](const QString &) { setupSearchBoxCompletions(); }); } void SearchSidebar::setTreeViewModel(QAbstractItemModel *model, bool isRootDecorated) { QItemSelectionModel *oldSelectionModel = m_treeView->selectionModel(); m_treeView->setModel(model); m_treeView->setRootIsDecorated(isRootDecorated); // Hide all but the first column. for (int i = 1; i < model->columnCount(); ++i) { m_treeView->setColumnHidden(i, true); } QItemSelectionModel *newSelectionModel = m_treeView->selectionModel(); if (newSelectionModel != oldSelectionModel) { // TODO: Remove once QTBUG-49966 is addressed. if (oldSelectionModel) { oldSelectionModel->deleteLater(); } // Connect to the new selection model. connect(m_treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SearchSidebar::navigateToSelectionWithDelay); } } SearchSidebar *SearchSidebar::clone(QWidget *parent) const { return new SearchSidebar(this, parent); } Registry::SearchModel *SearchSidebar::pageTocModel() const { return m_pageTocModel; } void SearchSidebar::focusSearchEdit(bool clear) { if (!isVisible()) { m_pendingSearchEditFocus = true; return; } m_searchEdit->setFocus(); if (clear) { m_searchEdit->clearQuery(); } } void SearchSidebar::search(const Registry::SearchQuery &query) { // TODO: Pass Registry::SearchQuery, converting to QString is dumb at this point. m_searchEdit->setText(query.toString()); } void SearchSidebar::navigateToIndex(const QModelIndex &index) { // When triggered by click, cancel delayed navigation request caused by the selection change. if (m_delayedNavigationTimer->isActive() && m_delayedNavigationTimer->property("index").toModelIndex() == index) { m_delayedNavigationTimer->stop(); } const QVariant url = index.data(Registry::ItemDataRole::UrlRole); if (url.isNull()) { return; } emit navigationRequested(url.toUrl()); } void SearchSidebar::navigateToIndexAndActivate(const QModelIndex &index) { const QVariant url = index.data(Registry::ItemDataRole::UrlRole); if (url.isNull()) { return; } emit navigationRequested(url.toUrl()); emit activated(); } void SearchSidebar::navigateToSelectionWithDelay(const QItemSelection &selection) { if (selection.isEmpty()) { return; } m_delayedNavigationTimer->setProperty("index", selection.indexes().first()); m_delayedNavigationTimer->start(); } void SearchSidebar::setupSearchBoxCompletions() { QStringList completions; const auto docsets = Core::Application::instance()->docsetRegistry()->docsets(); for (const Registry::Docset *docset : docsets) { const QStringList keywords = docset->keywords(); if (keywords.isEmpty()) continue; completions << keywords.constFirst() + QLatin1Char(':'); } if (completions.isEmpty()) return; m_searchEdit->setCompletions(completions); } bool SearchSidebar::eventFilter(QObject *object, QEvent *event) { if (object == m_searchEdit && event->type() == QEvent::KeyPress) { auto e = static_cast(event); switch (e->key()) { case Qt::Key_Return: emit activated(); break; case Qt::Key_Home: case Qt::Key_End: case Qt::Key_Left: case Qt::Key_Right: if (!m_searchEdit->text().isEmpty()) { break; } [[fallthrough]]; case Qt::Key_Down: case Qt::Key_Up: case Qt::Key_PageDown: case Qt::Key_PageUp: QCoreApplication::sendEvent(m_treeView, event); break; } } return Sidebar::View::eventFilter(object, event); } void SearchSidebar::showEvent(QShowEvent *event) { Q_UNUSED(event) if (m_pendingVerticalPosition > 0) { m_treeView->verticalScrollBar()->setValue(m_pendingVerticalPosition); m_pendingVerticalPosition = 0; } if (m_pendingSearchEditFocus) { m_searchEdit->setFocus(); m_pendingSearchEditFocus = false; } } ================================================ FILE: src/libs/ui/searchsidebar.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_WIDGETUI_SEARCHSIDEBAR_H #define ZEAL_WIDGETUI_SEARCHSIDEBAR_H #include #include #include class QItemSelection; class QSplitter; class QListView; class QTimer; class QTreeView; namespace Zeal { namespace Registry { class SearchModel; class SearchQuery; } // namespace Registry namespace WidgetUi { class SearchEdit; class SearchSidebar final : public Sidebar::View { Q_OBJECT Q_DISABLE_COPY_MOVE(SearchSidebar) public: explicit SearchSidebar(QWidget *parent = nullptr); SearchSidebar *clone(QWidget *parent = nullptr) const; Registry::SearchModel *pageTocModel() const; signals: void activated(); void navigationRequested(const QUrl &url); public slots: void focusSearchEdit(bool clear = false); void search(const Registry::SearchQuery &query); private slots: void navigateToIndex(const QModelIndex &index); void navigateToIndexAndActivate(const QModelIndex &index); void navigateToSelectionWithDelay(const QItemSelection &selection); void setupSearchBoxCompletions(); protected: bool eventFilter(QObject *object, QEvent *event) override; void showEvent(QShowEvent *event) override; private: explicit SearchSidebar(const SearchSidebar *other, QWidget *parent = nullptr); void setTreeViewModel(QAbstractItemModel *model, bool isRootDecorated); SearchEdit *m_searchEdit = nullptr; bool m_pendingSearchEditFocus = false; // Index and search results tree view state. QTreeView *m_treeView = nullptr; QModelIndexList m_expandedIndexList; int m_pendingVerticalPosition = 0; Registry::SearchModel *m_searchModel = nullptr; // TOC list view state. QListView *m_pageTocView = nullptr; Registry::SearchModel *m_pageTocModel = nullptr; QSplitter *m_splitter = nullptr; QTimer *m_delayedNavigationTimer = nullptr; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_SEARCHSIDEBAR_H ================================================ FILE: src/libs/ui/settingsdialog.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "settingsdialog.h" #include "ui_settingsdialog.h" #include #include #include #include #include #include #include #include using namespace Zeal; using namespace Zeal::WidgetUi; namespace { // QFontDatabase::standardSizes() lacks some sizes, like 13, which QtWK uses by default. constexpr int AvailableFontSizes[] = {9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 40, 44, 48, 56, 64, 72}; constexpr QWebEngineSettings::FontFamily BasicFontFamilies[] = {QWebEngineSettings::SerifFont, QWebEngineSettings::SansSerifFont, QWebEngineSettings::FixedFont}; } // namespace SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent) , ui(new Ui::SettingsDialog()) { ui->setupUi(this); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); // Setup signals & slots connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::saveSettings); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SettingsDialog::loadSettings); connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton *button) { if (button == ui->buttonBox->button(QDialogButtonBox::Apply)) saveSettings(); }); // Fonts ui->defaultFontComboBox->addItem(tr("Serif"), QStringLiteral("serif")); ui->defaultFontComboBox->addItem(tr("Sans-serif"), QStringLiteral("sans-serif")); ui->defaultFontComboBox->addItem(tr("Monospace"), QStringLiteral("monospace")); ui->minFontSizeComboBox->addItem(tr("None"), 0); for (int fontSize : AvailableFontSizes) { ui->fontSizeComboBox->addItem(QString::number(fontSize), fontSize); ui->fixedFontSizeComboBox->addItem(QString::number(fontSize), fontSize); ui->minFontSizeComboBox->addItem(QString::number(fontSize), fontSize); } // Fix tab order. setTabOrder(ui->defaultFontComboBox, ui->fontSizeComboBox); setTabOrder(ui->fontSizeComboBox, ui->serifFontComboBox); // Disable global shortcut settings if not supported. if (!QxtGlobalShortcut::isSupported()) { ui->globalHotKeyGroupBox->setEnabled(false); ui->globalHotKeyGroupBox->setToolTip(tr("Global shortcuts are not supported on the current platform.")); } QWebEngineSettings *webSettings = Browser::Settings::defaultProfile()->settings(); // Avoid casting in each connect. auto currentIndexChangedSignal = static_cast(&QComboBox::currentIndexChanged); auto syncStandardFont = [this, webSettings](QWebEngineSettings::FontFamily fontFamily, const QFont &font) { const int index = ui->defaultFontComboBox->currentIndex(); if (BasicFontFamilies[index] == fontFamily) { webSettings->setFontFamily(QWebEngineSettings::StandardFont, font.family()); } }; connect(ui->defaultFontComboBox, currentIndexChangedSignal, this, [webSettings](int index) { const QString fontFamily = webSettings->fontFamily(BasicFontFamilies[index]); webSettings->setFontFamily(QWebEngineSettings::StandardFont, fontFamily); }); connect(ui->serifFontComboBox, &QFontComboBox::currentFontChanged, this, [webSettings, syncStandardFont](const QFont &font) { webSettings->setFontFamily(QWebEngineSettings::SerifFont, font.family()); syncStandardFont(QWebEngineSettings::SerifFont, font); }); connect(ui->sansSerifFontComboBox, &QFontComboBox::currentFontChanged, this, [webSettings, syncStandardFont](const QFont &font) { webSettings->setFontFamily(QWebEngineSettings::SansSerifFont, font.family()); syncStandardFont(QWebEngineSettings::SansSerifFont, font); }); connect(ui->fixedFontComboBox, &QFontComboBox::currentFontChanged, this, [webSettings, syncStandardFont](const QFont &font) { webSettings->setFontFamily(QWebEngineSettings::FixedFont, font.family()); syncStandardFont(QWebEngineSettings::FixedFont, font); }); connect(ui->fontSizeComboBox, currentIndexChangedSignal, this, [webSettings](int index) { webSettings->setFontSize(QWebEngineSettings::DefaultFontSize, AvailableFontSizes[index]); }); connect(ui->fixedFontSizeComboBox, currentIndexChangedSignal, this, [webSettings](int index) { webSettings->setFontSize(QWebEngineSettings::DefaultFixedFontSize, AvailableFontSizes[index]); }); connect(ui->minFontSizeComboBox, currentIndexChangedSignal, this, [webSettings](int index) { const int fontSize = index == 0 ? 0 : AvailableFontSizes[index-1]; webSettings->setFontSize(QWebEngineSettings::MinimumFontSize, fontSize); }); loadSettings(); } SettingsDialog::~SettingsDialog() { delete ui; } void SettingsDialog::chooseCustomCssFile() { const QString file = QFileDialog::getOpenFileName(this, tr("Choose CSS File"), ui->customCssFileEdit->text(), tr("CSS Files (*.css);;All Files (*.*)")); if (file.isEmpty()) return; ui->customCssFileEdit->setText(QDir::toNativeSeparators(file)); } void SettingsDialog::chooseDocsetStoragePath() { QString path = QFileDialog::getExistingDirectory(this, tr("Open Directory"), ui->docsetStorageEdit->text()); if (path.isEmpty()) { return; } #ifdef PORTABLE_BUILD // Use relative path if selected directory is under the application binary path. if (path.startsWith(QCoreApplication::applicationDirPath() + QLatin1String("/"))) { const QDir appDirPath(QCoreApplication::applicationDirPath()); path = appDirPath.relativeFilePath(path); } #endif ui->docsetStorageEdit->setText(QDir::toNativeSeparators(path)); } void SettingsDialog::loadSettings() { const Core::Settings * const settings = Core::Application::instance()->settings(); // General Tab ui->startMinimizedCheckBox->setChecked(settings->startMinimized); ui->checkForUpdateCheckBox->setChecked(settings->checkForUpdate); ui->systrayGroupBox->setChecked(settings->showSystrayIcon); ui->minimizeToSystrayCheckBox->setChecked(settings->minimizeToSystray); ui->hideToSystrayCheckBox->setChecked(settings->hideOnClose); ui->toolButton->setKeySequence(settings->showShortcut); ui->docsetStorageEdit->setText(QDir::toNativeSeparators(settings->docsetPath)); // Tabs Tab ui->openNewTabAfterActive->setChecked(settings->openNewTabAfterActive); // Search Tab ui->fuzzySearchCheckBox->setChecked(settings->isFuzzySearchEnabled); // Content Tab for (int i = 0; i < ui->defaultFontComboBox->count(); ++i) { if (ui->defaultFontComboBox->itemData(i).toString() == settings->defaultFontFamily) { ui->defaultFontComboBox->setCurrentIndex(i); break; } } ui->serifFontComboBox->setCurrentText(settings->serifFontFamily); ui->sansSerifFontComboBox->setCurrentText(settings->sansSerifFontFamily); ui->fixedFontComboBox->setCurrentText(settings->fixedFontFamily); ui->fontSizeComboBox->setCurrentText(QString::number(settings->defaultFontSize)); ui->fixedFontSizeComboBox->setCurrentText(QString::number(settings->defaultFixedFontSize)); ui->minFontSizeComboBox->setCurrentText(QString::number(settings->minimumFontSize)); switch (settings->contentAppearance) { case Core::Settings::ContentAppearance::Automatic: ui->appearanceAutoRadioButton->setChecked(true); break; case Core::Settings::ContentAppearance::Light: ui->appearanceLightRadioButton->setChecked(true); break; case Core::Settings::ContentAppearance::Dark: ui->appearanceDarkRadioButton->setChecked(true); break; } ui->highlightOnNavigateCheckBox->setChecked(settings->isHighlightOnNavigateEnabled); ui->customCssFileEdit->setText(QDir::toNativeSeparators(settings->customCssFile)); switch (settings->externalLinkPolicy) { case Core::Settings::ExternalLinkPolicy::Ask: ui->radioExternalLinkAsk->setChecked(true); break; case Core::Settings::ExternalLinkPolicy::Open: ui->radioExternalLinkOpen->setChecked(true); break; case Core::Settings::ExternalLinkPolicy::OpenInSystemBrowser: ui->radioExternalLinkOpenDesktop->setChecked(true); break; } ui->useSmoothScrollingCheckBox->setChecked(settings->isSmoothScrollingEnabled); // Network Tab switch (settings->proxyType) { case Core::Settings::ProxyType::None: ui->noProxySettings->setChecked(true); break; case Core::Settings::ProxyType::System: ui->systemProxySettings->setChecked(true); break; case Core::Settings::ProxyType::Http: ui->manualProxySettings->setChecked(true); ui->proxyTypeHttpRadioButton->setChecked(true); break; case Core::Settings::ProxyType::Socks5: ui->manualProxySettings->setChecked(true); ui->proxyTypeSocks5RadioButton->setChecked(true); break; } ui->proxyHostEdit->setText(settings->proxyHost); ui->proxyPortEdit->setValue(settings->proxyPort); ui->proxyRequiresAuthCheckBox->setChecked(settings->proxyAuthenticate); ui->proxyUsernameEdit->setText(settings->proxyUserName); ui->proxyPasswordEdit->setText(settings->proxyPassword); ui->ignoreSslErrorsCheckBox->setChecked(settings->isIgnoreSslErrorsEnabled); } void SettingsDialog::saveSettings() { Core::Settings * const settings = Core::Application::instance()->settings(); // General Tab settings->startMinimized = ui->startMinimizedCheckBox->isChecked(); settings->checkForUpdate = ui->checkForUpdateCheckBox->isChecked(); settings->showSystrayIcon = ui->systrayGroupBox->isChecked(); settings->minimizeToSystray = ui->minimizeToSystrayCheckBox->isChecked(); settings->hideOnClose = ui->hideToSystrayCheckBox->isChecked(); settings->showShortcut = ui->toolButton->keySequence(); settings->docsetPath = QDir::fromNativeSeparators(ui->docsetStorageEdit->text()); // Tabs Tab settings->openNewTabAfterActive = ui->openNewTabAfterActive->isChecked(); // Search Tab settings->isFuzzySearchEnabled = ui->fuzzySearchCheckBox->isChecked(); // Content Tab #if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) // Applying dark mode requires restart. ui->appearanceLabel->setText(tr("Appearance (requires restart):")); #endif settings->defaultFontFamily = ui->defaultFontComboBox->currentData().toString(); settings->serifFontFamily = ui->serifFontComboBox->currentText(); settings->sansSerifFontFamily = ui->sansSerifFontComboBox->currentText(); settings->fixedFontFamily = ui->fixedFontComboBox->currentText(); settings->defaultFontSize = ui->fontSizeComboBox->currentData().toInt(); settings->defaultFixedFontSize = ui->fixedFontSizeComboBox->currentData().toInt(); settings->minimumFontSize = ui->minFontSizeComboBox->currentData().toInt(); if (ui->appearanceAutoRadioButton->isChecked()) { settings->contentAppearance = Core::Settings::ContentAppearance::Automatic; } else if (ui->appearanceLightRadioButton->isChecked()) { settings->contentAppearance = Core::Settings::ContentAppearance::Light; } else if (ui->appearanceDarkRadioButton->isChecked()) { settings->contentAppearance = Core::Settings::ContentAppearance::Dark; } settings->isHighlightOnNavigateEnabled = ui->highlightOnNavigateCheckBox->isChecked(); settings->customCssFile = QDir::fromNativeSeparators(ui->customCssFileEdit->text()); if (ui->radioExternalLinkAsk->isChecked()) { settings->externalLinkPolicy = Core::Settings::ExternalLinkPolicy::Ask; } else if (ui->radioExternalLinkOpen->isChecked()) { settings->externalLinkPolicy = Core::Settings::ExternalLinkPolicy::Open; } else if (ui->radioExternalLinkOpenDesktop->isChecked()) { settings->externalLinkPolicy = Core::Settings::ExternalLinkPolicy::OpenInSystemBrowser; } settings->isSmoothScrollingEnabled = ui->useSmoothScrollingCheckBox->isChecked(); // Network Tab // Proxy settings if (ui->noProxySettings->isChecked()) { settings->proxyType = Core::Settings::ProxyType::None; } else if (ui->systemProxySettings->isChecked()) { settings->proxyType = Core::Settings::ProxyType::System; } else if (ui->manualProxySettings->isChecked()) { if (ui->proxyTypeSocks5RadioButton->isChecked()) { settings->proxyType = Core::Settings::ProxyType::Socks5; } else { settings->proxyType = Core::Settings::ProxyType::Http; } } settings->proxyHost = ui->proxyHostEdit->text(); settings->proxyPort = ui->proxyPortEdit->text().toUShort(); settings->proxyAuthenticate = ui->proxyRequiresAuthCheckBox->isChecked(); settings->proxyUserName = ui->proxyUsernameEdit->text(); settings->proxyPassword = ui->proxyPasswordEdit->text(); settings->isIgnoreSslErrorsEnabled = ui->ignoreSslErrorsCheckBox->isChecked(); settings->save(); } ================================================ FILE: src/libs/ui/settingsdialog.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_WIDGETUI_SETTINGSDIALOG_H #define ZEAL_WIDGETUI_SETTINGSDIALOG_H #include namespace Zeal { namespace WidgetUi { namespace Ui { class SettingsDialog; } // namespace Ui class SettingsDialog : public QDialog { Q_OBJECT public: explicit SettingsDialog(QWidget *parent = nullptr); ~SettingsDialog() override; public slots: void chooseCustomCssFile(); void chooseDocsetStoragePath(); private: void loadSettings(); void saveSettings(); private: Ui::SettingsDialog *ui = nullptr; friend class Ui::SettingsDialog; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_SETTINGSDIALOG_H ================================================ FILE: src/libs/ui/settingsdialog.ui ================================================ Zeal::WidgetUi::SettingsDialog Qt::ApplicationModal 0 0 600 700 Preferences 0 General Startup Start minimized Check for update true Show system tray icon true Minimize to system tray instead of taskbar Hide to system tray on window close Global shortcuts QFormLayout::AllNonFixedFieldsGrow Show Zeal: Click to set shortcut true Docset storage QFormLayout::AllNonFixedFieldsGrow &Directory: docsetStorageEdit true &Browse… Qt::Vertical 20 40 Tabs Behavior Open new tab after active Qt::Vertical 20 40 Search Local search Use fuzzy search (experimental) Qt::Vertical 20 40 Content Style Appearance: A&utomatic true &Light &Dark Qt::Horizontal 40 20 &Custom CSS file: customCssFileEdit true Bro&wse… &Highlight on navigate Fonts QLayout::SetMinimumSize De&fault: defaultFontComboBox &Serif: serifFontComboBox QFontComboBox::ScalableFonts Sa&ns-serif: sansSerifFontComboBox QFontComboBox::ScalableFonts &Monospace: fixedFontComboBox QFontComboBox::MonospacedFonts|QFontComboBox::ScalableFonts Si&ze: fontSizeComboBox Qt::Vertical 20 40 Siz&e: fixedFontSizeComboBox Qt::Horizontal 40 20 Minimum f&ont size: minFontSizeComboBox External Link Behavior &Ask every time true Open in desktop &browser O&pen in Zeal Other Use smoo&th scrolling Qt::Vertical 20 40 Network Proxy Settings No prox&y false true &Use system proxy settings true &Manual proxy configuration false Pro&xy host: proxyHostEdit Type: &HTTP true proxyTypeButtonGroup &SOCKS5 proxyTypeButtonGroup Qt::Horizontal 40 20 &Port: proxyPortEdit 65535 &Authentication required false User&name: proxyUsernameEdit false Pass&word: proxyPasswordEdit false false QLineEdit::Password Security Ignore SSL errors Qt::Vertical 20 40 QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok ShortcutEdit QLineEdit
ui/widgets/shortcutedit.h
SettingsDialog QDialog
ui/settingsdialog.h
buttonBox accepted() Zeal::WidgetUi::SettingsDialog accept() 248 254 157 274 buttonBox rejected() Zeal::WidgetUi::SettingsDialog reject() 316 260 286 274 customCssFileBrowseButton clicked() Zeal::WidgetUi::SettingsDialog chooseCustomCssFile() 526 232 299 249 docsetStorageBrowseButton clicked() Zeal::WidgetUi::SettingsDialog chooseDocsetStoragePath() 526 371 299 249 manualProxySettings toggled(bool) manualProxySettingsGroup setEnabled(bool) 299 131 299 222 proxyRequiresAuthCheckBox toggled(bool) proxyUsernameLabel setEnabled(bool) 299 224 66 250 proxyRequiresAuthCheckBox toggled(bool) proxyUsernameEdit setEnabled(bool) 299 224 330 250 proxyRequiresAuthCheckBox toggled(bool) proxyPasswordLabel setEnabled(bool) 299 224 65 278 proxyRequiresAuthCheckBox toggled(bool) proxyPasswordEdit setEnabled(bool) 299 224 330 278
================================================ FILE: src/libs/ui/sidebarviewprovider.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "sidebarviewprovider.h" #include "browsertab.h" #include "mainwindow.h" #include "searchsidebar.h" using namespace Zeal; using namespace Zeal::WidgetUi; SidebarViewProvider::SidebarViewProvider(MainWindow *mainWindow) : Sidebar::ViewProvider(mainWindow) , m_mainWindow(mainWindow) { connect(m_mainWindow, &MainWindow::currentTabChanged, this, &SidebarViewProvider::viewChanged, Qt::QueuedConnection); } Sidebar::View *SidebarViewProvider::view(const QString &id) const { if (id != QLatin1String("index")) { return nullptr; } if (auto tab = m_mainWindow->currentTab()) { return tab->searchSidebar(); } return nullptr; } ================================================ FILE: src/libs/ui/sidebarviewprovider.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_WIDGETUI_SIDEBARVIEWPROVIDER_H #define ZEAL_WIDGETUI_SIDEBARVIEWPROVIDER_H #include namespace Zeal { namespace WidgetUi { class MainWindow; class SidebarViewProvider : public Sidebar::ViewProvider { Q_OBJECT Q_DISABLE_COPY_MOVE(SidebarViewProvider) public: explicit SidebarViewProvider(MainWindow *mainWindow); Sidebar::View *view(const QString &id = QString()) const override; private: MainWindow *m_mainWindow = nullptr; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_SIDEBARVIEWPROVIDER_H ================================================ FILE: src/libs/ui/widgets/CMakeLists.txt ================================================ add_library(Widgets STATIC layouthelper.cpp searchedit.cpp shortcutedit.cpp toolbarframe.cpp ) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED) target_link_libraries(Widgets PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) ================================================ FILE: src/libs/ui/widgets/layouthelper.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "layouthelper.h" ================================================ FILE: src/libs/ui/widgets/layouthelper.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_WIDGETUI_LAYOUTHELPER_H #define ZEAL_WIDGETUI_LAYOUTHELPER_H #include namespace Zeal { namespace WidgetUi { namespace LayoutHelper { template Layout *createBorderlessLayout() { static_assert(std::is_base_of::value, "Layout must derive from QLayout"); auto layout = new Layout(); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); return layout; } } // namespace LayoutHelper } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_LAYOUTHELPER_H ================================================ FILE: src/libs/ui/widgets/searchedit.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "searchedit.h" #include #include #include #include #include #include using namespace Zeal; using namespace Zeal::WidgetUi; SearchEdit::SearchEdit(QWidget *parent) : QLineEdit(parent) { setClearButtonEnabled(true); setPlaceholderText(tr("Search")); // Force QStyleSheetStyle wrapping so dynamic color scheme changes apply. setStyleSheet(QStringLiteral("QLineEdit {}")); m_completionLabel = new QLabel(this); m_completionLabel->setFont(font()); m_completionLabel->setObjectName(QStringLiteral("completer")); m_completionLabel->setStyleSheet(QStringLiteral("QLabel#completer { color: gray; }")); connect(this, &SearchEdit::textChanged, this, &SearchEdit::showCompletions); } // Makes the line edit use autocompletion. void SearchEdit::setCompletions(const QStringList &completions) { delete m_prefixCompleter; m_prefixCompleter = new QCompleter(completions, this); m_prefixCompleter->setCaseSensitivity(Qt::CaseInsensitive); m_prefixCompleter->setCompletionMode(QCompleter::InlineCompletion); m_prefixCompleter->setWidget(this); } // Clear input with consideration to docset filters void SearchEdit::clearQuery() { setText(text().left(queryStart())); } void SearchEdit::selectQuery() { if (text().isEmpty()) return; const int pos = hasSelectedText() ? selectionStart() : cursorPosition(); const int queryPos = queryStart(); const int textSize = text().size(); if (pos >= queryPos && selectionEnd() < textSize) { setSelection(queryPos, textSize); return; } // Avoid some race condition which breaks Ctrl+K shortcut. QTimer::singleShot(0, this, &QLineEdit::selectAll); } bool SearchEdit::event(QEvent *event) { switch (event->type()) { case QEvent::KeyPress: { auto keyEvent = static_cast(event); // Tab key cannot be overriden in keyPressEvent(). if (keyEvent->key() == Qt::Key_Tab) { const QString completed = currentCompletion(text()); if (!completed.isEmpty()) { setText(completed); } return true; } else if (keyEvent->key() == Qt::Key_Escape) { clearQuery(); return true; } break; } case QEvent::ShortcutOverride: { // TODO: Should be obtained from the ActionManager. static const QStringList focusShortcuts = { QStringLiteral("Ctrl+K"), QStringLiteral("Ctrl+L") }; auto keyEvent = static_cast(event); const int keyCode = keyEvent->key() | static_cast(keyEvent->modifiers()); if (focusShortcuts.contains(QKeySequence(keyCode).toString())) { selectQuery(); event->accept(); return true; } break; } default: break; } return QLineEdit::event(event); } void SearchEdit::focusInEvent(QFocusEvent *event) { QLineEdit::focusInEvent(event); // Do not change the default behavior when focused with mouse. if (event->reason() == Qt::MouseFocusReason) return; selectQuery(); } void SearchEdit::showCompletions(const QString &newValue) { if (!isVisible()) return; const int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); const int textWidth = fontMetrics().horizontalAdvance(newValue); if (m_prefixCompleter) m_prefixCompleter->setCompletionPrefix(text()); const QString completed = currentCompletion(newValue).mid(newValue.size()); const QSize labelSize(fontMetrics().horizontalAdvance(completed), size().height()); const int shiftX = static_cast(window()->devicePixelRatioF() * (frameWidth + 2)) + textWidth; m_completionLabel->setMinimumSize(labelSize); m_completionLabel->move(shiftX, 0); m_completionLabel->setText(completed); } QString SearchEdit::currentCompletion(const QString &text) const { if (text.isEmpty() || !m_prefixCompleter) return QString(); return m_prefixCompleter->currentCompletion(); } int SearchEdit::queryStart() const { const Registry::SearchQuery currentQuery = Registry::SearchQuery::fromString(text()); // Keep the filter for the first Escape press return currentQuery.query().isEmpty() ? 0 : currentQuery.keywordPrefixSize(); } ================================================ FILE: src/libs/ui/widgets/searchedit.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_WIDGETUI_SEARCHEDIT_H #define ZEAL_WIDGETUI_SEARCHEDIT_H #include class QCompleter; class QEvent; class QLabel; namespace Zeal { namespace WidgetUi { class SearchEdit : public QLineEdit { Q_OBJECT public: explicit SearchEdit(QWidget *parent = nullptr); void clearQuery(); void selectQuery(); void setCompletions(const QStringList &completions); protected: bool event(QEvent *event) override; void focusInEvent(QFocusEvent *event) override; private slots: void showCompletions(const QString &text); private: QString currentCompletion(const QString &text) const; int queryStart() const; QCompleter *m_prefixCompleter = nullptr; QLabel *m_completionLabel = nullptr; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_SEARCHEDIT_H ================================================ FILE: src/libs/ui/widgets/shortcutedit.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "shortcutedit.h" #include #include using namespace Zeal::WidgetUi; ShortcutEdit::ShortcutEdit(QWidget *parent) : ShortcutEdit(QString(), parent) { } ShortcutEdit::ShortcutEdit(const QString &text, QWidget *parent) : QLineEdit(text, parent) { connect(this, &QLineEdit::textChanged, [this](const QString &text) { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) m_key = QKeySequence(text, QKeySequence::NativeText)[0]; #else m_key = QKeySequence(text, QKeySequence::NativeText)[0].toCombined(); #endif }); } bool ShortcutEdit::event(QEvent *event) { switch (event->type()) { case QEvent::KeyPress: { auto keyEvent = static_cast(event); switch (keyEvent->key()) { case Qt::Key_Alt: case Qt::Key_Control: case Qt::Key_Meta: case Qt::Key_Shift: return true; default: m_key = keyEvent->key(); m_key |= translateModifiers(keyEvent->modifiers(), keyEvent->text()); setText(keySequence().toString(QKeySequence::NativeText)); } return true; } case QEvent::ShortcutOverride: case QEvent::KeyRelease: case QEvent::Shortcut: return true; default: return QLineEdit::event(event); } } QKeySequence ShortcutEdit::keySequence() const { return QKeySequence(m_key); } void ShortcutEdit::setKeySequence(const QKeySequence &keySequence) { setText(keySequence.toString(QKeySequence::NativeText)); } // Inspired by QKeySequenceEditPrivate::translateModifiers() int ShortcutEdit::translateModifiers(Qt::KeyboardModifiers state, const QString &text) { int modifiers = 0; // The shift modifier only counts when it is not used to type a symbol // that is only reachable using the shift key if ((state & Qt::ShiftModifier) && (text.isEmpty() || !text.at(0).isPrint() || text.at(0).isLetterOrNumber() || text.at(0).isSpace())) { modifiers |= Qt::ShiftModifier; } modifiers |= state & Qt::ControlModifier; modifiers |= state & Qt::MetaModifier; modifiers |= state & Qt::AltModifier; return modifiers; } ================================================ FILE: src/libs/ui/widgets/shortcutedit.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2013-2014 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_WIDGETUI_SHORTCUTEDIT_H #define ZEAL_WIDGETUI_SHORTCUTEDIT_H #include namespace Zeal { namespace WidgetUi { class ShortcutEdit : public QLineEdit { Q_OBJECT public: explicit ShortcutEdit(QWidget *parent = nullptr); explicit ShortcutEdit(const QString &text, QWidget *parent = nullptr); bool event(QEvent *event) override; QKeySequence keySequence() const; void setKeySequence(const QKeySequence &keySequence); private: int translateModifiers(Qt::KeyboardModifiers state, const QString &text); int m_key = 0; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_SHORTCUTEDIT_H ================================================ FILE: src/libs/ui/widgets/toolbarframe.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "toolbarframe.h" #include using namespace Zeal::WidgetUi; ToolBarFrame::ToolBarFrame(QWidget *parent) : QWidget(parent) { setMaximumHeight(40); setMinimumHeight(40); // Force QStyleSheetStyle wrapping so dynamic color scheme changes apply. setStyleSheet(QStringLiteral("ToolBarFrame {}")); } void ToolBarFrame::paintEvent(QPaintEvent *event) { QWidget::paintEvent(event); // Draw a line at the bottom. QPainter painter(this); painter.setPen(palette().mid().color()); painter.drawLine(0, height() - 1, width() - 1, height() - 1); } ================================================ FILE: src/libs/ui/widgets/toolbarframe.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_WIDGETUI_TOOLBARFRAME_H #define ZEAL_WIDGETUI_TOOLBARFRAME_H #include namespace Zeal { namespace WidgetUi { class ToolBarFrame : public QWidget { Q_OBJECT public: explicit ToolBarFrame(QWidget *parent = nullptr); private: void paintEvent(QPaintEvent *event) override; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_TOOLBARFRAME_H ================================================ FILE: src/libs/util/CMakeLists.txt ================================================ add_library(Util STATIC fuzzy.cpp humanizer.cpp plist.cpp sqlitedatabase.cpp # Show headers without .cpp in Qt Creator. caseinsensitivemap.h ) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED) target_link_libraries(Util PRIVATE Qt${QT_VERSION_MAJOR}::Core) find_package(SQLite3 REQUIRED) target_link_libraries(Util PRIVATE SQLite::SQLite3) # TODO: Do not export SQLite headers. target_include_directories(Util PUBLIC ${SQLite3_INCLUDE_DIRS}) # Tests if(BUILD_TESTING) add_subdirectory(tests) endif() ================================================ FILE: src/libs/util/caseinsensitivemap.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_UTIL_CASEINSENSITIVEMAP_H #define ZEAL_UTIL_CASEINSENSITIVEMAP_H #include #include namespace Zeal { namespace Util { struct CaseInsensitiveStringComparator { bool operator()(const QString &lhs, const QString &rhs) const { return QString::compare(lhs, rhs, Qt::CaseInsensitive) < 0; } }; template using CaseInsensitiveMap = std::map; } // namespace Util } // namespace Zeal #endif // ZEAL_UTIL_CASEINSENSITIVEMAP_H ================================================ FILE: src/libs/util/fuzzy.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "fuzzy.h" #include #include #include namespace Zeal { namespace Util { namespace Fuzzy { namespace { constexpr double SCORE_GAP_LEADING = -0.005; constexpr double SCORE_GAP_TRAILING = -0.005; constexpr double SCORE_GAP_INNER = -0.01; constexpr double SCORE_MATCH_CONSECUTIVE = 1.0; constexpr double SCORE_MATCH_SLASH = 0.9; constexpr double SCORE_MATCH_WORD = 0.8; constexpr double SCORE_MATCH_CAPITAL = 0.7; constexpr double SCORE_MATCH_DOT = 0.6; constexpr int FZY_MAX_LEN = 1024; void precomputeBonus(const QString &haystack, double *matchBonus) { // Initialize to '/' so the first character of the haystack always receives // SCORE_MATCH_SLASH (0.9), the highest boundary bonus. This mirrors fzy's // original file-path design where every path component starts after a '/'. // For Zeal's symbol names the first character is conceptually a word // boundary, but the high bonus is kept intentionally: it strongly rewards // prefix matches. QChar lastCh = '/'; for (int i = 0; i < haystack.length(); ++i) { const QChar ch = haystack[i]; if (lastCh == '/' || lastCh == '\\') { matchBonus[i] = SCORE_MATCH_SLASH; } else if (lastCh == '-' || lastCh == '_' || lastCh == ' ') { matchBonus[i] = SCORE_MATCH_WORD; } else if (lastCh == '.' || lastCh == ':') { matchBonus[i] = SCORE_MATCH_DOT; } else if (lastCh.isLower() && ch.isUpper()) { matchBonus[i] = SCORE_MATCH_CAPITAL; } else { matchBonus[i] = 0.0; } lastCh = ch; } } // Check if all characters in needle exist in haystack (in order, case-insensitive) // This is a pre-filter before running the expensive DP algorithm bool hasMatch(const QString &needle, const QString &haystack) { int haystackPos = 0; const int haystackLen = haystack.length(); for (int i = 0; i < needle.length(); ++i) { const QChar needleCh = needle[i].toLower(); bool found = false; while (haystackPos < haystackLen) { if (haystack[haystackPos].toLower() == needleCh) { found = true; ++haystackPos; break; } ++haystackPos; } if (!found) { return false; } } return true; } } // anonymous namespace // ============================================================================ // High-level Qt convenience API implementation // ============================================================================ double score(const QString &needle, const QString &haystack, QVector *positions) { // Pre-filter: check if all needle characters exist in haystack (performance optimization) // This avoids expensive DP computation on unmatchable strings if (!needle.isEmpty() && !haystack.isEmpty() && hasMatch(needle, haystack)) { return computeScore(needle, haystack, positions); } // No match - return -infinity (SQL will filter with WHERE score > 0) if (positions) { positions->clear(); } return -std::numeric_limits::infinity(); } // ============================================================================ // Low-level API implementation // ============================================================================ double computeScore(const QString &needle, const QString &haystack, QVector *positions) { const int needleLen = needle.length(); const int haystackLen = haystack.length(); if (needleLen == 0 || haystackLen == 0 || needleLen > haystackLen) { if (positions) { positions->clear(); } return -std::numeric_limits::infinity(); } if (needleLen == haystackLen) { // Equal length strings get infinity score only if they actually match (case-insensitive) if (needle.compare(haystack, Qt::CaseInsensitive) == 0) { if (positions) { // Fill positions for exact match: [0, 1, 2, ..., n-1] positions->resize(needleLen); for (int i = 0; i < needleLen; ++i) { (*positions)[i] = i; } } return std::numeric_limits::infinity(); } // Otherwise return no match - equal length non-matching strings can't fuzzy match if (positions) { positions->clear(); } return -std::numeric_limits::infinity(); } if (haystackLen > FZY_MAX_LEN || needleLen > FZY_MAX_LEN) { if (positions) { positions->clear(); } return -std::numeric_limits::infinity(); } double matchBonus[FZY_MAX_LEN] = {}; // Zero-initialize to satisfy static analyzer precomputeBonus(haystack, matchBonus); const double SCORE_MIN = -std::numeric_limits::infinity(); // Always allocate 2D tables on heap (simpler, memory overhead negligible for typical searches) double **D = new double*[needleLen]; double **M = new double*[needleLen]; for (int i = 0; i < needleLen; ++i) { D[i] = new double[haystackLen]; M[i] = new double[haystackLen]; } // Forward pass: compute scores for (int i = 0; i < needleLen; ++i) { double prevScore = SCORE_MIN; const double gapScore = (i == needleLen - 1) ? SCORE_GAP_TRAILING : SCORE_GAP_INNER; const QChar needleCh = needle[i].toLower(); for (int j = 0; j < haystackLen; ++j) { if (needleCh == haystack[j].toLower()) { double score = SCORE_MIN; if (i == 0) { score = (j * SCORE_GAP_LEADING) + matchBonus[j]; } else if (j > 0) { const double prevM = M[i - 1][j - 1]; const double prevD = D[i - 1][j - 1]; score = std::max(prevM + matchBonus[j], prevD + SCORE_MATCH_CONSECUTIVE); } D[i][j] = score; M[i][j] = prevScore = std::max(score, prevScore + gapScore); } else { D[i][j] = SCORE_MIN; M[i][j] = prevScore = prevScore + gapScore; } } } const double result = M[needleLen - 1][haystackLen - 1]; // Backtrack to find positions if requested (fzy algorithm) // Only backtrack if we have a valid match (not SCORE_MIN) if (positions != nullptr && result != SCORE_MIN) { positions->resize(needleLen); bool matchRequired = false; for (int i = needleLen - 1, j = haystackLen - 1; i >= 0; --i) { for (; j >= 0; --j) { // Check if this is a valid match position on the optimal path if (D[i][j] != SCORE_MIN && (matchRequired || D[i][j] == M[i][j])) { // Check if we used consecutive match bonus to get here. // Use D[i][j] (score at this specific position), not M[i][j] // (global prefix optimum), which may reflect a different path entirely. matchRequired = (i > 0 && j > 0 && D[i][j] == D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE); (*positions)[i] = j; --j; break; } } } } // Clean up for (int i = 0; i < needleLen; ++i) { delete[] D[i]; delete[] M[i]; } delete[] D; delete[] M; return result; } double scoreFunction(const QString &needle, const QString &haystack) { return score(needle, haystack, nullptr); } // Legacy C-string wrapper for SQLite callback double scoreFunction(const char *needle, const char *haystack) { return scoreFunction(QString::fromUtf8(needle), QString::fromUtf8(haystack)); } } // namespace Fuzzy } // namespace Util } // namespace Zeal ================================================ FILE: src/libs/util/fuzzy.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_UTIL_FUZZY_H #define ZEAL_UTIL_FUZZY_H #include #include // TODO: [Qt 6] Use QList. namespace Zeal { namespace Util { namespace Fuzzy { // Fuzzy matching is based on https://github.com/jhawthorn/fzy by John Hawthorn, MIT License. /** * @brief Computes fuzzy match score for QString inputs, optionally returning match positions * * Convenience wrapper around computeScore() with pre-filtering for performance. * Returns raw fzy scores - caller should filter results (e.g., WHERE score > 0 in SQL). * * Score ranges: * - Exact matches (needle == haystack): infinity * - Valid fuzzy matches: unbounded; ~0.9 + (n-1) for a perfect length-n consecutive match * - Poor matches: negative scores (many gaps outweigh bonuses) * - No match: -infinity * * @param needle Search query * @param haystack Text to search in * @param positions Optional output list of matched haystack indices for highlighting * @return Match score (higher is better, -infinity for no match) */ double score(const QString &needle, const QString &haystack, QVector *positions = nullptr); /** * @brief Computes fuzzy match score, optionally returning match positions * * Low-level scoring function using the fzy algorithm (https://github.com/jhawthorn/fzy). * Uses case-insensitive matching with Unicode support via Qt. When positions are requested, * backtracks through the DP matrices to find the exact character positions that produced * the best score. * * @param needle Search query * @param haystack Text to search in * @param positions Optional output list of haystack character indices * @return Fuzzy match score (-infinity if no match possible, infinity if needle == haystack, * otherwise unbounded: ~0.9 + (n-1) for a perfect length-n consecutive match) */ double computeScore(const QString &needle, const QString &haystack, QVector *positions = nullptr); /** * @brief Main scoring function for use in SQLite callbacks * * Thin wrapper around score() that provides fzy-based fuzzy matching. * Returns raw fzy scores - use WHERE score > 0 in SQL to filter results. * * Score ranges: * - Exact matches (needle == haystack): infinity * - Valid fuzzy matches: unbounded; ~0.9 + (n-1) for a perfect length-n consecutive match * - Poor matches: negative scores (many gaps outweigh bonuses) * - No match: -infinity * * @param needle Search query * @param haystack Text to search in * @return Match score (higher is better, -infinity for no match) */ double scoreFunction(const QString &needle, const QString &haystack); /** * @brief Legacy C-string wrapper for scoreFunction * * Provided for compatibility with SQLite callbacks. Converts C strings to QString * and calls the QString version. * * @param needle Search query (UTF-8 C string) * @param haystack Text to search in (UTF-8 C string) * @return Match score (higher is better, -infinity for no match) */ double scoreFunction(const char *needle, const char *haystack); } // namespace Fuzzy } // namespace Util } // namespace Zeal #endif // ZEAL_UTIL_FUZZY_H ================================================ FILE: src/libs/util/humanizer.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "humanizer.h" #include #include using namespace Zeal::Util; namespace { // Time thresholds in seconds. constexpr double Minute = 60.0; constexpr double Hour = 3600.0; constexpr double Day = 86400.0; // constexpr double Week = 604800.0; // Not used at the moment. constexpr double Month = 2629746.0; // Average month (30.44 days) constexpr double Year = 31556952.0; // Average year (365.25 days) QString humanizeDuration(double seconds) { if (seconds < 45) { return Humanizer::tr("a few seconds"); } else if (seconds < 90) { return Humanizer::tr("a minute"); } else if (seconds < 45 * Minute) { const int minutes = static_cast(std::round(seconds / Minute)); // return Humanizer::tr("%n minute(s)", nullptr, minutes); return Humanizer::tr("%1 minutes").arg(minutes); } else if (seconds < 90 * Minute) { return Humanizer::tr("an hour"); } else if (seconds < 22 * Hour) { const int hours = static_cast(std::round(seconds / Hour)); // return Humanizer::tr("%n hour(s)", nullptr, hours); return Humanizer::tr("%1 hours").arg(hours); } else if (seconds < 36 * Hour) { return Humanizer::tr("a day"); } else if (seconds < 25 * Day) { const int days = static_cast(std::round(seconds / Day)); // return Humanizer::tr("%n day(s)", nullptr, days); return Humanizer::tr("%1 days").arg(days); } else if (seconds < 45 * Day) { return Humanizer::tr("a month"); } else if (seconds < 320 * Day) { const int months = static_cast(std::round(seconds / Month)); // return Humanizer::tr("%n month(s)", nullptr, months); return Humanizer::tr("%1 months").arg(months); } else if (seconds < 548 * Day) { return Humanizer::tr("a year"); } else { const int years = static_cast(std::round(seconds / Year)); // return Humanizer::tr("%n year(s)", nullptr, years); return Humanizer::tr("%1 years").arg(years); } } } // namespace QString Humanizer::fromNow(const QDateTime& dt) { const double seconds = QDateTime::currentDateTime().secsTo(dt); const QString humanizedDuration = humanizeDuration(std::abs(seconds)); return (seconds > 0 ? tr("%1 from now") : tr("%1 ago")).arg(humanizedDuration); } ================================================ FILE: src/libs/util/humanizer.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_UTIL_HUMANIZER_H #define ZEAL_UTIL_HUMANIZER_H #include #include class QDateTime; namespace Zeal { namespace Util { class Humanizer { Q_DECLARE_TR_FUNCTIONS(Humanizer) Q_DISABLE_COPY_MOVE(Humanizer) Humanizer() = delete; ~Humanizer() = delete; public: static QString fromNow(const QDateTime& dt); }; } // namespace Util } // namespace Zeal #endif // ZEAL_UTIL_HUMANIZER_H ================================================ FILE: src/libs/util/plist.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "plist.h" #include #include #include using namespace Zeal::Util; static Q_LOGGING_CATEGORY(log, "zeal.util.plist") bool Plist::read(const QString &fileName) { QScopedPointer file(new QFile(fileName)); if (!file->open(QIODevice::ReadOnly)) { qCWarning(log, "Cannot open plist file '%s'.", qPrintable(fileName)); m_hasError = true; return false; } QXmlStreamReader xml(file.data()); while (!xml.atEnd()) { const QXmlStreamReader::TokenType token = xml.readNext(); if (token != QXmlStreamReader::StartElement) continue; if (xml.name() != QLatin1String("key")) continue; // TODO: Should it fail here? const QString key = xml.readElementText(); // Skip whitespaces between tags while (xml.readNext() == QXmlStreamReader::Characters); if (xml.tokenType() != QXmlStreamReader::StartElement) continue; QVariant value; if (xml.name() == QLatin1String("string")) value = xml.readElementText(); else if (xml.name() == QLatin1String("true")) value = true; else if (xml.name() == QLatin1String("false")) value = false; else continue; // Skip unknown types insert(key, value); } return !m_hasError; } bool Plist::hasError() const { return m_hasError; } ================================================ FILE: src/libs/util/plist.h ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_UTIL_PLIST_H #define ZEAL_UTIL_PLIST_H #include #include namespace Zeal { namespace Util { class Plist : public QHash { public: Plist() = default; bool read(const QString &fileName); bool hasError() const; private: bool m_hasError = false; }; } // namespace Util } // namespace Zeal #endif // ZEAL_UTIL_PLIST_H ================================================ FILE: src/libs/util/sqlitedatabase.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2016 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #include "sqlitedatabase.h" #include using namespace Zeal::Util; namespace { constexpr char ListTablesSql[] = "SELECT name" " FROM" " (SELECT * FROM sqlite_master UNION ALL" " SELECT * FROM sqlite_temp_master)" " WHERE type='table'" " ORDER BY name"; constexpr char ListViewsSql[] = "SELECT name" " FROM" " (SELECT * FROM sqlite_master UNION ALL" " SELECT * FROM sqlite_temp_master)" " WHERE type='view'" " ORDER BY name"; // sqlite3_exec() callback used in tables() and views(). auto ListCallback = [](void *ptr, int, char **data, char **) { static_cast(ptr)->append(QString::fromUtf8(data[0])); return 0; }; } // namespace SQLiteDatabase::SQLiteDatabase(const QString &path) { if (sqlite3_initialize() != SQLITE_OK) { return; } if (sqlite3_open16(path.constData(), &m_db) != SQLITE_OK) { updateLastError(); close(); } } SQLiteDatabase::~SQLiteDatabase() { finalize(); close(); } bool SQLiteDatabase::isOpen() const { return m_db != nullptr; } QStringList SQLiteDatabase::tables() { if (m_db == nullptr) { return {}; } QStringList list; char *errmsg = nullptr; const int rc = sqlite3_exec(m_db, ListTablesSql, ListCallback, &list, &errmsg); if (rc != SQLITE_OK) { if (errmsg) { m_lastError = QString::fromUtf8(errmsg); sqlite3_free(errmsg); } return {}; } return list; } QStringList SQLiteDatabase::views() { if (m_db == nullptr) { return {}; } QStringList list; char *errmsg = nullptr; const int rc = sqlite3_exec(m_db, ListViewsSql, ListCallback, &list, &errmsg); if (rc != SQLITE_OK) { if (errmsg) { m_lastError = QString::fromUtf8(errmsg); sqlite3_free(errmsg); } return {}; } return list; } bool SQLiteDatabase::prepare(const QString &sql) { if (m_db == nullptr) { return false; } if (m_stmt != nullptr) { finalize(); } m_lastError.clear(); sqlite3_mutex_enter(sqlite3_db_mutex(m_db)); const void *pzTail = nullptr; const int res = sqlite3_prepare16_v2(m_db, sql.constData(), (sql.size() + 1) * 2, // 2 = sizeof(QChar) &m_stmt, &pzTail); sqlite3_mutex_leave(sqlite3_db_mutex(m_db)); if (res != SQLITE_OK) { // "Unable to execute statement" updateLastError(); finalize(); return false; } if (pzTail && !QString(static_cast(pzTail)).trimmed().isEmpty()) { // Unable to execute multiple statements at a time updateLastError(); finalize(); return false; } return true; } bool SQLiteDatabase::next() { if (m_stmt == nullptr) { return false; } sqlite3_mutex_enter(sqlite3_db_mutex(m_db)); const int res = sqlite3_step(m_stmt); sqlite3_mutex_leave(sqlite3_db_mutex(m_db)); switch (res) { case SQLITE_ROW: return true; case SQLITE_DONE: case SQLITE_CONSTRAINT: case SQLITE_ERROR: case SQLITE_MISUSE: case SQLITE_BUSY: default: updateLastError(); } return false; } bool SQLiteDatabase::execute(const QString &sql) { if (m_db == nullptr) { return false; } m_lastError.clear(); char *errmsg = nullptr; const int rc = sqlite3_exec(m_db, sql.toUtf8(), nullptr, nullptr, &errmsg); if (rc != SQLITE_OK) { if (errmsg) { m_lastError = QString::fromUtf8(errmsg); sqlite3_free(errmsg); } return false; } return true; } QVariant SQLiteDatabase::value(int index) const { Q_ASSERT(index >= 0); // sqlite3_data_count() returns 0 if m_stmt is nullptr. if (index >= sqlite3_data_count(m_stmt)) { return QVariant(); } sqlite3_mutex_enter(sqlite3_db_mutex(m_db)); const int type = sqlite3_column_type(m_stmt, index); QVariant ret; switch (type) { case SQLITE_INTEGER: ret = sqlite3_column_int64(m_stmt, index); break; case SQLITE_NULL: ret = QVariant(); break; default: ret = QString(static_cast(sqlite3_column_text16(m_stmt, index)), sqlite3_column_bytes16(m_stmt, index) / 2); // 2 = sizeof(QChar) break; } sqlite3_mutex_leave(sqlite3_db_mutex(m_db)); return ret; } QString SQLiteDatabase::lastError() const { return m_lastError; } void SQLiteDatabase::close() { sqlite3_close(m_db); m_db = nullptr; } void SQLiteDatabase::finalize() { sqlite3_mutex_enter(sqlite3_db_mutex(m_db)); sqlite3_finalize(m_stmt); m_stmt = nullptr; sqlite3_mutex_leave(sqlite3_db_mutex(m_db)); } void SQLiteDatabase::updateLastError() { if (m_db == nullptr) { return; } m_lastError = QString(static_cast(sqlite3_errmsg16(m_db))); } sqlite3 *SQLiteDatabase::handle() const { return m_db; } ================================================ FILE: src/libs/util/sqlitedatabase.h ================================================ // Copyright (C) Oleg Shparber, et al. // Copyright (C) 2016 Jerzy Kozera // SPDX-License-Identifier: GPL-3.0-or-later #ifndef ZEAL_UTIL_SQLITEDATABASE_H #define ZEAL_UTIL_SQLITEDATABASE_H #include #include struct sqlite3; struct sqlite3_stmt; namespace Zeal { namespace Util { class SQLiteDatabase { Q_DISABLE_COPY_MOVE(SQLiteDatabase) public: explicit SQLiteDatabase(const QString &path); virtual ~SQLiteDatabase(); bool isOpen() const; QStringList tables(); QStringList views(); bool prepare(const QString &sql); bool next(); bool execute(const QString &sql); QVariant value(int index) const; QString lastError() const; sqlite3 *handle() const; private: void close(); void finalize(); void updateLastError(); sqlite3 *m_db = nullptr; sqlite3_stmt *m_stmt = nullptr; QString m_lastError; }; } // namespace Util } // namespace Zeal #endif // ZEAL_UTIL_SQLITEDATABASE_H ================================================ FILE: src/libs/util/tests/CMakeLists.txt ================================================ find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test) # Fuzzy matching tests add_executable(fuzzy_test fuzzy_test.cpp) target_link_libraries(fuzzy_test PRIVATE Util Qt::Test) add_test(NAME fuzzy_test COMMAND fuzzy_test) ================================================ FILE: src/libs/util/tests/fuzzy_test.cpp ================================================ // Copyright (C) Oleg Shparber, et al. // SPDX-License-Identifier: GPL-3.0-or-later #include "../fuzzy.h" #include #include using namespace Zeal::Util::Fuzzy; class FuzzyTest : public QObject { Q_OBJECT private slots: void testEmptyStrings(); void testExactMatch(); void testExactMatchAtWordBoundary(); void testFuzzyMatch(); void testPositionsFuzzy(); void testPositionsExact(); void testCamelCase(); void testUnicode(); void testPubTrimIssue(); void testPubProtIssue(); void testNoMatch(); void testScoreOrdering(); // Edge cases void testSingleCharacter(); void testNeedleLongerThanHaystack(); void testEqualLengthNonMatching(); void testSpecialCharacters(); void testMaxLength(); // Bonus verification void testSlashBonus(); void testBackslashBonus(); void testColonBonus(); void testDotBonus(); void testWordBoundaryBonuses(); void testConsecutiveBonus(); // Negative scores void testManyGapsNegativeScore(); // Position handling void testPositionsClearedOnNoMatch(); void testPositionsForInfinityScore(); // Return values void testInfinityForEqualLengthMatch(); void testNegativeInfinityScenarios(); // Space handling void testSpaceHandling(); // Backtracking regression tests void testBacktrackingPrefixConflict(); void testBacktrackingWordBoundaryWins(); }; void FuzzyTest::testEmptyStrings() { // Empty needle or haystack should return -infinity QCOMPARE(score(QString(), QStringLiteral("test")), -std::numeric_limits::infinity()); QCOMPARE(score(QStringLiteral("test"), QString()), -std::numeric_limits::infinity()); QCOMPARE(score(QString(), QString()), -std::numeric_limits::infinity()); } void FuzzyTest::testExactMatch() { // Exact match at start should score highest double scoreStart = score(QStringLiteral("test"), QStringLiteral("test")); double scoreMiddle = score(QStringLiteral("test"), QStringLiteral("prefix_test")); double scoreEnd = score(QStringLiteral("test"), QStringLiteral("prefix_middle_test")); QVERIFY(scoreStart > 0); QVERIFY(scoreMiddle > 0); QVERIFY(scoreEnd > 0); // Start should score higher than middle or end QVERIFY(scoreStart > scoreMiddle); QVERIFY(scoreMiddle > scoreEnd); } void FuzzyTest::testExactMatchAtWordBoundary() { // Match after word boundary should score higher than match in middle of word double scoreWordBoundary = score(QStringLiteral("trim"), QStringLiteral("Publisher.prototype.toTrim")); double scoreMiddle = score(QStringLiteral("trim"), QStringLiteral("sometrimword")); QVERIFY(scoreWordBoundary > 0); QVERIFY(scoreMiddle > 0); QVERIFY(scoreWordBoundary > scoreMiddle); } void FuzzyTest::testFuzzyMatch() { // Fuzzy match should still work double fuzzyScore = score(QStringLiteral("abc"), QStringLiteral("aXbXc")); QVERIFY(fuzzyScore > 0); // Consecutive match should score higher than non-consecutive double consecutive = score(QStringLiteral("abc"), QStringLiteral("abc")); double nonConsecutive = score(QStringLiteral("abc"), QStringLiteral("aXbXc")); QVERIFY(consecutive > nonConsecutive); } void FuzzyTest::testPositionsFuzzy() { QVector positions; double fuzzyScore = score(QStringLiteral("abc"), QStringLiteral("aXbXc"), &positions); QVERIFY(fuzzyScore > 0); QCOMPARE(positions.size(), 3); QCOMPARE(positions[0], 0); // 'a' QCOMPARE(positions[1], 2); // 'b' QCOMPARE(positions[2], 4); // 'c' } void FuzzyTest::testPositionsExact() { QVector positions; double exactScore = score(QStringLiteral("test"), QStringLiteral("prefix_test"), &positions); QVERIFY(exactScore > 0); QCOMPARE(positions.size(), 4); QCOMPARE(positions[0], 7); // 't' QCOMPARE(positions[1], 8); // 'e' QCOMPARE(positions[2], 9); // 's' QCOMPARE(positions[3], 10); // 't' } void FuzzyTest::testCamelCase() { // Should detect camelCase word boundaries QVector positions; double camelScore = score(QStringLiteral("path"), QStringLiteral("HasPermissionForPath"), &positions); QVERIFY(camelScore > 0); QCOMPARE(positions.size(), 4); // Should match 'Path' at the end, not scattered through the string QCOMPARE(positions[0], 16); // 'P' QCOMPARE(positions[1], 17); // 'a' QCOMPARE(positions[2], 18); // 't' QCOMPARE(positions[3], 19); // 'h' } void FuzzyTest::testUnicode() { // Should handle Unicode correctly double unicodeScore = score(QStringLiteral("café"), QStringLiteral("café")); QVERIFY(unicodeScore > 0); // Case-insensitive Unicode double caseScore = score(QStringLiteral("café"), QStringLiteral("CAFÉ")); QVERIFY(caseScore > 0); } void FuzzyTest::testPubTrimIssue() { // Regression test: "pubtrim" should highlight Publisher + toTrim, not prototype QVector positions; score(QStringLiteral("pubtrim"), QStringLiteral("Publisher.prototype.toTrim"), &positions); QCOMPARE(positions.size(), 7); // Should match "Pub" from Publisher QCOMPARE(positions[0], 0); // 'P' QCOMPARE(positions[1], 1); // 'u' QCOMPARE(positions[2], 2); // 'b' // Should match "trim" from toTrim, NOT from prototype QVERIFY(positions[3] >= 20); // 't' should be in "toTrim" (position 20+) } void FuzzyTest::testPubProtIssue() { // Regression test: "pubprot" should highlight Publisher + prototype QVector positions; score(QStringLiteral("pubprot"), QStringLiteral("Publisher.prototype.fieldsToTrim"), &positions); QCOMPARE(positions.size(), 7); // Should match "Pub" from Publisher QCOMPARE(positions[0], 0); // 'P' QCOMPARE(positions[1], 1); // 'u' QCOMPARE(positions[2], 2); // 'b' // Should match "prot" from prototype (around position 10-14), not from ToTrim QVERIFY(positions[3] >= 10 && positions[3] <= 14); QVERIFY(positions[6] >= 10 && positions[6] <= 14); } void FuzzyTest::testNoMatch() { // No match should return -infinity QCOMPARE(score(QStringLiteral("xyz"), QStringLiteral("abc")), -std::numeric_limits::infinity()); } void FuzzyTest::testScoreOrdering() { // Scores should order results correctly double score1 = score(QStringLiteral("array"), QStringLiteral("Array")); double score2 = score(QStringLiteral("array"), QStringLiteral("ArrayList")); double score3 = score(QStringLiteral("array"), QStringLiteral("someArrayMethod")); // Exact match at start > camelCase word boundary > middle of word QVERIFY(score1 > score2); QVERIFY(score2 > score3); } // ============================================================================ // Edge Cases // ============================================================================ void FuzzyTest::testSingleCharacter() { // Single character needle should work double s = score(QStringLiteral("a"), QStringLiteral("abc")); QVERIFY(s > 0); // Single character exact match double exact = score(QStringLiteral("a"), QStringLiteral("a")); QCOMPARE(exact, std::numeric_limits::infinity()); // Single character no match double noMatch = score(QStringLiteral("z"), QStringLiteral("abc")); QCOMPARE(noMatch, -std::numeric_limits::infinity()); // Single character in haystack double singleHay = score(QStringLiteral("ab"), QStringLiteral("a")); QCOMPARE(singleHay, -std::numeric_limits::infinity()); } void FuzzyTest::testNeedleLongerThanHaystack() { // Needle longer than haystack should return -infinity double s = score(QStringLiteral("longstring"), QStringLiteral("short")); QCOMPARE(s, -std::numeric_limits::infinity()); } void FuzzyTest::testEqualLengthNonMatching() { // Equal length strings that don't match should return -infinity double s = score(QStringLiteral("abcd"), QStringLiteral("efgh")); QCOMPARE(s, -std::numeric_limits::infinity()); // Case-insensitive equal length match should return infinity double match = score(QStringLiteral("test"), QStringLiteral("TEST")); QCOMPARE(match, std::numeric_limits::infinity()); } void FuzzyTest::testSpecialCharacters() { // Should handle special characters QVector positions; double s = score(QStringLiteral("a-b"), QStringLiteral("foo-a-bar-b"), &positions); QVERIFY(s > 0); QCOMPARE(positions.size(), 3); // Underscores double underscore = score(QStringLiteral("ab"), QStringLiteral("a_b")); QVERIFY(underscore > 0); // Spaces double space = score(QStringLiteral("ab"), QStringLiteral("a b")); QVERIFY(space > 0); } void FuzzyTest::testMaxLength() { // FZY_MAX_LEN (1024) gates the DP algorithm only. Equal-length strings use // a simple strcmp shortcut and correctly return infinity regardless of length. // Use unequal lengths to exercise the DP length guard. QString longNeedle(1025, 'a'); QString longHaystack(1026, 'a'); double s = score(longNeedle, longHaystack); QCOMPARE(s, -std::numeric_limits::infinity()); // Equal-length exact match at the limit should work QString maxNeedle(1024, 'a'); QString maxHaystack(1024, 'a'); double atLimit = score(maxNeedle, maxHaystack); QCOMPARE(atLimit, std::numeric_limits::infinity()); } // ============================================================================ // Bonus Verification // ============================================================================ void FuzzyTest::testSlashBonus() { // Character after '/' should get SCORE_MATCH_SLASH bonus double afterSlash = score(QStringLiteral("test"), QStringLiteral("path/test")); double noSlash = score(QStringLiteral("test"), QStringLiteral("path_test")); QVERIFY(afterSlash > 0); QVERIFY(noSlash > 0); // Slash bonus should score higher than underscore (word boundary) QVERIFY(afterSlash > noSlash); } void FuzzyTest::testBackslashBonus() { // Character after '\\' should get SCORE_MATCH_SLASH bonus (Windows path support) double afterBackslash = score(QStringLiteral("test"), QStringLiteral("path\\test")); double noBackslash = score(QStringLiteral("test"), QStringLiteral("path_test")); QVERIFY(afterBackslash > 0); QVERIFY(noBackslash > 0); // Backslash bonus should equal slash bonus QVERIFY(afterBackslash > noBackslash); } void FuzzyTest::testColonBonus() { // Character after ':' should get SCORE_MATCH_DOT bonus (namespace delimiter) double afterColon = score(QStringLiteral("method"), QStringLiteral("Class:method")); double noColon = score(QStringLiteral("method"), QStringLiteral("Classmethod")); QVERIFY(afterColon > 0); QVERIFY(noColon > 0); // Colon bonus should score higher than no bonus QVERIFY(afterColon > noColon); } void FuzzyTest::testDotBonus() { // Character after '.' should get SCORE_MATCH_DOT bonus double afterDot = score(QStringLiteral("ext"), QStringLiteral("file.ext")); double noDot = score(QStringLiteral("ext"), QStringLiteral("fileext")); QVERIFY(afterDot > 0); QVERIFY(noDot > 0); QVERIFY(afterDot > noDot); } void FuzzyTest::testWordBoundaryBonuses() { // Test different word boundary bonuses in order // Slash (0.9) > Word boundary (0.8, underscore/dash/space) > Dot (0.6) double slashScore = score(QStringLiteral("t"), QStringLiteral("a/t")); double underscoreScore = score(QStringLiteral("t"), QStringLiteral("a_t")); double dashScore = score(QStringLiteral("t"), QStringLiteral("a-t")); double spaceScore = score(QStringLiteral("t"), QStringLiteral("a t")); double dotScore = score(QStringLiteral("t"), QStringLiteral("a.t")); // Slash should be highest QVERIFY(slashScore > underscoreScore); QVERIFY(slashScore > dotScore); // Word boundaries (underscore, dash, space) should be equal and higher than dot QVERIFY(underscoreScore > dotScore); QVERIFY(dashScore > dotScore); QVERIFY(spaceScore > dotScore); } void FuzzyTest::testConsecutiveBonus() { // Consecutive matches should score higher than non-consecutive double consecutive = score(QStringLiteral("abc"), QStringLiteral("abc")); double withGaps = score(QStringLiteral("abc"), QStringLiteral("axbxc")); QVERIFY(consecutive > 0); QVERIFY(withGaps > 0); QVERIFY(consecutive > withGaps); } // ============================================================================ // Negative Scores // ============================================================================ void FuzzyTest::testManyGapsNegativeScore() { // The first haystack character always receives SCORE_MATCH_SLASH (0.9) because // precomputeBonus initialises lastCh to '/'. Each inner gap costs -0.01, so // 90+ gap characters are needed before the total score goes negative. const QString haystack = QStringLiteral("a") + QString(50, 'x') + QStringLiteral("b") + QString(50, 'x') + QStringLiteral("c"); double manyGaps = score(QStringLiteral("abc"), haystack); // Should still match but with negative score QVERIFY(manyGaps < 0); QVERIFY(manyGaps > -std::numeric_limits::infinity()); } // ============================================================================ // Position Handling // ============================================================================ void FuzzyTest::testPositionsClearedOnNoMatch() { QVector positions; positions.append(1); positions.append(2); positions.append(3); // No match should clear positions double s = score(QStringLiteral("xyz"), QStringLiteral("abc"), &positions); QCOMPARE(s, -std::numeric_limits::infinity()); QCOMPARE(positions.size(), 0); } void FuzzyTest::testPositionsForInfinityScore() { QVector positions; // Equal length exact match should return infinity and fill positions sequentially double s = score(QStringLiteral("test"), QStringLiteral("test"), &positions); QCOMPARE(s, std::numeric_limits::infinity()); QCOMPARE(positions.size(), 4); QCOMPARE(positions[0], 0); QCOMPARE(positions[1], 1); QCOMPARE(positions[2], 2); QCOMPARE(positions[3], 3); } // ============================================================================ // Return Values // ============================================================================ void FuzzyTest::testInfinityForEqualLengthMatch() { // Exact case-insensitive match of equal length strings should return infinity QCOMPARE(score(QStringLiteral("test"), QStringLiteral("test")), std::numeric_limits::infinity()); QCOMPARE(score(QStringLiteral("TeSt"), QStringLiteral("test")), std::numeric_limits::infinity()); QCOMPARE(score(QStringLiteral("TEST"), QStringLiteral("test")), std::numeric_limits::infinity()); } void FuzzyTest::testNegativeInfinityScenarios() { // All scenarios that should return -infinity // Empty strings QCOMPARE(score(QString(), QStringLiteral("test")), -std::numeric_limits::infinity()); QCOMPARE(score(QStringLiteral("test"), QString()), -std::numeric_limits::infinity()); QCOMPARE(score(QString(), QString()), -std::numeric_limits::infinity()); // No match QCOMPARE(score(QStringLiteral("xyz"), QStringLiteral("abc")), -std::numeric_limits::infinity()); // Needle longer than haystack QCOMPARE(score(QStringLiteral("longer"), QStringLiteral("ab")), -std::numeric_limits::infinity()); // Equal length non-matching QCOMPARE(score(QStringLiteral("abcd"), QStringLiteral("efgh")), -std::numeric_limits::infinity()); // Strings too long (> FZY_MAX_LEN = 1024) — use unequal lengths to reach // the DP length guard (equal-length match shortcuts to +infinity) QString tooLong(1025, 'a'); QString tooLongHaystack(1026, 'a'); QCOMPARE(score(tooLong, tooLongHaystack), -std::numeric_limits::infinity()); } // ============================================================================ // Space Handling // ============================================================================ void FuzzyTest::testSpaceHandling() { // Spaces are treated as literal characters in fzy algorithm // They must match exactly and give word boundary bonus to the next character QVector positions; // Space in needle matches space in haystack double spaceMatch = score(QStringLiteral("a b"), QStringLiteral("a b"), &positions); QCOMPARE(spaceMatch, std::numeric_limits::infinity()); // Exact match QCOMPARE(positions.size(), 3); QCOMPARE(positions[0], 0); // 'a' QCOMPARE(positions[1], 1); // ' ' QCOMPARE(positions[2], 2); // 'b' // Space in needle does NOT match underscore in haystack double noMatch = score(QStringLiteral("a b"), QStringLiteral("a_b")); QCOMPARE(noMatch, -std::numeric_limits::infinity()); // Character after space gets word boundary bonus. // Note: fzy scores can be slightly negative for valid matches due to leading // gap penalties — test relative ordering, not absolute sign. double afterSpace = score(QStringLiteral("b"), QStringLiteral("a b")); double noBonus = score(QStringLiteral("b"), QStringLiteral("ab")); QVERIFY(afterSpace > -std::numeric_limits::infinity()); QVERIFY(noBonus > -std::numeric_limits::infinity()); QVERIFY(afterSpace > noBonus); // Space gives bonus // Fuzzy match with space - space must be present double fuzzyWithSpace = score(QStringLiteral("a c"), QStringLiteral("a b c")); QVERIFY(fuzzyWithSpace > 0); // Matches 'a' at 0, 'c' at 4 positions.clear(); score(QStringLiteral("a c"), QStringLiteral("a b c"), &positions); QCOMPARE(positions.size(), 3); QCOMPARE(positions[0], 0); // 'a' QCOMPARE(positions[1], 1); // ' ' (first space — 'a'+'space' are consecutive) QCOMPARE(positions[2], 4); // 'c' // Space in needle cannot match non-space in haystack double noSpaceMatch = score(QStringLiteral("a b"), QStringLiteral("abc")); QCOMPARE(noSpaceMatch, -std::numeric_limits::infinity()); } // ============================================================================ // Backtracking Regression Tests // ============================================================================ void FuzzyTest::testBacktrackingPrefixConflict() { // Bug: "string" in "str::to_string" was highlighting "st" + "ring" instead // of the complete "string" at the word boundary after '_'. // // Root cause: backtracking used M[i][j] instead of D[i][j] to determine // matchRequired. M[i][j] reflects the global prefix optimum (s,t,r at 0,1,2 // decaying via gaps), not the path being backtracked, so matchRequired // incorrectly dropped to false and the algorithm fell back to j=0,1. QVector positions; score(QStringLiteral("string"), QStringLiteral("str::to_string"), &positions); QCOMPARE(positions.size(), 6); // Must be the consecutive "string" from "to_string", not split across the string QCOMPARE(positions[0], 8); // 's' QCOMPARE(positions[1], 9); // 't' QCOMPARE(positions[2], 10); // 'r' QCOMPARE(positions[3], 11); // 'i' QCOMPARE(positions[4], 12); // 'n' QCOMPARE(positions[5], 13); // 'g' } void FuzzyTest::testBacktrackingWordBoundaryWins() { // Bug: "string" in "Static String" was highlighting "S" + "tring" instead // of the complete "String" word (positions 7-12). // // The 'S' at position 0 (slash bonus 0.9) builds up an M score that is // higher than D at position 7 (word boundary bonus 0.8), causing the same // matchRequired bug to drop false and fall back to j=0 for the first char. QVector positions; score(QStringLiteral("string"), QStringLiteral("Static String"), &positions); QCOMPARE(positions.size(), 6); // Must be the consecutive "String" word, not "S" from "Static" + "tring" QCOMPARE(positions[0], 7); // 'S' QCOMPARE(positions[1], 8); // 't' QCOMPARE(positions[2], 9); // 'r' QCOMPARE(positions[3], 10); // 'i' QCOMPARE(positions[4], 11); // 'n' QCOMPARE(positions[5], 12); // 'g' } QTEST_MAIN(FuzzyTest) #include "fuzzy_test.moc" ================================================ FILE: vcpkg.json ================================================ { "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", "builtin-baseline": "120deac3062162151622ca4860575a33844ba10b", "dependencies": [ { "name": "cpp-httplib", "default-features": false }, { "name": "libarchive", "default-features": false }, "openssl", "sqlite3", "vulkan-headers" ] }