├── .github ├── dependabot.yml └── workflows │ ├── benchmark.yml │ ├── build-upload.yml │ ├── coverage.yml │ ├── msys2.yml │ ├── test.yml │ └── valgrind.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── conftest.py ├── docs ├── Makefile ├── api.rst ├── changelog.rst ├── conf.py ├── index.rst ├── intro.rst └── license.rst ├── noxfile.py ├── pyproject.toml ├── requirements-benchmark.txt ├── requirements-coverage.txt ├── requirements-doc.txt ├── requirements-test.txt ├── setup.py ├── src └── pybase64 │ ├── __init__.py │ ├── __main__.py │ ├── _fallback.py │ ├── _license.pyi │ ├── _pybase64.c │ ├── _pybase64.pyi │ ├── _pybase64_get_simd_flags.c │ ├── _pybase64_get_simd_flags.h │ ├── _typing.py │ ├── _version.py │ └── py.typed └── tests ├── __init__.py ├── conftest.py ├── test_benchmark.py ├── test_main.py ├── test_pybase64.py └── utils.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | # Maintain dependencies for python 10 | - package-ecosystem: "pip" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | 15 | # Maintain dependencies for git submodule (base64) 16 | - package-ecosystem: "gitsubmodule" 17 | directory: "/" 18 | schedule: 19 | interval: "daily" 20 | 21 | # Maintain dependencies for GitHub Actions 22 | - package-ecosystem: "github-actions" 23 | directory: "/" 24 | schedule: 25 | interval: "daily" 26 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "dependabot/**" 7 | - "pre-commit-ci-update-config" 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | concurrency: 12 | group: benchmark-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | benchmark: 17 | name: Benchmark ${{ matrix.archs }} ${{ matrix.build }} on ${{ matrix.os }} 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | os: [ubuntu-22.04] 22 | archs: ["x86_64"] 23 | build: ["manylinux"] 24 | 25 | steps: 26 | - name: Set git to use LF 27 | run: | 28 | git config --global core.autocrlf false 29 | git config --global core.eol lf 30 | 31 | - uses: actions/checkout@v4 32 | with: 33 | submodules: recursive 34 | 35 | - name: Build wheel 36 | uses: pypa/cibuildwheel@v3.0.0rc1 37 | env: 38 | CIBW_ARCHS: "${{ matrix.archs }}" 39 | CIBW_BUILD: "cp312-${{ matrix.build }}*" 40 | 41 | - uses: wntrblm/nox@2025.05.01 42 | name: Install Nox 43 | with: 44 | python-versions: "3.12" 45 | 46 | - name: Install dependencies 47 | run: nox -s benchmark --install-only -- --wheel wheelhouse/*.whl 48 | 49 | - name: Run benchmark 50 | uses: CodSpeedHQ/action@v3 51 | with: 52 | run: nox -s benchmark --reuse-existing-virtualenvs --no-install -- -v 53 | -------------------------------------------------------------------------------- /.github/workflows/build-upload.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | branches-ignore: 8 | - "dependabot/**" 9 | - "pre-commit-ci-update-config" 10 | pull_request: 11 | 12 | concurrency: 13 | group: build-upload-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | lint: 18 | name: Lint 19 | runs-on: ubuntu-22.04 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-python@v5 23 | with: 24 | python-version: "3.10" 25 | - uses: pre-commit/action@v3.0.1 26 | 27 | build_wheels: 28 | name: ${{ matrix.build || matrix.platform }} ${{ matrix.archs }} wheels 29 | needs: [lint] 30 | runs-on: "${{ matrix.runs-on }}${{ startsWith(matrix.archs, 'aarch64') && '-arm' || '' }}" 31 | strategy: 32 | matrix: 33 | platform: ["linux"] 34 | archs: ["x86_64, i686", "aarch64, armv7l", "ppc64le", "s390x"] 35 | build: ["manylinux", "musllinux"] 36 | runs-on: [ubuntu-24.04] 37 | include: 38 | - platform: "linux" 39 | archs: "riscv64" 40 | build: "manylinux" 41 | runs-on: ubuntu-24.04 42 | - platform: "windows" 43 | archs: "AMD64" 44 | runs-on: windows-2022 45 | - platform: "windows" 46 | archs: "x86" 47 | runs-on: windows-2022 48 | - platform: "windows" 49 | archs: "ARM64" 50 | runs-on: windows-11-arm 51 | - platform: "macos" 52 | archs: "x86_64" 53 | runs-on: macos-13 54 | - platform: "macos" 55 | archs: "arm64" 56 | runs-on: macos-14 57 | - platform: "ios" 58 | archs: "x86_64_iphonesimulator" 59 | runs-on: macos-13 60 | - platform: "ios" 61 | archs: "arm64_iphonesimulator,arm64_iphoneos" 62 | runs-on: macos-14 63 | - platform: "pyodide" 64 | archs: "wasm32" 65 | runs-on: ubuntu-24.04 66 | 67 | steps: 68 | - name: Set git to use LF 69 | run: | 70 | git config --global core.autocrlf false 71 | git config --global core.eol lf 72 | 73 | - uses: actions/checkout@v4 74 | with: 75 | submodules: recursive 76 | 77 | - name: Set up QEMU 78 | uses: docker/setup-qemu-action@v3.6.0 79 | if: runner.os == 'Linux' && runner.arch == 'X64' 80 | 81 | - name: Install cmake 82 | if: runner.os == 'macOS' 83 | run: | 84 | # workaround https://gitlab.kitware.com/cmake/cmake/-/issues/26570 85 | pipx install -f cmake 86 | which cmake 87 | cmake --version 88 | 89 | # see https://cibuildwheel.pypa.io/en/stable/faq/#macos-building-cpython-38-wheels-on-arm64 90 | - name: "Install python 3.8 universal2 on macOS arm64" 91 | if: runner.os == 'macOS' && runner.arch == 'ARM64' 92 | uses: actions/setup-python@v5 93 | env: 94 | PIP_DISABLE_PIP_VERSION_CHECK: 1 95 | with: 96 | python-version: 3.8 97 | 98 | - name: Build wheels 99 | uses: pypa/cibuildwheel@v3.0.0rc1 100 | env: 101 | CIBW_ARCHS: "${{ matrix.archs }}" 102 | CIBW_BUILD: "${{ matrix.build && '*-' || ''}}${{ matrix.build }}*" 103 | CIBW_ENABLE: "${{ startsWith(github.ref, 'refs/tags/v') && '' || 'cpython-prerelease'}}" 104 | CIBW_PLATFORM: "${{ matrix.platform }}" 105 | 106 | - uses: actions/upload-artifact@v4 107 | with: 108 | name: "${{ (matrix.archs != 'riscv64' && matrix.platform != 'pyodide') && 'pypi' || 'cibw' }}-wheels ${{ matrix.build || matrix.platform }} ${{ matrix.archs }}" 109 | path: ./wheelhouse/*.whl 110 | 111 | build_sdist: 112 | name: source distribution 113 | needs: [lint] 114 | runs-on: ubuntu-22.04 115 | env: 116 | CIBUILDWHEEL: 1 # make C extension mandatory 117 | steps: 118 | - uses: actions/checkout@v4 119 | with: 120 | submodules: recursive 121 | 122 | - name: Build sdist 123 | run: pipx run build --sdist 124 | 125 | - uses: actions/setup-python@v5 126 | name: Install Python 127 | with: 128 | python-version: "3.11" 129 | 130 | - name: Install 131 | run: python -m pip install ${{ github.workspace }}/dist/pybase64*.tar.gz 132 | working-directory: "${{ runner.temp }}" 133 | 134 | - name: Test 135 | run: | 136 | python -m pip install -r requirements-test.txt 137 | pytest 138 | 139 | - uses: actions/upload-artifact@v4 140 | with: 141 | name: pypi-sdist 142 | path: dist/*.tar.gz 143 | 144 | check_dist: 145 | name: Check dist 146 | needs: [build_wheels, build_sdist] 147 | runs-on: ubuntu-24.04 148 | steps: 149 | - uses: actions/download-artifact@v4 150 | with: 151 | # unpacks all PyPI artifacts into dist/ 152 | pattern: pypi-* 153 | path: dist 154 | merge-multiple: true 155 | - run: | 156 | set -x 157 | WHEEL_COUNT=$(find dist -name '*.whl' | wc -l) 158 | EXPECTED_WHEEL_COUNT=${{ startsWith(github.ref, 'refs/tags/v') && '150' || '184' }} 159 | test ${WHEEL_COUNT} -eq ${EXPECTED_WHEEL_COUNT} 160 | pipx run twine check --strict dist/* 161 | - uses: actions/download-artifact@v4 162 | with: 163 | # unpacks all non PyPI artifacts into nodist/ 164 | pattern: cibw-* 165 | path: nodist 166 | merge-multiple: true 167 | - run: | 168 | set -x 169 | WHEEL_COUNT=$(find nodist -name '*.whl' | wc -l) 170 | EXPECTED_WHEEL_COUNT=${{ startsWith(github.ref, 'refs/tags/v') && '8' || '10' }} 171 | test ${WHEEL_COUNT} -eq ${EXPECTED_WHEEL_COUNT} 172 | pipx run twine check --strict nodist/* 173 | 174 | upload_test_pypi: 175 | name: Upload to Test PyPI 176 | needs: [check_dist] 177 | runs-on: ubuntu-22.04 178 | if: github.event_name == 'push' && github.repository == 'mayeut/pybase64' 179 | environment: 180 | name: test-pypi 181 | url: https://test.pypi.org/p/pybase64 182 | permissions: 183 | id-token: write 184 | steps: 185 | - uses: actions/download-artifact@v4 186 | with: 187 | # unpacks all PyPI artifacts into dist/ 188 | pattern: pypi-* 189 | path: dist 190 | merge-multiple: true 191 | - name: Upload to Test PyPI 192 | uses: pypa/gh-action-pypi-publish@v1.12.4 193 | with: 194 | skip-existing: true 195 | repository-url: https://test.pypi.org/legacy/ 196 | 197 | upload_pypi: 198 | name: Upload to PyPI 199 | needs: [upload_test_pypi] 200 | runs-on: ubuntu-22.04 201 | if: github.event_name == 'push' && github.repository == 'mayeut/pybase64' && startsWith(github.ref, 'refs/tags/v') 202 | environment: 203 | name: pypi 204 | url: https://pypi.org/p/pybase64 205 | permissions: 206 | id-token: write 207 | steps: 208 | - uses: actions/download-artifact@v4 209 | with: 210 | # unpacks all PyPI artifacts into dist/ 211 | pattern: pypi-* 212 | path: dist 213 | merge-multiple: true 214 | - name: Upload to PyPI 215 | uses: pypa/gh-action-pypi-publish@v1.12.4 216 | with: 217 | skip-existing: true 218 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "dependabot/**" 7 | - "pre-commit-ci-update-config" 8 | pull_request: 9 | 10 | concurrency: 11 | group: coverage-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | build_sdist: 16 | name: Coverage 17 | runs-on: ubuntu-22.04 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | submodules: recursive 22 | 23 | - uses: wntrblm/nox@2025.05.01 24 | with: 25 | python-versions: "3.13, pypy3.10" 26 | 27 | - name: Install Intel SDE 28 | run: | 29 | curl -fsSLo ${HOME}/sde.tar.xz https://downloadmirror.intel.com/813591/sde-external-9.33.0-2024-01-07-lin.tar.xz 30 | mkdir ${HOME}/sde 31 | tar -C ${HOME}/sde --strip-components 1 -xf ${HOME}/sde.tar.xz 32 | echo "PATH=${HOME}/sde:${PATH}" >> $GITHUB_ENV 33 | 34 | - name: Run coverage tests 35 | run: nox -s coverage -- --with-sde 36 | 37 | - name: Upload coverage to codecov 38 | uses: codecov/codecov-action@v5 39 | with: 40 | files: coverage-native.xml,coverage-python.xml 41 | token: ${{ secrets.CODECOV_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/msys2.yml: -------------------------------------------------------------------------------- 1 | name: MSYS2 Tests 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "dependabot/**" 7 | - "pre-commit-ci-update-config" 8 | pull_request: 9 | 10 | concurrency: 11 | group: msys2-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | test: 16 | name: Test with MSYS2 ${{ matrix.sys }} 17 | runs-on: windows-2022 18 | strategy: 19 | matrix: 20 | include: 21 | - { sys: msys, toolchain: "gcc" } 22 | - { sys: mingw64, env: mingw-w64-x86_64- } 23 | - { sys: ucrt64, env: mingw-w64-ucrt-x86_64- } 24 | - { sys: clang64, env: mingw-w64-clang-x86_64- } 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | submodules: recursive 29 | - name: 'Setup MSYS2' 30 | uses: msys2/setup-msys2@v2 31 | with: 32 | msystem: ${{matrix.sys}} 33 | update: true 34 | install: >- 35 | make 36 | ${{matrix.env}}cmake 37 | ${{matrix.env}}ninja 38 | ${{matrix.env}}${{matrix.toolchain || 'toolchain' }} 39 | ${{matrix.env}}python 40 | ${{matrix.env}}python-pip 41 | ${{matrix.env}}python-pytest 42 | ${{matrix.env}}python-setuptools 43 | ${{matrix.env}}python-typing_extensions 44 | - name: "Run tests" 45 | shell: msys2 {0} 46 | env: 47 | CIBUILDWHEEL: 1 48 | CC: cc 49 | run: | 50 | # virtual env seems to be broken 51 | # just allow breaking system packages, we're in CI 52 | python -m pip install --break-system-packages -v --no-build-isolation . 53 | python -m pytest 54 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "dependabot/**" 7 | - "pre-commit-ci-update-config" 8 | pull_request: 9 | 10 | concurrency: 11 | group: test-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | test: 16 | name: Test 17 | runs-on: ubuntu-22.04 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | submodules: recursive 22 | - uses: wntrblm/nox@2025.05.01 23 | with: 24 | python-versions: "3.8, 3.9, 3.10, 3.11, 3.12, 3.13, pypy3.9, pypy3.10" 25 | - name: "Run tests" 26 | run: nox --error-on-missing-interpreters -s test 27 | -------------------------------------------------------------------------------- /.github/workflows/valgrind.yml: -------------------------------------------------------------------------------- 1 | name: Valgrind 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "dependabot/**" 7 | - "pre-commit-ci-update-config" 8 | pull_request: 9 | 10 | concurrency: 11 | group: valgrind-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | build_sdist: 16 | name: Valgrind 17 | runs-on: ubuntu-22.04 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | submodules: recursive 22 | 23 | - name: Install dependencies 24 | run: | 25 | set -exuo pipefail 26 | sudo apt-get update 27 | sudo apt-get install -y --no-install-recommends valgrind python3.10-dbg python3.10-dev python3-distutils 28 | python3.10-dbg -mvenv ./venv-dbg 29 | ln -sf $(pwd)/venv-dbg/bin/python /usr/local/bin/python-pb64 30 | python-pb64 -m pip install --upgrade pip setuptools wheel 31 | python-pb64 -m pip install -r requirements-test.txt 32 | 33 | - name: Run valgrind tests 34 | run: | 35 | set -exuo pipefail 36 | CFLAGS="-O0" CIBUILDWHEEL=1 python-pb64 -m pip install -e . 37 | PYTHONMALLOC=malloc valgrind --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite --error-exitcode=2 $(which python-pb64) -m pytest 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Genertaed files 2 | src/pybase64/_license.py 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | .base64_build/ 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheelhouse/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage*.xml 50 | *,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # IPython Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv/ 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "base64"] 2 | path = base64 3 | url = https://github.com/aklomp/base64.git 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: check-builtin-literals 8 | - id: check-added-large-files 9 | - id: check-case-conflict 10 | - id: check-toml 11 | - id: check-yaml 12 | - id: debug-statements 13 | - id: end-of-file-fixer 14 | - id: forbid-new-submodules 15 | - id: trailing-whitespace 16 | 17 | - repo: https://github.com/astral-sh/ruff-pre-commit 18 | rev: v0.11.11 19 | hooks: 20 | - id: ruff 21 | args: ["--fix", "--show-fixes"] 22 | - id: ruff-format 23 | 24 | - repo: https://github.com/pre-commit/mirrors-mypy 25 | rev: v1.15.0 26 | hooks: 27 | - id: mypy 28 | name: mypy 3.8 29 | exclude: docs|conftest.py 30 | args: ["--python-version=3.8"] 31 | additional_dependencies: 32 | - nox 33 | - pytest 34 | - types-setuptools 35 | - id: mypy 36 | name: mypy 3.12 37 | exclude: docs|conftest.py 38 | args: ["--python-version=3.12"] 39 | additional_dependencies: 40 | - nox 41 | - pytest 42 | - types-setuptools 43 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # If using Sphinx, optionally build your docs in additional formats such as PDF 19 | formats: 20 | - epub 21 | - pdf 22 | 23 | # Optionally declare the Python requirements required to build your docs 24 | python: 25 | install: 26 | - requirements: requirements-doc.txt 27 | - method: pip 28 | path: . 29 | 30 | submodules: 31 | include: all 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2017-2022, Matthieu Darbois 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the license file 2 | include LICENSE 3 | 4 | # Include headers 5 | include src/pybase64/_pybase64_get_simd_flags.h 6 | 7 | # Include type stub for extension 8 | include src/pybase64/_pybase64.pyi 9 | 10 | # Include full base64 folder 11 | graft base64 12 | # but the git folder 13 | prune base64/.git 14 | # and hidden files 15 | global-exclude .* 16 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. SETUP VARIABLES 2 | .. |license-status| image:: https://img.shields.io/badge/license-BSD%202--Clause-blue.svg 3 | :target: https://github.com/mayeut/pybase64/blob/master/LICENSE 4 | .. |pypi-status| image:: https://img.shields.io/pypi/v/pybase64.svg 5 | :target: https://pypi.python.org/pypi/pybase64 6 | .. |python-versions| image:: https://img.shields.io/pypi/pyversions/pybase64.svg 7 | .. |rtd-status| image:: https://readthedocs.org/projects/pybase64/badge/?version=stable 8 | :target: http://pybase64.readthedocs.io/en/stable/?badge=stable 9 | :alt: Documentation Status 10 | .. |gha-status| image:: https://github.com/mayeut/pybase64/workflows/Build%20and%20upload%20to%20PyPI/badge.svg 11 | :target: https://github.com/mayeut/pybase64/actions?query=workflow%3A%22Build+and+upload+to+PyPI%22 12 | .. |codecov-status| image:: https://codecov.io/gh/mayeut/pybase64/branch/master/graph/badge.svg 13 | :target: https://codecov.io/gh/mayeut/pybase64/branch/master 14 | .. END OF SETUP 15 | 16 | Fast Base64 implementation 17 | ========================== 18 | 19 | |license-status| |pypi-status| |python-versions| |rtd-status| |gha-status| |codecov-status| 20 | 21 | This project is a wrapper on `libbase64 `_. 22 | 23 | It aims to provide a fast base64 implementation for base64 encoding/decoding. 24 | 25 | Installation 26 | ============ 27 | 28 | .. code:: 29 | 30 | pip install pybase64 31 | 32 | Usage 33 | ===== 34 | 35 | ``pybase64`` uses the same API as Python base64 "modern interface" (introduced in Python 2.4) for an easy integration. 36 | 37 | To get the fastest decoding, it is recommended to use the ``pybase64.b64decode`` and ``validate=True`` when possible. 38 | 39 | .. code:: python 40 | 41 | import pybase64 42 | 43 | print(pybase64.b64encode(b'>>>foo???', altchars='_:')) 44 | # b'Pj4_Zm9vPz8:' 45 | print(pybase64.b64decode(b'Pj4_Zm9vPz8:', altchars='_:', validate=True)) 46 | # b'>>>foo???' 47 | 48 | # Standard encoding helpers 49 | print(pybase64.standard_b64encode(b'>>>foo???')) 50 | # b'Pj4+Zm9vPz8/' 51 | print(pybase64.standard_b64decode(b'Pj4+Zm9vPz8/')) 52 | # b'>>>foo???' 53 | 54 | # URL safe encoding helpers 55 | print(pybase64.urlsafe_b64encode(b'>>>foo???')) 56 | # b'Pj4-Zm9vPz8_' 57 | print(pybase64.urlsafe_b64decode(b'Pj4-Zm9vPz8_')) 58 | # b'>>>foo???' 59 | 60 | .. begin cli 61 | 62 | A command-line tool is also provided. It has encode, decode and benchmark subcommands. 63 | 64 | .. code:: 65 | 66 | usage: pybase64 [-h] [-V] {benchmark,encode,decode} ... 67 | 68 | pybase64 command-line tool. 69 | 70 | positional arguments: 71 | {benchmark,encode,decode} 72 | tool help 73 | benchmark -h for usage 74 | encode -h for usage 75 | decode -h for usage 76 | 77 | optional arguments: 78 | -h, --help show this help message and exit 79 | -V, --version show program's version number and exit 80 | 81 | .. end cli 82 | 83 | Full documentation on `Read the Docs `_. 84 | 85 | Benchmark 86 | ========= 87 | 88 | .. begin benchmark 89 | 90 | Running Python 3.7.2, Apple LLVM version 10.0.0 (clang-1000.11.45.5), Mac OS X 10.14.2 on an Intel Core i7-4870HQ @ 2.50GHz 91 | 92 | .. code:: 93 | 94 | pybase64 0.5.0 (C extension active - AVX2) 95 | bench: altchars=None, validate=False 96 | pybase64._pybase64.encodebytes: 1734.776 MB/s (13,271,472 bytes -> 17,928,129 bytes) 97 | pybase64._pybase64.b64encode: 4039.539 MB/s (13,271,472 bytes -> 17,695,296 bytes) 98 | pybase64._pybase64.b64decode: 1854.423 MB/s (17,695,296 bytes -> 13,271,472 bytes) 99 | base64.encodebytes: 78.352 MB/s (13,271,472 bytes -> 17,928,129 bytes) 100 | base64.b64encode: 539.840 MB/s (13,271,472 bytes -> 17,695,296 bytes) 101 | base64.b64decode: 287.826 MB/s (17,695,296 bytes -> 13,271,472 bytes) 102 | bench: altchars=None, validate=True 103 | pybase64._pybase64.b64encode: 4156.607 MB/s (13,271,472 bytes -> 17,695,296 bytes) 104 | pybase64._pybase64.b64decode: 4107.997 MB/s (17,695,296 bytes -> 13,271,472 bytes) 105 | base64.b64encode: 559.342 MB/s (13,271,472 bytes -> 17,695,296 bytes) 106 | base64.b64decode: 143.674 MB/s (17,695,296 bytes -> 13,271,472 bytes) 107 | bench: altchars=b'-_', validate=False 108 | pybase64._pybase64.b64encode: 2786.776 MB/s (13,271,472 bytes -> 17,695,296 bytes) 109 | pybase64._pybase64.b64decode: 1124.136 MB/s (17,695,296 bytes -> 13,271,472 bytes) 110 | base64.b64encode: 322.427 MB/s (13,271,472 bytes -> 17,695,296 bytes) 111 | base64.b64decode: 205.195 MB/s (17,695,296 bytes -> 13,271,472 bytes) 112 | bench: altchars=b'-_', validate=True 113 | pybase64._pybase64.b64encode: 2806.271 MB/s (13,271,472 bytes -> 17,695,296 bytes) 114 | pybase64._pybase64.b64decode: 2740.456 MB/s (17,695,296 bytes -> 13,271,472 bytes) 115 | base64.b64encode: 314.709 MB/s (13,271,472 bytes -> 17,695,296 bytes) 116 | base64.b64decode: 121.803 MB/s (17,695,296 bytes -> 13,271,472 bytes) 117 | 118 | .. end benchmark 119 | 120 | .. begin changelog 121 | 122 | Changelog 123 | ========= 124 | 1.4.1 125 | ----- 126 | - Publish PyPy 3.11 wheels 127 | - Publish armv7l wheels 128 | 129 | 1.4.0 130 | ----- 131 | - Publish python 3.13 wheels 132 | - Add support for free-threaded builds 133 | - Add MSYS2 support for C-extension 134 | - Better logging on base64 build failure when C-extension build is optional 135 | - Drop python 3.6 & 3.7 support 136 | 137 | 1.3.2 138 | ----- 139 | - Update base64 library 140 | - PyPy: fix wrong outcome with non C-contiguous buffer 141 | 142 | 1.3.1 143 | ----- 144 | - Add missing py.typed marker 145 | 146 | 1.3.0 147 | ----- 148 | - Update base64 library 149 | - Add AVX512-VBMI implementation 150 | - Rework extension build to remove adherence on distutils 151 | - Publish python 3.12 wheels 152 | - Documentation now uses furo theme 153 | 154 | 1.2.3 155 | ----- 156 | - Update base64 library 157 | - Publish python 3.11 wheels 158 | 159 | 1.2.2 160 | ----- 161 | - Update base64 library 162 | - Fix C extension build on musl distros 163 | - Publish musllinux wheels 164 | 165 | 1.2.1 166 | ----- 167 | - Publish PyPy 3.8 (pypy38_pp73) wheels 168 | 169 | 1.2.0 170 | ----- 171 | - Release the GIL 172 | - Publish CPython 3.10 wheels 173 | - Drop python 3.5 support 174 | 175 | 1.1.4 176 | ----- 177 | - Add macOS arm64 wheel 178 | 179 | 1.1.3 180 | ----- 181 | - GitHub Actions: fix build on tag 182 | 183 | 1.1.2 184 | ----- 185 | - Add PyPy wheels 186 | - Add aarch64, ppc64le & s390x manylinux wheels 187 | 188 | 1.1.1 189 | ----- 190 | - Move CI from TravisCI/AppVeyor to GitHub Actions 191 | - Fix publication of Linux/macOS wheels 192 | 193 | 1.1.0 194 | ----- 195 | - Add b64encode_as_string, same as b64encode but returns a str object instead of a bytes object 196 | - Add b64decode_as_bytearray, same as b64decode but returns a bytarray object instead of a bytes object 197 | - Speed-Up decoding from UCS1 strings 198 | 199 | 1.0.2 200 | ----- 201 | - Update base64 library 202 | - Publish python 3.9 wheels 203 | 204 | 1.0.1 205 | ----- 206 | - Publish python 3.8 wheels 207 | 208 | 1.0.0 209 | ----- 210 | - Drop python 3.4 support 211 | - Drop python 2.7 support 212 | 213 | 0.5.0 214 | ----- 215 | - Publish python 3.7 wheels 216 | - Drop python 3.3 support 217 | 218 | 0.4.0 219 | ----- 220 | - Speed-up decoding when validate==False 221 | 222 | 0.3.1 223 | ----- 224 | - Fix deployment issues 225 | 226 | 0.3.0 227 | ----- 228 | - Add encodebytes function 229 | 230 | 0.2.1 231 | ----- 232 | - Fixed invalid results on Windows 233 | 234 | 0.2.0 235 | ----- 236 | - Added documentation 237 | - Added subcommands to the main script: 238 | 239 | * help 240 | * version 241 | * encode 242 | * decode 243 | * benchmark 244 | 245 | 0.1.2 246 | ----- 247 | - Updated base64 native library 248 | 249 | 0.1.1 250 | ----- 251 | - Fixed deployment issues 252 | 253 | 0.1.0 254 | ----- 255 | - First public release 256 | 257 | .. end changelog 258 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | def pytest_addoption(parser) -> None: 5 | parser.addoption("--sde-cpu", action="store", default=None, help="run sde tests") 6 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = pybase64 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | Main API Reference 5 | ------------------ 6 | 7 | .. autofunction:: pybase64.b64encode 8 | 9 | .. autofunction:: pybase64.b64encode_as_string 10 | 11 | .. autofunction:: pybase64.b64decode 12 | 13 | .. autofunction:: pybase64.b64decode_as_bytearray 14 | 15 | Helpers API Reference 16 | --------------------- 17 | 18 | .. autofunction:: pybase64.standard_b64encode 19 | 20 | .. autofunction:: pybase64.standard_b64decode 21 | 22 | .. autofunction:: pybase64.urlsafe_b64encode 23 | 24 | .. autofunction:: pybase64.urlsafe_b64decode 25 | 26 | Legacy API Reference 27 | -------------------- 28 | 29 | .. autofunction:: pybase64.encodebytes 30 | 31 | Information API Reference 32 | ------------------------- 33 | 34 | .. autofunction:: pybase64.get_version 35 | 36 | .. autofunction:: pybase64.get_license_text 37 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: .. begin changelog 3 | :end-before: .. end changelog 4 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # pybase64 documentation build configuration file, created by 2 | # sphinx-quickstart on Tue Aug 29 22:06:28 2017. 3 | # 4 | # This file is execfile()d with the current directory set to its 5 | # containing dir. 6 | # 7 | # Note that not all possible configuration values are present in this 8 | # autogenerated file. 9 | # 10 | # All configuration values have a default; values that are commented out 11 | # serve to show the default. 12 | 13 | from __future__ import annotations 14 | 15 | import datetime 16 | import os 17 | import runpy 18 | import sys 19 | 20 | here = os.path.abspath(os.path.dirname(__file__)) 21 | 22 | # If extensions (or modules to document with autodoc) are in another directory, 23 | # add these directories to sys.path here. If the directory is relative to the 24 | # documentation root, use os.path.abspath to make it absolute, like shown here. 25 | sys.path.insert(0, os.path.abspath(os.path.join(here, ".."))) 26 | 27 | # -- General configuration ------------------------------------------------ 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | # 31 | # needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | "sphinx.ext.autodoc", 38 | "sphinx.ext.coverage", 39 | "sphinx.ext.intersphinx", 40 | "sphinx.ext.viewcode", 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ["_templates"] 45 | 46 | # The suffix(es) of source filenames. 47 | # You can specify multiple suffix as a list of string: 48 | # 49 | # source_suffix = ['.rst', '.md'] 50 | source_suffix = ".rst" 51 | 52 | # The master toctree document. 53 | master_doc = "index" 54 | 55 | # General information about the project. 56 | project = "pybase64" 57 | author = "Matthieu Darbois" 58 | copyright = f"2017-{datetime.date.today().year}, Matthieu Darbois" 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # Get version 64 | _version = runpy.run_path(os.path.join(here, "..", "src", "pybase64", "_version.py"))["_version"] 65 | # The short X.Y version. 66 | version = _version 67 | # The full version, including alpha/beta/rc tags. 68 | release = _version 69 | 70 | # default highlight language to 'none', requires to be explicit 71 | highlight_language = "none" 72 | 73 | # The language for content autogenerated by Sphinx. Refer to documentation 74 | # for a list of supported languages. 75 | # 76 | # This is also used if you do content translation via gettext catalogs. 77 | # Usually you set "language" from the command line for these cases. 78 | language = "en" 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | # This patterns also effect to html_static_path and html_extra_path 83 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 84 | 85 | # The name of the Pygments (syntax highlighting) style to use. 86 | pygments_style = "sphinx" 87 | 88 | # If true, `todo` and `todoList` produce output, else they produce nothing. 89 | todo_include_todos = False 90 | 91 | 92 | # -- Options for HTML output ---------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. See the documentation for 95 | # a list of builtin themes. 96 | # 97 | html_theme = "furo" 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | # 103 | html_theme_options = {} 104 | 105 | # Add any paths that contain custom static files (such as style sheets) here, 106 | # relative to this directory. They are copied after the builtin static files, 107 | # so a file named "default.css" will overwrite the builtin "default.css". 108 | html_static_path = ["_static"] 109 | 110 | 111 | # -- Options for HTMLHelp output ------------------------------------------ 112 | 113 | # Output file base name for HTML help builder. 114 | htmlhelp_basename = "pybase64doc" 115 | 116 | 117 | # -- Options for LaTeX output --------------------------------------------- 118 | 119 | latex_elements = { 120 | # The paper size ('letterpaper' or 'a4paper'). 121 | # 122 | # 'papersize': 'letterpaper', 123 | # The font size ('10pt', '11pt' or '12pt'). 124 | # 125 | # 'pointsize': '10pt', 126 | # Additional stuff for the LaTeX preamble. 127 | # 128 | # 'preamble': '', 129 | # Latex figure (float) alignment 130 | # 131 | # 'figure_align': 'htbp', 132 | } 133 | 134 | # Grouping the document tree into LaTeX files. List of tuples 135 | # (source start file, target name, title, 136 | # author, documentclass [howto, manual, or own class]). 137 | latex_documents = [ 138 | (master_doc, "pybase64.tex", "pybase64 Documentation", author, "manual"), 139 | ] 140 | 141 | 142 | # -- Options for manual page output --------------------------------------- 143 | 144 | # One entry per manual page. List of tuples 145 | # (source start file, name, description, authors, manual section). 146 | man_pages = [(master_doc, "pybase64", "pybase64 Documentation", [author], 1)] 147 | 148 | 149 | # -- Options for Texinfo output ------------------------------------------- 150 | 151 | # Grouping the document tree into Texinfo files. List of tuples 152 | # (source start file, target name, title, author, 153 | # dir menu entry, description, category) 154 | texinfo_documents = [ 155 | ( 156 | master_doc, 157 | "pybase64", 158 | "pybase64 Documentation", 159 | author, 160 | "pybase64", 161 | "Fast Base64 implementation for Python.", 162 | "Miscellaneous", 163 | ), 164 | ] 165 | 166 | 167 | # Example configuration for intersphinx: refer to the Python standard library. 168 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 169 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | pybase64 2 | ======== 3 | 4 | Fast Base64 implementation for Python. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: Contents: 9 | 10 | intro 11 | api 12 | changelog 13 | license 14 | pybase64 on Github 15 | -------------------------------------------------------------------------------- /docs/intro.rst: -------------------------------------------------------------------------------- 1 | Getting started 2 | =============== 3 | 4 | ``pybase64`` is a wrapper on `libbase64 `_. 5 | 6 | It aims to provide a fast base64 implementation for base64 encoding/decoding. 7 | 8 | Installation 9 | ------------ 10 | 11 | .. code-block:: bash 12 | 13 | pip install pybase64 14 | 15 | Usage 16 | ----- 17 | 18 | ``pybase64`` uses the same API as Python :mod:`base64` "modern interface" (introduced in Python 2.4) for an easy integration. 19 | 20 | To get the fastest decoding, it is recommended to use the :func:`~pybase64.b64decode` and `validate=True` when possible. 21 | 22 | .. code-block:: python 23 | 24 | import pybase64 25 | 26 | print(pybase64.b64encode(b'>>>foo???', altchars='_:')) 27 | # b'Pj4_Zm9vPz8:' 28 | print(pybase64.b64decode(b'Pj4_Zm9vPz8:', altchars='_:', validate=True)) 29 | # b'>>>foo???' 30 | 31 | # Standard encoding helpers 32 | print(pybase64.standard_b64encode(b'>>>foo???')) 33 | # b'Pj4+Zm9vPz8/' 34 | print(pybase64.standard_b64decode(b'Pj4+Zm9vPz8/')) 35 | # b'>>>foo???' 36 | 37 | # URL safe encoding helpers 38 | print(pybase64.urlsafe_b64encode(b'>>>foo???')) 39 | # b'Pj4-Zm9vPz8_' 40 | print(pybase64.urlsafe_b64decode(b'Pj4-Zm9vPz8_')) 41 | # b'>>>foo???' 42 | 43 | 44 | Check :doc:`api` for more details. 45 | 46 | .. include:: ../README.rst 47 | :start-after: .. begin cli 48 | :end-before: .. end cli 49 | 50 | Benchmark 51 | --------- 52 | .. include:: ../README.rst 53 | :start-after: .. begin benchmark 54 | :end-before: .. end benchmark 55 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | pybase64 5 | -------- 6 | 7 | .. literalinclude:: ../LICENSE 8 | :language: none 9 | 10 | libbase64 11 | --------- 12 | 13 | .. literalinclude:: ../base64/LICENSE 14 | :language: none 15 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import sys 5 | from pathlib import Path 6 | 7 | import nox 8 | 9 | HERE = Path(__file__).resolve().parent 10 | 11 | nox.options.sessions = ["lint", "test"] 12 | 13 | ALL_CPYTHON = [f"3.{minor}" for minor in range(8, 13 + 1)] 14 | ALL_PYPY = [f"pypy3.{minor}" for minor in range(9, 10 + 1)] 15 | ALL_PYTHON = ALL_CPYTHON + ALL_PYPY 16 | 17 | 18 | @nox.session 19 | def lint(session: nox.Session) -> None: 20 | """Run linters on the codebase.""" 21 | session.install("pre-commit") 22 | session.run("pre-commit", "run", "-a") 23 | 24 | 25 | def update_env_macos(session: nox.Session, env: dict[str, str]) -> None: 26 | if sys.platform.startswith("darwin"): 27 | # we don't support universal builds 28 | machine = session.run( # type: ignore[union-attr] 29 | "python", "-sSEc", "import platform; print(platform.machine())", silent=True 30 | ).strip() 31 | env["ARCHFLAGS"] = f"-arch {machine}" 32 | env["_PYTHON_HOST_PLATFORM"] = f"macosx-11.0-{machine}" 33 | 34 | 35 | def remove_extension(session: nox.Session, in_place: bool = False) -> None: 36 | if in_place: 37 | where = HERE / "src" / "pybase64" 38 | else: 39 | command = "import sysconfig; print(sysconfig.get_path('platlib'))" 40 | platlib = session.run("python", "-c", command, silent=True).strip() # type: ignore[union-attr] 41 | where = Path(platlib) / "pybase64" 42 | assert where.exists() 43 | 44 | removed = False 45 | for ext in ["*.so", "*.pyd"]: 46 | for file in where.glob(ext): 47 | session.log(f"removing '{file.relative_to(HERE)}'") 48 | file.unlink() 49 | removed = True 50 | if not in_place: 51 | assert removed 52 | 53 | 54 | @nox.session(python="3.12") 55 | def develop(session: nox.Session) -> None: 56 | """create venv for dev.""" 57 | session.install("nox", "setuptools", "-r", "requirements-test.txt") 58 | # make extension mandatory by exporting CIBUILDWHEEL=1 59 | env = {"CIBUILDWHEEL": "1"} 60 | update_env_macos(session, env) 61 | session.install("-e", ".", env=env) 62 | 63 | 64 | @nox.session(python=ALL_PYTHON) 65 | def test(session: nox.Session) -> None: 66 | """Run tests.""" 67 | session.install("-r", "requirements-test.txt") 68 | # make extension mandatory by exporting CIBUILDWHEEL=1 69 | env = {"CIBUILDWHEEL": "1"} 70 | update_env_macos(session, env) 71 | session.install(".", env=env) 72 | session.run("pytest", *session.posargs, env=env) 73 | # run without extension as well 74 | env.pop("CIBUILDWHEEL") 75 | remove_extension(session) 76 | session.run("pytest", *session.posargs, env=env) 77 | 78 | 79 | @nox.session(python=["3.13", "pypy3.10"]) 80 | def _coverage(session: nox.Session) -> None: 81 | """internal coverage run. Do not run manually""" 82 | with_sde = "--with-sde" in session.posargs 83 | clean = "--clean" in session.posargs 84 | report = "--report" in session.posargs 85 | coverage_args = ( 86 | "--cov=pybase64", 87 | "--cov=tests", 88 | "--cov-append", 89 | "--cov-report=", 90 | ) 91 | pytest_command = ("pytest", *coverage_args) 92 | 93 | session.install("-r", "requirements-test.txt", "-r", "requirements-coverage.txt") 94 | remove_extension(session, in_place=True) 95 | # make extension mandatory by exporting CIBUILDWHEEL=1 96 | env = { 97 | "CIBUILDWHEEL": "1", 98 | "CFLAGS": "-O0 -coverage", 99 | "LDFLAGS": "-coverage", 100 | "COVERAGE_PROCESS_START": "1", 101 | } 102 | update_env_macos(session, env) 103 | session.install("-e", ".", env=env) 104 | if clean: 105 | session.run("coverage", "erase", env=env) 106 | session.run(*pytest_command, env=env) 107 | if with_sde: 108 | cpu = "spr" 109 | sde = ("sde", f"-{cpu}", "--") 110 | session.run(*sde, *pytest_command, f"--sde-cpu={cpu}", env=env, external=True) 111 | for cpu in ["p4p", "mrm", "pnr", "nhm", "snb", "hsw"]: 112 | sde = ("sde", f"-{cpu}", "--") 113 | pytest_addopt = (f"--sde-cpu={cpu}", "-k=test_flags") 114 | session.run(*sde, *pytest_command, *pytest_addopt, env=env, external=True) 115 | 116 | # run without extension as well 117 | env.pop("CIBUILDWHEEL") 118 | remove_extension(session, in_place=True) 119 | session.run(*pytest_command, env=env) 120 | 121 | # reports 122 | if report: 123 | threshold = 100.0 if "CI" in os.environ else 99.8 124 | session.run("coverage", "report", "--show-missing", f"--fail-under={threshold}") 125 | session.run("coverage", "xml", "-ocoverage-python.xml") 126 | gcovr_config = ("-r=.", "-e=base64", "-e=.base64_build") 127 | session.run( 128 | "gcovr", 129 | *gcovr_config, 130 | "--fail-under-line=90", 131 | "--txt", 132 | "-s", 133 | "--xml=coverage-native.xml", 134 | ) 135 | 136 | 137 | @nox.session(venv_backend="none") 138 | def coverage(session: nox.Session) -> None: 139 | """Coverage tests.""" 140 | posargs_ = set(session.posargs) 141 | assert len(posargs_ & {"--clean", "--report"}) == 0 142 | assert len(posargs_ - {"--with-sde"}) == 0 143 | posargs = [*session.posargs, "--report"] 144 | session.notify("_coverage-pypy3.10", ["--clean"]) 145 | session.notify("_coverage-3.13", posargs) 146 | 147 | 148 | @nox.session(python="3.12") 149 | def benchmark(session: nox.Session) -> None: 150 | """Benchmark tests.""" 151 | project_install: tuple[str, ...] = ("-e", ".") 152 | posargs = session.posargs.copy() 153 | if "--wheel" in posargs: 154 | index = posargs.index("--wheel") 155 | posargs.pop(index) 156 | project_install = (posargs.pop(index),) 157 | env = {"CIBUILDWHEEL": "1"} 158 | update_env_macos(session, env) 159 | session.install("-r", "requirements-benchmark.txt", *project_install, env=env) 160 | session.run("pytest", "--codspeed", *posargs) 161 | 162 | 163 | @nox.session(python="3.11") 164 | def docs(session: nox.Session) -> None: 165 | """ 166 | Build the docs. 167 | """ 168 | session.install("-r", "requirements-doc.txt", ".") 169 | session.run("pip", "list") 170 | session.chdir("docs") 171 | session.run( 172 | "python", 173 | "-m", 174 | "sphinx", 175 | "-T", 176 | "-E", 177 | "-b", 178 | "html", 179 | "-d", 180 | "_build/doctrees", 181 | "-D", 182 | "language=en", 183 | ".", 184 | "build", 185 | ) 186 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.cibuildwheel] 6 | build-verbosity = 1 7 | enable = ["cpython-experimental-riscv64", "cpython-freethreading", "graalpy", "pypy", "pypy-eol"] 8 | test-skip = ["gp311_242-win_amd64"] # https://github.com/oracle/graalpython/issues/490 9 | test-requires = "-r requirements-test.txt" 10 | test-sources = ["conftest.py", "pyproject.toml", "tests"] 11 | test-command = "python -m pytest" 12 | xbuild-tools = ["cmake"] 13 | manylinux-i686-image = "manylinux2014" 14 | manylinux-pypy_i686-image = "manylinux2014" 15 | manylinux-x86_64-image = "manylinux_2_28" 16 | manylinux-pypy_x86_64-image = "manylinux_2_28" 17 | manylinux-aarch64-image = "manylinux_2_28" 18 | manylinux-pypy_aarch64-image = "manylinux_2_28" 19 | manylinux-ppc64le-image = "manylinux_2_28" 20 | manylinux-s390x-image = "manylinux_2_28" 21 | manylinux-riscv64-image = "ghcr.io/mayeut/manylinux_2_35:2025.05.19-1" 22 | 23 | [[tool.cibuildwheel.overrides]] 24 | select = "*-manylinux*" 25 | environment = { AUDITWHEEL_PLAT="manylinux2014_${AUDITWHEEL_ARCH}" } 26 | 27 | [[tool.cibuildwheel.overrides]] 28 | select = "*-manylinux_riscv64" 29 | environment = { AUDITWHEEL_PLAT="manylinux_2_31_${AUDITWHEEL_ARCH}" } 30 | 31 | [tool.coverage.run] 32 | branch = true 33 | omit = [ 34 | "tests/conftest.py", 35 | "tests/test_benchmark.py", 36 | ] 37 | 38 | [tool.coverage.report] 39 | exclude_lines = ["pragma: no cover", "class .*\\(Protocol\\):", "if TYPE_CHECKING:"] 40 | 41 | [tool.mypy] 42 | python_version = "3.8" 43 | files = [ 44 | "src/**/*.py", 45 | "test/**/*.py", 46 | "noxfile.py", 47 | "setup.py", 48 | ] 49 | warn_unused_configs = true 50 | show_error_codes = true 51 | 52 | warn_redundant_casts = true 53 | no_implicit_reexport = true 54 | strict_equality = true 55 | warn_unused_ignores = true 56 | check_untyped_defs = true 57 | ignore_missing_imports = false 58 | 59 | disallow_subclassing_any = true 60 | disallow_any_generics = true 61 | disallow_untyped_defs = true 62 | disallow_untyped_calls = true 63 | disallow_incomplete_defs = true 64 | disallow_untyped_decorators = true 65 | disallow_any_explicit = true 66 | warn_return_any = true 67 | 68 | no_implicit_optional = true 69 | enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] 70 | warn_unreachable = true 71 | 72 | [[tool.mypy.overrides]] 73 | module = ["pybase64.__main__", "tests.test_pybase64", "tests.utils"] 74 | disallow_any_explicit = false 75 | 76 | [tool.pytest.ini_options] 77 | minversion = "7.0" 78 | addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config", "-p", "no:legacypath"] 79 | markers = ["benchmark"] 80 | 81 | [tool.ruff] 82 | target-version = "py38" 83 | line-length = 100 84 | 85 | [tool.ruff.lint] 86 | extend-select = [ 87 | "B", # flake8-bugbear 88 | "I", # isort 89 | "ARG", # flake8-unused-arguments 90 | "C4", # flake8-comprehensions 91 | "EM", # flake8-errmsg 92 | "ICN", # flake8-import-conventions 93 | "ISC", # flake8-implicit-str-concat 94 | "G", # flake8-logging-format 95 | "PGH", # pygrep-hooks 96 | "PIE", # flake8-pie 97 | "PL", # pylint 98 | "PT", # flake8-pytest-style 99 | "RET", # flake8-return 100 | "RUF", # Ruff-specific 101 | "SIM", # flake8-simplify 102 | "TID251", # flake8-tidy-imports.banned-api 103 | "UP", # pyupgrade 104 | "YTT", # flake8-2020 105 | "EXE", # flake8-executable 106 | "PYI", # flake8-pyi 107 | ] 108 | ignore = [ 109 | "PLR", # Design related pylint codes 110 | ] 111 | typing-modules = ["pybase64._typing"] 112 | 113 | [tool.ruff.lint.flake8-tidy-imports.banned-api] 114 | "typing.Callable".msg = "Use collections.abc.Callable instead." 115 | "typing.Iterator".msg = "Use collections.abc.Iterator instead." 116 | "typing.Sequence".msg = "Use collections.abc.Sequence instead." 117 | -------------------------------------------------------------------------------- /requirements-benchmark.txt: -------------------------------------------------------------------------------- 1 | -r requirements-test.txt 2 | pytest-codspeed==3.2.0 3 | -------------------------------------------------------------------------------- /requirements-coverage.txt: -------------------------------------------------------------------------------- 1 | -r requirements-test.txt 2 | gcovr==8.3 3 | pytest-cov==6.1.1 4 | coverage==7.8.2 5 | -------------------------------------------------------------------------------- /requirements-doc.txt: -------------------------------------------------------------------------------- 1 | sphinx==8.2.3 2 | furo==2024.8.6 3 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest==8.3.5 2 | typing_extensions>=4.6.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | import platform as platform_module 6 | import shutil 7 | import subprocess 8 | import sys 9 | import sysconfig 10 | from contextlib import contextmanager 11 | from pathlib import Path 12 | from typing import Generator, cast 13 | 14 | from setuptools import Extension, find_packages, setup 15 | from setuptools.command.build_ext import build_ext 16 | 17 | HERE = Path(__file__).resolve().parent 18 | OPTIONAL_EXTENSION = os.environ.get("CIBUILDWHEEL", "0") != "1" 19 | IS_64BIT = sys.maxsize > 2**32 20 | IS_WINDOWS = sys.platform.startswith("win32") 21 | IS_MACOS = sys.platform.startswith("darwin") 22 | IS_IOS = sys.platform.startswith("ios") 23 | 24 | log = logging.getLogger("pybase64-setup") 25 | 26 | # Get version 27 | version_dict: dict[str, object] = {} 28 | exec(HERE.joinpath("src", "pybase64", "_version.py").read_text(), {}, version_dict) 29 | version = cast(str, version_dict["_version"]) 30 | 31 | # Get the long description from the README file 32 | long_description = HERE.joinpath("README.rst").read_text() 33 | 34 | # Generate license text 35 | with HERE.joinpath("src", "pybase64", "_license.py").open("w") as f: 36 | f.write('_license = """') 37 | f.write("pybase64\n") 38 | f.write("=" * 79 + "\n") 39 | f.write(HERE.joinpath("LICENSE").read_text()) 40 | f.write("=" * 79 + "\n\n") 41 | f.write("libbase64\n") 42 | f.write("=" * 79 + "\n") 43 | f.write(HERE.joinpath("base64", "LICENSE").read_text()) 44 | f.write("=" * 74) 45 | f.write('""" \\\n') 46 | f.write(' + "====="\n') 47 | 48 | pybase64_ext = Extension( 49 | "pybase64._pybase64", 50 | [ 51 | "src/pybase64/_pybase64.c", 52 | "src/pybase64/_pybase64_get_simd_flags.c", 53 | ], 54 | include_dirs=["base64/include/", "base64/lib/", ".base64_build"], 55 | library_dirs=[".base64_build"], 56 | libraries=["base64"], 57 | define_macros=[("BASE64_STATIC_DEFINE", "1")], 58 | optional=OPTIONAL_EXTENSION, 59 | ) 60 | 61 | 62 | def get_cmake_extra_config(plat_name: str | None, build_type: str) -> tuple[bool, list[str]]: 63 | log.info("getting cmake extra config") 64 | extra_config = [] 65 | machine = platform_module.machine().lower() 66 | platform = sysconfig.get_platform() 67 | archflags = os.environ.get("ARCHFLAGS", None) 68 | 69 | log.info(" machine: %s", machine) 70 | log.info(" platform: %s", platform) 71 | log.info(" plat_name: %s", plat_name) 72 | log.info(" ARCHFLAGS: %s", archflags) 73 | log.info(" CC: %s", os.environ.get("CC", None)) 74 | log.info(" CFLAGS: %s", os.environ.get("CFLAGS", None)) 75 | log.info(" LDFLAGS: %s", os.environ.get("LDFLAGS", None)) 76 | log.info(" sysconfig CC: %s", sysconfig.get_config_var("CC")) 77 | log.info(" sysconfig CCSHARED: %s", sysconfig.get_config_var("CCSHARED")) 78 | log.info(" sysconfig CFLAGS: %s", sysconfig.get_config_var("CFLAGS")) 79 | log.info(" sysconfig BASECFLAGS: %s", sysconfig.get_config_var("BASECFLAGS")) 80 | log.info(" sysconfig OPT: %s", sysconfig.get_config_var("OPT")) 81 | log.info(" sysconfig LDFLAGS: %s", sysconfig.get_config_var("LDFLAGS")) 82 | 83 | platform = plat_name or platform 84 | is_msvc = platform.startswith("win") 85 | 86 | if not is_msvc: 87 | extra_config.append(f"-DCMAKE_BUILD_TYPE={build_type}") 88 | 89 | if is_msvc: 90 | if not IS_WINDOWS: 91 | msg = f"Building {platform} is only supported on Windows" 92 | raise ValueError(msg) 93 | # setup cross-compile 94 | # assumes VS2019 or VS2022 will be used as the default generator 95 | if platform == "win-amd64" and machine != "amd64": 96 | extra_config.append("-A x64") 97 | if platform == "win32" and machine != "x86": 98 | extra_config.append("-A Win32") 99 | if platform == "win-arm64" and machine != "arm64": 100 | extra_config.append("-A ARM64") 101 | elif IS_MACOS or IS_IOS: 102 | known_archs = { 103 | "arm64", 104 | "arm64e", 105 | "armv7", 106 | "armv7s", 107 | "x86_64", 108 | "i386", 109 | "ppc", 110 | "ppc64", 111 | } 112 | if not platform.startswith(("macosx-", "ios-")): 113 | msg = f"Building {platform} is not supported on macOS" 114 | raise ValueError(msg) 115 | if IS_IOS: 116 | _, min_ver, platform_arch, sdk = platform.split("-") 117 | min_ver = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", min_ver) 118 | extra_config.append("-DCMAKE_SYSTEM_NAME=iOS") 119 | extra_config.append(f"-DCMAKE_OSX_SYSROOT={sdk}") 120 | else: 121 | _, min_ver, platform_arch = platform.split("-") 122 | min_ver = os.getenv("MACOSX_DEPLOYMENT_TARGET", min_ver) 123 | extra_config.append(f"-DCMAKE_OSX_DEPLOYMENT_TARGET={min_ver}") 124 | if platform_arch.startswith(("universal", "fat")): 125 | msg = f"multiple arch `{platform_arch}` is not supported" 126 | raise ValueError(msg) 127 | configured_archs = {platform_arch} 128 | if archflags: 129 | flags = [arch.strip() for arch in archflags.strip().split() if arch.strip()] 130 | for i in range(len(flags) - 1): 131 | if flags[i] == "-arch": 132 | configured_archs.add(flags[i + 1]) 133 | if len(configured_archs) > 1: 134 | msg = f"multiple arch `{configured_archs}` is not supported" 135 | raise ValueError(msg) 136 | arch = configured_archs.pop() 137 | if arch in known_archs: 138 | extra_config.append(f"-DCMAKE_OSX_ARCHITECTURES={arch}") 139 | else: 140 | log.warning("`%s` is not a known value for CMAKE_OSX_ARCHITECTURES", arch) 141 | 142 | return is_msvc, extra_config 143 | 144 | 145 | def cmake(*args: str) -> None: 146 | args_string = " ".join(f"'{arg}'" for arg in args) 147 | log.info("running cmake %s", args_string) 148 | subprocess.run(["cmake", *args], check=True) 149 | 150 | 151 | @contextmanager 152 | def base64_build(plat_name: str | None) -> Generator[bool, None, None]: 153 | base64_built = False 154 | source_dir = HERE / "base64" 155 | build_dir = HERE / ".base64_build" 156 | build_type = "Release" 157 | config_options = [ 158 | "-S", 159 | str(source_dir), 160 | "-B", 161 | str(build_dir), 162 | "-DBASE64_BUILD_TESTS:BOOL=OFF", 163 | "-DBASE64_BUILD_CLI:BOOL=OFF", 164 | "-DCMAKE_POSITION_INDEPENDENT_CODE=ON", 165 | "-DBUILD_SHARED_LIBS:BOOL=OFF", 166 | ] 167 | if build_dir.exists(): 168 | shutil.rmtree(build_dir) 169 | try: 170 | try: 171 | cmake("--version") 172 | is_msvc, extra_config = get_cmake_extra_config(plat_name, build_type) 173 | config_options.extend(extra_config) 174 | cmake(*config_options) 175 | cmake("--build", str(build_dir), "--config", build_type, "--verbose") 176 | if is_msvc: 177 | shutil.copyfile(build_dir / build_type / "base64.lib", build_dir / "base64.lib") 178 | base64_built = True 179 | except Exception as e: 180 | log.error("error: %s", e) 181 | if not OPTIONAL_EXTENSION: 182 | raise 183 | yield base64_built 184 | finally: 185 | if build_dir.exists(): 186 | shutil.rmtree(build_dir, ignore_errors=True) 187 | 188 | 189 | class BuildExt(build_ext): 190 | def finalize_options(self) -> None: 191 | if "-coverage" in os.environ.get("CFLAGS", "").split(): 192 | plat_name = getattr(self, "plat_name", None) or sysconfig.get_platform() 193 | temp_name = f"coverage-{plat_name}-{sys.implementation.cache_tag}" 194 | coverage_build = HERE / "build" / temp_name 195 | if coverage_build.exists(): 196 | shutil.rmtree(coverage_build) 197 | self.build_temp = str(coverage_build) 198 | super().finalize_options() 199 | 200 | def run(self) -> None: 201 | plat_name = getattr(self, "plat_name", None) 202 | with base64_build(plat_name) as build_successful: 203 | if build_successful: 204 | super().run() 205 | else: 206 | assert OPTIONAL_EXTENSION 207 | log.warning("warning: skipping optional C-extension, base64 library build failed") 208 | 209 | 210 | setup( 211 | name="pybase64", 212 | cmdclass={"build_ext": BuildExt}, 213 | ext_modules=[pybase64_ext], 214 | # Versions should comply with PEP440. For a discussion on single-sourcing 215 | # the version across setup.py and the project code, see 216 | # https://packaging.python.org/en/latest/single_source_version.html 217 | version=version, 218 | description="Fast Base64 encoding/decoding", 219 | long_description=long_description, 220 | long_description_content_type="text/x-rst", 221 | # The project's main homepage. 222 | url="https://github.com/mayeut/pybase64", 223 | project_urls={ 224 | "Source": "https://github.com/mayeut/pybase64", 225 | "Tracker": "https://github.com/mayeut/pybase64/issues", 226 | "Documentation": "https://pybase64.readthedocs.io/en/stable", 227 | }, 228 | # Author details 229 | author="Matthieu Darbois", 230 | # author_email = 'mayeut@users.noreply.github.com', 231 | # Choose your license 232 | license="BSD-2-Clause", 233 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 234 | classifiers=[ 235 | # How mature is this project? Common values are 236 | # 3 - Alpha 237 | # 4 - Beta 238 | # 5 - Production/Stable 239 | "Development Status :: 5 - Production/Stable", 240 | # Indicate who your project is intended for 241 | "Intended Audience :: Developers", 242 | "Topic :: Software Development :: Libraries :: Python Modules", 243 | "Topic :: Utilities", 244 | # Pick your license as you wish (should match "license" above) 245 | "License :: OSI Approved :: BSD License", 246 | # Specify the Python versions you support here. In particular, ensure 247 | # that you indicate whether you support Python 2, Python 3 or both. 248 | "Programming Language :: C", 249 | "Programming Language :: Python :: 3", 250 | "Programming Language :: Python :: 3.8", 251 | "Programming Language :: Python :: 3.9", 252 | "Programming Language :: Python :: 3.10", 253 | "Programming Language :: Python :: 3.11", 254 | "Programming Language :: Python :: 3.12", 255 | "Programming Language :: Python :: 3.13", 256 | ], 257 | # Supported python versions 258 | python_requires=">=3.8", 259 | # What does your project relate to? 260 | keywords="base64", 261 | # You can just specify the packages manually here if your project is 262 | # simple. Or you can use find_packages(). 263 | packages=find_packages(where="src"), 264 | package_dir={"": "src"}, 265 | package_data={"pybase64": ["py.typed", "_pybase64.pyi"]}, 266 | # To provide executable scripts, use entry points in preference to the 267 | # "scripts" keyword. Entry points provide cross-platform support and allow 268 | # pip to create the appropriate form of executable for the target platform. 269 | entry_points={ 270 | "console_scripts": [ 271 | "pybase64=pybase64.__main__:main", 272 | ], 273 | }, 274 | ) 275 | -------------------------------------------------------------------------------- /src/pybase64/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from ._license import _license 6 | from ._version import _version 7 | 8 | if TYPE_CHECKING: 9 | from ._typing import Buffer 10 | 11 | try: 12 | from ._pybase64 import ( 13 | _get_simd_flags_compile, # noqa: F401 14 | _get_simd_flags_runtime, # noqa: F401 15 | _get_simd_name, 16 | _get_simd_path, 17 | _set_simd_path, # noqa: F401 18 | b64decode, 19 | b64decode_as_bytearray, 20 | b64encode, 21 | b64encode_as_string, 22 | encodebytes, 23 | ) 24 | except ImportError: 25 | from ._fallback import ( 26 | _get_simd_name, 27 | _get_simd_path, 28 | b64decode, 29 | b64decode_as_bytearray, 30 | b64encode, 31 | b64encode_as_string, 32 | encodebytes, 33 | ) 34 | 35 | 36 | __all__ = ( 37 | "b64decode", 38 | "b64decode_as_bytearray", 39 | "b64encode", 40 | "b64encode_as_string", 41 | "encodebytes", 42 | "standard_b64decode", 43 | "standard_b64encode", 44 | "urlsafe_b64decode", 45 | "urlsafe_b64encode", 46 | ) 47 | 48 | __version__ = _version 49 | 50 | 51 | def get_license_text() -> str: 52 | """Returns pybase64 license information as a :class:`str` object. 53 | 54 | The result includes libbase64 license information as well. 55 | """ 56 | return _license 57 | 58 | 59 | def get_version() -> str: 60 | """Returns pybase64 version as a :class:`str` object. 61 | 62 | The result reports if the C extension is used or not. 63 | e.g. `1.0.0 (C extension active - AVX2)` 64 | """ 65 | simd_name = _get_simd_name(_get_simd_path()) 66 | if simd_name != "fallback": 67 | return f"{__version__} (C extension active - {simd_name})" 68 | return f"{__version__} (C extension inactive)" 69 | 70 | 71 | def standard_b64encode(s: Buffer) -> bytes: 72 | """Encode bytes using the standard Base64 alphabet. 73 | 74 | Argument ``s`` is a :term:`bytes-like object` to encode. 75 | 76 | The result is returned as a :class:`bytes` object. 77 | """ 78 | return b64encode(s) 79 | 80 | 81 | def standard_b64decode(s: str | Buffer) -> bytes: 82 | """Decode bytes encoded with the standard Base64 alphabet. 83 | 84 | Argument ``s`` is a :term:`bytes-like object` or ASCII string to 85 | decode. 86 | 87 | The result is returned as a :class:`bytes` object. 88 | 89 | A :exc:`binascii.Error` is raised if the input is incorrectly padded. 90 | 91 | Characters that are not in the standard alphabet are discarded prior 92 | to the padding check. 93 | """ 94 | return b64decode(s) 95 | 96 | 97 | def urlsafe_b64encode(s: Buffer) -> bytes: 98 | """Encode bytes using the URL- and filesystem-safe Base64 alphabet. 99 | 100 | Argument ``s`` is a :term:`bytes-like object` to encode. 101 | 102 | The result is returned as a :class:`bytes` object. 103 | 104 | The alphabet uses '-' instead of '+' and '_' instead of '/'. 105 | """ 106 | return b64encode(s, b"-_") 107 | 108 | 109 | def urlsafe_b64decode(s: str | Buffer) -> bytes: 110 | """Decode bytes using the URL- and filesystem-safe Base64 alphabet. 111 | 112 | Argument ``s`` is a :term:`bytes-like object` or ASCII string to 113 | decode. 114 | 115 | The result is returned as a :class:`bytes` object. 116 | 117 | A :exc:`binascii.Error` is raised if the input is incorrectly padded. 118 | 119 | Characters that are not in the URL-safe base-64 alphabet, and are not 120 | a plus '+' or slash '/', are discarded prior to the padding check. 121 | 122 | The alphabet uses '-' instead of '+' and '_' instead of '/'. 123 | """ 124 | return b64decode(s, b"-_") 125 | -------------------------------------------------------------------------------- /src/pybase64/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import base64 5 | import sys 6 | from base64 import b64decode as b64decodeValidate 7 | from base64 import encodebytes as b64encodebytes 8 | from collections.abc import Sequence 9 | from pathlib import Path 10 | from timeit import default_timer as timer 11 | from typing import TYPE_CHECKING, Any 12 | 13 | import pybase64 14 | 15 | if TYPE_CHECKING: 16 | from pybase64._typing import Decode, Encode, EncodeBytes 17 | 18 | 19 | def bench_one( 20 | duration: float, 21 | data: bytes, 22 | enc: Encode, 23 | dec: Decode, 24 | encbytes: EncodeBytes, 25 | altchars: bytes | None = None, 26 | validate: bool = False, 27 | ) -> None: 28 | duration = duration / 2.0 29 | 30 | if not validate and altchars is None: 31 | number = 0 32 | time = timer() 33 | while True: 34 | encodedcontent = encbytes(data) 35 | number += 1 36 | if timer() - time > duration: 37 | break 38 | iter = number 39 | time = timer() 40 | while iter > 0: 41 | encodedcontent = encbytes(data) 42 | iter -= 1 43 | time = timer() - time 44 | print( 45 | "{:<32s} {:9.3f} MB/s ({:,d} bytes -> {:,d} bytes)".format( 46 | encbytes.__module__ + "." + encbytes.__name__ + ":", 47 | ((number * len(data)) / (1024.0 * 1024.0)) / time, 48 | len(data), 49 | len(encodedcontent), 50 | ) 51 | ) 52 | 53 | number = 0 54 | time = timer() 55 | while True: 56 | encodedcontent = enc(data, altchars=altchars) 57 | number += 1 58 | if timer() - time > duration: 59 | break 60 | iter = number 61 | time = timer() 62 | while iter > 0: 63 | encodedcontent = enc(data, altchars=altchars) 64 | iter -= 1 65 | time = timer() - time 66 | print( 67 | "{:<32s} {:9.3f} MB/s ({:,d} bytes -> {:,d} bytes)".format( 68 | enc.__module__ + "." + enc.__name__ + ":", 69 | ((number * len(data)) / (1024.0 * 1024.0)) / time, 70 | len(data), 71 | len(encodedcontent), 72 | ) 73 | ) 74 | 75 | number = 0 76 | time = timer() 77 | while True: 78 | decodedcontent = dec(encodedcontent, altchars=altchars, validate=validate) 79 | number += 1 80 | if timer() - time > duration: 81 | break 82 | iter = number 83 | time = timer() 84 | while iter > 0: 85 | decodedcontent = dec(encodedcontent, altchars=altchars, validate=validate) 86 | iter -= 1 87 | time = timer() - time 88 | print( 89 | "{:<32s} {:9.3f} MB/s ({:,d} bytes -> {:,d} bytes)".format( 90 | dec.__module__ + "." + dec.__name__ + ":", 91 | ((number * len(data)) / (1024.0 * 1024.0)) / time, 92 | len(encodedcontent), 93 | len(data), 94 | ) 95 | ) 96 | assert decodedcontent == data 97 | 98 | 99 | def readall(file: str) -> bytes: 100 | if file == "-": 101 | return sys.stdin.buffer.read() 102 | return Path(file).read_bytes() 103 | 104 | 105 | def writeall(file: str, data: bytes) -> None: 106 | if file == "-": 107 | sys.stdout.buffer.write(data) 108 | else: 109 | Path(file).write_bytes(data) 110 | 111 | 112 | def benchmark(duration: float, input: str) -> None: 113 | print(__package__ + " " + pybase64.get_version()) 114 | data = readall(input) 115 | for altchars in [None, b"-_"]: 116 | for validate in [False, True]: 117 | print(f"bench: altchars={altchars!r:s}, validate={validate!r:s}") 118 | bench_one( 119 | duration, 120 | data, 121 | pybase64.b64encode, 122 | pybase64.b64decode, 123 | pybase64.encodebytes, 124 | altchars, 125 | validate, 126 | ) 127 | bench_one( 128 | duration, 129 | data, 130 | base64.b64encode, 131 | b64decodeValidate, 132 | b64encodebytes, 133 | altchars, 134 | validate, 135 | ) 136 | 137 | 138 | def encode(input: str, altchars: bytes | None, output: str) -> None: 139 | data = readall(input) 140 | data = pybase64.b64encode(data, altchars) 141 | writeall(output, data) 142 | 143 | 144 | def decode(input: str, altchars: bytes | None, validate: bool, output: str) -> None: 145 | data = readall(input) 146 | data = pybase64.b64decode(data, altchars, validate) 147 | writeall(output, data) 148 | 149 | 150 | class LicenseAction(argparse.Action): 151 | def __init__( 152 | self, 153 | option_strings: Sequence[str], 154 | dest: str, 155 | license: str | None = None, 156 | help: str | None = "show license information and exit", 157 | ): 158 | super().__init__( 159 | option_strings=option_strings, 160 | dest=dest, 161 | default=argparse.SUPPRESS, 162 | nargs=0, 163 | help=help, 164 | ) 165 | self.license = license 166 | 167 | def __call__( 168 | self, 169 | parser: argparse.ArgumentParser, 170 | namespace: argparse.Namespace, # noqa: ARG002 171 | values: str | Sequence[Any] | None, # noqa: ARG002 172 | option_string: str | None = None, # noqa: ARG002 173 | ) -> None: 174 | print(self.license) 175 | parser.exit() 176 | 177 | 178 | def check_file(value: str, is_input: bool) -> str: 179 | if value == "-": 180 | return value 181 | path = Path(value) 182 | if is_input: 183 | return str(path.resolve(strict=True)) 184 | return str(path.parent.resolve(strict=True) / path.name) 185 | 186 | 187 | def main(argv: Sequence[str] | None = None) -> None: 188 | # main parser 189 | parser = argparse.ArgumentParser( 190 | prog=__package__, description=__package__ + " command-line tool." 191 | ) 192 | parser.add_argument( 193 | "-V", 194 | "--version", 195 | action="version", 196 | version=__package__ + " " + pybase64.get_version(), 197 | ) 198 | parser.add_argument("--license", action=LicenseAction, license=pybase64.get_license_text()) 199 | # create sub-parsers 200 | subparsers = parser.add_subparsers(help="tool help") 201 | # benchmark parser 202 | benchmark_parser = subparsers.add_parser("benchmark", help="-h for usage") 203 | benchmark_parser.add_argument( 204 | "-d", 205 | "--duration", 206 | metavar="D", 207 | dest="duration", 208 | type=float, 209 | default=1.0, 210 | help="expected duration for a single encode or decode test", 211 | ) 212 | benchmark_parser.register("type", "input file", lambda s: check_file(s, True)) 213 | benchmark_parser.add_argument( 214 | "input", type="input file", help="input file used for the benchmark" 215 | ) 216 | benchmark_parser.set_defaults(func=benchmark) 217 | # encode parser 218 | encode_parser = subparsers.add_parser("encode", help="-h for usage") 219 | encode_parser.register("type", "input file", lambda s: check_file(s, True)) 220 | encode_parser.register("type", "output file", lambda s: check_file(s, False)) 221 | encode_parser.add_argument("input", type="input file", help="input file to be encoded") 222 | group = encode_parser.add_mutually_exclusive_group() 223 | group.add_argument( 224 | "-u", 225 | "--url", 226 | action="store_const", 227 | const=b"-_", 228 | dest="altchars", 229 | help="use URL encoding", 230 | ) 231 | group.add_argument( 232 | "-a", 233 | "--altchars", 234 | dest="altchars", 235 | help="use alternative characters for encoding", 236 | ) 237 | encode_parser.add_argument( 238 | "-o", 239 | "--output", 240 | dest="output", 241 | type="output file", 242 | default="-", 243 | help="encoded output file (default to stdout)", 244 | ) 245 | encode_parser.set_defaults(func=encode) 246 | # decode parser 247 | decode_parser = subparsers.add_parser("decode", help="-h for usage") 248 | decode_parser.register("type", "input file", lambda s: check_file(s, True)) 249 | decode_parser.register("type", "output file", lambda s: check_file(s, False)) 250 | decode_parser.add_argument("input", type="input file", help="input file to be decoded") 251 | group = decode_parser.add_mutually_exclusive_group() 252 | group.add_argument( 253 | "-u", 254 | "--url", 255 | action="store_const", 256 | const=b"-_", 257 | dest="altchars", 258 | help="use URL decoding", 259 | ) 260 | group.add_argument( 261 | "-a", 262 | "--altchars", 263 | dest="altchars", 264 | help="use alternative characters for decoding", 265 | ) 266 | decode_parser.add_argument( 267 | "-o", 268 | "--output", 269 | dest="output", 270 | type="output file", 271 | default="-", 272 | help="decoded output file (default to stdout)", 273 | ) 274 | decode_parser.add_argument( 275 | "--no-validation", 276 | dest="validate", 277 | action="store_false", 278 | help="disable validation of the input data", 279 | ) 280 | decode_parser.set_defaults(func=decode) 281 | # ready, parse 282 | if argv is None: 283 | argv = sys.argv[1:] 284 | if len(argv) == 0: 285 | argv = ["-h"] 286 | args = vars(parser.parse_args(args=argv)) 287 | func = args.pop("func") 288 | func(**args) 289 | 290 | 291 | if __name__ == "__main__": 292 | main() 293 | -------------------------------------------------------------------------------- /src/pybase64/_fallback.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from base64 import b64decode as builtin_decode 4 | from base64 import b64encode as builtin_encode 5 | from base64 import encodebytes as builtin_encodebytes 6 | from binascii import Error as BinAsciiError 7 | from typing import TYPE_CHECKING 8 | 9 | if TYPE_CHECKING: 10 | from ._typing import Buffer 11 | 12 | _bytes_types = (bytes, bytearray) # Types acceptable as binary data 13 | 14 | 15 | def _get_simd_name(flags: int) -> str: 16 | assert flags == 0 17 | return "fallback" 18 | 19 | 20 | def _get_simd_path() -> int: 21 | return 0 22 | 23 | 24 | def _get_bytes(s: str | Buffer) -> bytes | bytearray: 25 | if isinstance(s, str): 26 | try: 27 | return s.encode("ascii") 28 | except UnicodeEncodeError: 29 | msg = "string argument should contain only ASCII characters" 30 | raise ValueError(msg) from None 31 | if isinstance(s, _bytes_types): 32 | return s 33 | try: 34 | mv = memoryview(s) 35 | if not mv.c_contiguous: 36 | msg = f"{s.__class__.__name__!r:s}: underlying buffer is not C-contiguous" 37 | raise BufferError(msg) 38 | return mv.tobytes() 39 | except TypeError: 40 | msg = ( 41 | "argument should be a bytes-like object or ASCII " 42 | f"string, not {s.__class__.__name__!r:s}" 43 | ) 44 | raise TypeError(msg) from None 45 | 46 | 47 | def b64decode( 48 | s: str | Buffer, altchars: str | Buffer | None = None, validate: bool = False 49 | ) -> bytes: 50 | """Decode bytes encoded with the standard Base64 alphabet. 51 | 52 | Argument ``s`` is a :term:`bytes-like object` or ASCII string to 53 | decode. 54 | 55 | Optional ``altchars`` must be a :term:`bytes-like object` or ASCII 56 | string of length 2 which specifies the alternative alphabet used instead 57 | of the '+' and '/' characters. 58 | 59 | If ``validate`` is ``False`` (the default), characters that are neither in 60 | the normal base-64 alphabet nor the alternative alphabet are discarded 61 | prior to the padding check. 62 | If ``validate`` is ``True``, these non-alphabet characters in the input 63 | result in a :exc:`binascii.Error`. 64 | 65 | The result is returned as a :class:`bytes` object. 66 | 67 | A :exc:`binascii.Error` is raised if ``s`` is incorrectly padded. 68 | """ 69 | s = _get_bytes(s) 70 | if altchars is not None: 71 | altchars = _get_bytes(altchars) 72 | if validate: 73 | if len(s) % 4 != 0: 74 | msg = "Incorrect padding" 75 | raise BinAsciiError(msg) 76 | result = builtin_decode(s, altchars, validate=False) 77 | 78 | # check length of result vs length of input 79 | expected_len = 0 80 | if len(s) > 0: 81 | padding = 0 82 | # len(s) % 4 != 0 implies len(s) >= 4 here 83 | if s[-2] == 61: # 61 == ord("=") 84 | padding += 1 85 | if s[-1] == 61: 86 | padding += 1 87 | expected_len = 3 * (len(s) // 4) - padding 88 | if expected_len != len(result): 89 | msg = "Non-base64 digit found" 90 | raise BinAsciiError(msg) 91 | return result 92 | return builtin_decode(s, altchars, validate=False) 93 | 94 | 95 | def b64decode_as_bytearray( 96 | s: str | Buffer, altchars: str | Buffer | None = None, validate: bool = False 97 | ) -> bytearray: 98 | """Decode bytes encoded with the standard Base64 alphabet. 99 | 100 | Argument ``s`` is a :term:`bytes-like object` or ASCII string to 101 | decode. 102 | 103 | Optional ``altchars`` must be a :term:`bytes-like object` or ASCII 104 | string of length 2 which specifies the alternative alphabet used instead 105 | of the '+' and '/' characters. 106 | 107 | If ``validate`` is ``False`` (the default), characters that are neither in 108 | the normal base-64 alphabet nor the alternative alphabet are discarded 109 | prior to the padding check. 110 | If ``validate`` is ``True``, these non-alphabet characters in the input 111 | result in a :exc:`binascii.Error`. 112 | 113 | The result is returned as a :class:`bytearray` object. 114 | 115 | A :exc:`binascii.Error` is raised if ``s`` is incorrectly padded. 116 | """ 117 | return bytearray(b64decode(s, altchars=altchars, validate=validate)) 118 | 119 | 120 | def b64encode(s: Buffer, altchars: str | Buffer | None = None) -> bytes: 121 | """Encode bytes using the standard Base64 alphabet. 122 | 123 | Argument ``s`` is a :term:`bytes-like object` to encode. 124 | 125 | Optional ``altchars`` must be a byte string of length 2 which specifies 126 | an alternative alphabet for the '+' and '/' characters. This allows an 127 | application to e.g. generate url or filesystem safe Base64 strings. 128 | 129 | The result is returned as a :class:`bytes` object. 130 | """ 131 | mv = memoryview(s) 132 | if not mv.c_contiguous: 133 | msg = f"{s.__class__.__name__!r:s}: underlying buffer is not C-contiguous" 134 | raise BufferError(msg) 135 | if altchars is not None: 136 | altchars = _get_bytes(altchars) 137 | return builtin_encode(s, altchars) 138 | 139 | 140 | def b64encode_as_string(s: Buffer, altchars: str | Buffer | None = None) -> str: 141 | """Encode bytes using the standard Base64 alphabet. 142 | 143 | Argument ``s`` is a :term:`bytes-like object` to encode. 144 | 145 | Optional ``altchars`` must be a byte string of length 2 which specifies 146 | an alternative alphabet for the '+' and '/' characters. This allows an 147 | application to e.g. generate url or filesystem safe Base64 strings. 148 | 149 | The result is returned as a :class:`str` object. 150 | """ 151 | return b64encode(s, altchars).decode("ascii") 152 | 153 | 154 | def encodebytes(s: Buffer) -> bytes: 155 | """Encode bytes into a bytes object with newlines (b'\\\\n') inserted after 156 | every 76 bytes of output, and ensuring that there is a trailing newline, 157 | as per :rfc:`2045` (MIME). 158 | 159 | Argument ``s`` is a :term:`bytes-like object` to encode. 160 | 161 | The result is returned as a :class:`bytes` object. 162 | """ 163 | mv = memoryview(s) 164 | if not mv.c_contiguous: 165 | msg = f"{s.__class__.__name__!r:s}: underlying buffer is not C-contiguous" 166 | raise BufferError(msg) 167 | return builtin_encodebytes(s) 168 | -------------------------------------------------------------------------------- /src/pybase64/_license.pyi: -------------------------------------------------------------------------------- 1 | _license: str 2 | -------------------------------------------------------------------------------- /src/pybase64/_pybase64.c: -------------------------------------------------------------------------------- 1 | #include "_pybase64_get_simd_flags.h" 2 | #define PY_SSIZE_T_CLEAN 3 | #define PY_CXX_CONST const 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include /* memset */ 10 | #include 11 | 12 | #ifdef __SSE2__ 13 | #include 14 | #endif 15 | 16 | #if defined(__x86_64__) || defined(__i386__) || defined(_M_IX86) || defined(_M_X64) 17 | #define HAVE_FAST_UNALIGNED_ACCESS 1 18 | #else 19 | #define HAVE_FAST_UNALIGNED_ACCESS 0 20 | #endif 21 | 22 | typedef struct pybase64_state { 23 | PyObject *binAsciiError; 24 | uint32_t active_simd_flag; 25 | uint32_t simd_flags; 26 | int libbase64_simd_flag; 27 | } pybase64_state; 28 | 29 | #if defined(PY_VERSION_HEX) && PY_VERSION_HEX >= 0x030d0000 30 | #define KW_CONST_CAST 31 | #else 32 | #define KW_CONST_CAST (char**) 33 | #endif 34 | 35 | /* returns 0 on success */ 36 | static int get_buffer(PyObject* object, Py_buffer* buffer) 37 | { 38 | if (PyObject_GetBuffer(object, buffer, PyBUF_RECORDS_RO | PyBUF_C_CONTIGUOUS) != 0) { 39 | return -1; 40 | } 41 | #if defined(PYPY_VERSION) 42 | /* PyPy does not respect PyBUF_C_CONTIGUOUS */ 43 | if (!PyBuffer_IsContiguous(buffer, 'C')) { 44 | PyBuffer_Release(buffer); 45 | PyErr_Format(PyExc_BufferError, "%R: underlying buffer is not C-contiguous", Py_TYPE(object)); 46 | return -1; 47 | } 48 | #endif 49 | return 0; 50 | } 51 | 52 | 53 | /* returns 0 on success */ 54 | static int parse_alphabet(PyObject* alphabetObject, char* alphabet, int* useAlphabet) 55 | { 56 | Py_buffer buffer; 57 | 58 | assert(useAlphabet != NULL); 59 | 60 | if ((alphabetObject == NULL) || (alphabetObject == Py_None)) { 61 | *useAlphabet = 0; 62 | return 0; 63 | } 64 | 65 | if (PyUnicode_Check(alphabetObject)) { 66 | alphabetObject = PyUnicode_AsASCIIString(alphabetObject); 67 | if (alphabetObject == NULL) { 68 | if (PyErr_ExceptionMatches(PyExc_UnicodeEncodeError)) { 69 | PyErr_SetString(PyExc_ValueError, "string argument should contain only ASCII characters"); 70 | } 71 | return -1; 72 | } 73 | } 74 | else { 75 | Py_INCREF(alphabetObject); 76 | } 77 | 78 | if (get_buffer(alphabetObject, &buffer) != 0) { 79 | Py_DECREF(alphabetObject); 80 | return -1; 81 | } 82 | 83 | if (buffer.len != 2) { 84 | PyBuffer_Release(&buffer); 85 | Py_DECREF(alphabetObject); 86 | PyErr_SetString(PyExc_AssertionError, "len(altchars) != 2"); 87 | return -1; 88 | } 89 | 90 | *useAlphabet = 1; 91 | alphabet[0] = ((const char*)buffer.buf)[0]; 92 | alphabet[1] = ((const char*)buffer.buf)[1]; 93 | 94 | if ((alphabet[0] == '+') && (alphabet[1] == '/')) { 95 | *useAlphabet = 0; 96 | } 97 | 98 | PyBuffer_Release(&buffer); 99 | Py_DECREF(alphabetObject); 100 | 101 | return 0; 102 | } 103 | 104 | static void translate_inplace(char* pSrcDst, size_t len, const char* alphabet) 105 | { 106 | size_t i = 0U; 107 | const char c0 = alphabet[0]; 108 | const char c1 = alphabet[1]; 109 | 110 | #ifdef __SSE2__ 111 | if (len >= 16U) { 112 | const __m128i plus = _mm_set1_epi8('+'); 113 | const __m128i slash = _mm_set1_epi8('/'); 114 | const __m128i c0_ = _mm_set1_epi8(c0); 115 | const __m128i c1_ = _mm_set1_epi8(c1); 116 | 117 | for (; i < (len & ~(size_t)15U); i += 16) { 118 | __m128i srcDst = _mm_loadu_si128((const __m128i*)(pSrcDst + i)); 119 | __m128i m0 = _mm_cmpeq_epi8(srcDst, plus); 120 | __m128i m1 = _mm_cmpeq_epi8(srcDst, slash); 121 | 122 | srcDst = _mm_or_si128(_mm_andnot_si128(m0, srcDst), _mm_and_si128(m0, c0_)); 123 | srcDst = _mm_or_si128(_mm_andnot_si128(m1, srcDst), _mm_and_si128(m1, c1_)); 124 | 125 | _mm_storeu_si128((__m128i*)(pSrcDst + i), srcDst); 126 | } 127 | } 128 | #endif 129 | 130 | for (; i < len; ++i) { 131 | char c = pSrcDst[i]; 132 | 133 | if (c == '+') { 134 | pSrcDst[i] = c0; 135 | } 136 | else if (c == '/') { 137 | pSrcDst[i] = c1; 138 | } 139 | } 140 | } 141 | 142 | static void translate(const char* pSrc, char* pDst, size_t len, const char* alphabet) 143 | { 144 | size_t i = 0U; 145 | const char c0 = alphabet[0]; 146 | const char c1 = alphabet[1]; 147 | 148 | #ifdef __SSE2__ 149 | if (len >= 16U) { 150 | const __m128i plus = _mm_set1_epi8('+'); 151 | const __m128i slash = _mm_set1_epi8('/'); 152 | const __m128i c0_ = _mm_set1_epi8(c0); 153 | const __m128i c1_ = _mm_set1_epi8(c1); 154 | 155 | for (; i < (len & ~(size_t)15U); i += 16) { 156 | __m128i srcDst = _mm_loadu_si128((const __m128i*)(pSrc + i)); 157 | __m128i m0 = _mm_cmpeq_epi8(srcDst, c0_); 158 | __m128i m1 = _mm_cmpeq_epi8(srcDst, c1_); 159 | 160 | srcDst = _mm_or_si128(_mm_andnot_si128(m0, srcDst), _mm_and_si128(m0, plus)); 161 | srcDst = _mm_or_si128(_mm_andnot_si128(m1, srcDst), _mm_and_si128(m1, slash)); 162 | 163 | _mm_storeu_si128((__m128i*)(pDst + i), srcDst); 164 | } 165 | } 166 | #endif 167 | 168 | for (; i < len; ++i) { 169 | const char cs = pSrc[i]; 170 | char cd; 171 | 172 | if (cs == c0) { 173 | cd = '+'; 174 | } 175 | else if (cs == c1) { 176 | cd = '/'; 177 | } 178 | #if 0 /* TODO, python does not do this, add option */ 179 | else if (cs == '+') { 180 | cd = c0; 181 | } 182 | else if (cs == '/') { 183 | cd = c1; 184 | } 185 | #endif 186 | else { 187 | cd = cs; 188 | } 189 | pDst[i] = cd; 190 | } 191 | } 192 | 193 | 194 | static int next_valid_padding(const uint8_t *src, size_t srclen) 195 | { 196 | int ret = 255; 197 | 198 | while (srclen && (ret == 255)) 199 | { 200 | ret = base64_table_dec_8bit[*src++]; 201 | srclen--; 202 | } 203 | 204 | return ret; 205 | } 206 | 207 | static int decode_novalidate(const uint8_t *src, size_t srclen, uint8_t *out, size_t*outlen) 208 | { 209 | uint8_t* out_start = out; 210 | uint8_t carry; 211 | 212 | while (srclen > 0U) 213 | { 214 | /* case bytes == 0 */ 215 | while (srclen > 4U) 216 | { 217 | union { 218 | uint32_t asint; 219 | uint8_t aschar[4]; 220 | } x; 221 | 222 | x.asint = base64_table_dec_32bit_d0[src[0]] 223 | | base64_table_dec_32bit_d1[src[1]] 224 | | base64_table_dec_32bit_d2[src[2]] 225 | | base64_table_dec_32bit_d3[src[3]]; 226 | #if BASE64_LITTLE_ENDIAN 227 | /* LUTs for little-endian set Most Significant Bit 228 | in case of invalid character */ 229 | if (x.asint & 0x80000000U) break; 230 | #else 231 | /* LUTs for big-endian set Least Significant Bit 232 | in case of invalid character */ 233 | if (x.asint & 1U) break; 234 | #endif 235 | 236 | #if HAVE_FAST_UNALIGNED_ACCESS 237 | /* This might segfault or be too slow on 238 | some architectures, do this only if specified 239 | with HAVE_FAST_UNALIGNED_ACCESS macro 240 | We write one byte more than needed */ 241 | *(uint32_t*)out = x.asint; 242 | #else 243 | /* Fallback, write bytes one by one */ 244 | out[0] = x.aschar[0]; 245 | out[1] = x.aschar[1]; 246 | out[2] = x.aschar[2]; 247 | #endif 248 | src += 4; 249 | out += 3; 250 | srclen -= 4; 251 | } 252 | /* case bytes == 0, remainder */ 253 | { 254 | uint8_t c = *src++; srclen--; 255 | uint8_t q; 256 | if ((q = base64_table_dec_8bit[c]) >= 254) { 257 | continue; 258 | } 259 | carry = q << 2; 260 | } 261 | /* case bytes == 1 */ 262 | for(;;) 263 | { 264 | if (srclen-- == 0) { 265 | return 1; 266 | } 267 | uint8_t c = *src++; 268 | uint8_t q; 269 | if ((q = base64_table_dec_8bit[c]) >= 254) { 270 | continue; 271 | } 272 | *out++ = carry | (q >> 4); 273 | carry = q << 4; 274 | break; 275 | } 276 | /* case bytes == 2 */ 277 | for(;;) 278 | { 279 | if (srclen-- == 0) { 280 | return 1; 281 | } 282 | uint8_t c = *src++; 283 | uint8_t q; 284 | if ((q = base64_table_dec_8bit[c]) >= 254) { 285 | if (q == 254) { 286 | /* if the next valid byte is '=' => end */ 287 | if (next_valid_padding(src, srclen) == 254) { 288 | goto END; 289 | } 290 | } 291 | continue; 292 | } 293 | *out++ = carry | (q >> 2); 294 | carry = q << 6; 295 | break; 296 | } 297 | /* case bytes == 3 */ 298 | for(;;) 299 | { 300 | if (srclen-- == 0) { 301 | return 1; 302 | } 303 | uint8_t c = *src++; 304 | uint8_t q; 305 | if ((q = base64_table_dec_8bit[c]) >= 254) { 306 | if (q == 254) { 307 | srclen = 0U; 308 | break; 309 | } 310 | continue; 311 | } 312 | *out++ = carry | q; 313 | break; 314 | } 315 | } 316 | END: 317 | *outlen = out - out_start; 318 | return 0; 319 | } 320 | 321 | static PyObject* pybase64_encode_impl(PyObject* self, PyObject* args, PyObject *kwds, int return_string) 322 | { 323 | static const char *kwlist[] = { "", "altchars", NULL }; 324 | 325 | int use_alphabet = 0; 326 | char alphabet[2]; 327 | Py_buffer buffer; 328 | size_t out_len; 329 | PyObject* out_object; 330 | PyObject* in_object; 331 | PyObject* in_alphabet = NULL; 332 | char* dst; 333 | pybase64_state *state = (pybase64_state*)PyModule_GetState(self); 334 | if (state == NULL) { 335 | return NULL; 336 | } 337 | 338 | /* Parse the input tuple */ 339 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", KW_CONST_CAST kwlist, &in_object, &in_alphabet)) { 340 | return NULL; 341 | } 342 | 343 | if (parse_alphabet(in_alphabet, alphabet, &use_alphabet) != 0) { 344 | return NULL; 345 | } 346 | 347 | if (get_buffer(in_object, &buffer) != 0) { 348 | return NULL; 349 | } 350 | 351 | if (buffer.len > (3 * (PY_SSIZE_T_MAX / 4))) { 352 | PyBuffer_Release(&buffer); 353 | return PyErr_NoMemory(); 354 | } 355 | 356 | out_len = (size_t)(((buffer.len + 2) / 3) * 4); 357 | if (return_string) { 358 | out_object = PyUnicode_New((Py_ssize_t)out_len, 127); 359 | } 360 | else { 361 | out_object = PyBytes_FromStringAndSize(NULL, (Py_ssize_t)out_len); 362 | } 363 | if (out_object == NULL) { 364 | PyBuffer_Release(&buffer); 365 | return NULL; 366 | } 367 | if (return_string) { 368 | dst = (char*)PyUnicode_1BYTE_DATA(out_object); 369 | } 370 | else { 371 | dst = PyBytes_AS_STRING(out_object); 372 | } 373 | 374 | /* not interacting with Python objects from here, release the GIL */ 375 | Py_BEGIN_ALLOW_THREADS 376 | 377 | int const libbase64_simd_flag = state->libbase64_simd_flag; 378 | 379 | if (use_alphabet) { 380 | /* TODO, make this more efficient */ 381 | const size_t dst_slice = 16U * 1024U; 382 | const Py_ssize_t src_slice = (Py_ssize_t)((dst_slice / 4U) * 3U); 383 | Py_ssize_t len = buffer.len; 384 | const char* src = (const char*)buffer.buf; 385 | size_t remainder; 386 | 387 | while (out_len > dst_slice) { 388 | size_t dst_len = dst_slice; 389 | 390 | base64_encode(src, src_slice, dst, &dst_len, libbase64_simd_flag); 391 | translate_inplace(dst, dst_slice, alphabet); 392 | 393 | len -= src_slice; 394 | src += src_slice; 395 | out_len -= dst_slice; 396 | dst += dst_slice; 397 | } 398 | remainder = out_len; 399 | base64_encode(src, len, dst, &out_len, libbase64_simd_flag); 400 | translate_inplace(dst, remainder, alphabet); 401 | } 402 | else { 403 | base64_encode(buffer.buf, buffer.len, dst, &out_len, libbase64_simd_flag); 404 | } 405 | 406 | /* restore the GIL */ 407 | Py_END_ALLOW_THREADS 408 | 409 | PyBuffer_Release(&buffer); 410 | 411 | return out_object; 412 | } 413 | 414 | static PyObject* pybase64_encode(PyObject* self, PyObject* args, PyObject *kwds) 415 | { 416 | return pybase64_encode_impl(self, args, kwds, 0); 417 | } 418 | 419 | static PyObject* pybase64_encode_as_string(PyObject* self, PyObject* args, PyObject *kwds) 420 | { 421 | return pybase64_encode_impl(self, args, kwds, 1); 422 | } 423 | 424 | static PyObject* pybase64_decode_impl(PyObject* self, PyObject* args, PyObject *kwds, int return_bytearray) 425 | { 426 | static const char *kwlist[] = { "", "altchars", "validate", NULL }; 427 | 428 | int use_alphabet = 0; 429 | char alphabet[2]; 430 | char validation = 0; 431 | Py_buffer buffer; 432 | size_t out_len; 433 | PyObject* in_alphabet = NULL; 434 | PyObject* in_object; 435 | PyObject* out_object = NULL; 436 | const void* source = NULL; 437 | Py_ssize_t source_len; 438 | int source_use_buffer = 0; 439 | void* dest; 440 | pybase64_state *state = (pybase64_state*)PyModule_GetState(self); 441 | if (state == NULL) { 442 | return NULL; 443 | } 444 | /* Parse the input tuple */ 445 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|Ob", KW_CONST_CAST kwlist, &in_object, &in_alphabet, &validation)) { 446 | return NULL; 447 | } 448 | 449 | if (parse_alphabet(in_alphabet, alphabet, &use_alphabet) != 0) { 450 | return NULL; 451 | } 452 | 453 | if (PyUnicode_Check(in_object)) { 454 | if ((PyUnicode_READY(in_object) == 0) && (PyUnicode_KIND(in_object) == PyUnicode_1BYTE_KIND)) { 455 | source = PyUnicode_1BYTE_DATA(in_object); 456 | source_len = PyUnicode_GET_LENGTH(in_object); 457 | } 458 | else { 459 | in_object = PyUnicode_AsASCIIString(in_object); 460 | if (in_object == NULL) { 461 | if (PyErr_ExceptionMatches(PyExc_UnicodeEncodeError)) { 462 | PyErr_SetString(PyExc_ValueError, "string argument should contain only ASCII characters"); 463 | } 464 | return NULL; 465 | } 466 | } 467 | } 468 | else { 469 | Py_INCREF(in_object); 470 | } 471 | if (source == NULL) { 472 | if (get_buffer(in_object, &buffer) != 0) { 473 | Py_DECREF(in_object); 474 | return NULL; 475 | } 476 | source = buffer.buf; 477 | source_len = buffer.len; 478 | source_use_buffer = 1; 479 | } 480 | 481 | /* TRY: */ 482 | if (!validation && use_alphabet) { 483 | PyObject* translate_object; 484 | char* translate_dst; 485 | 486 | translate_object = PyBytes_FromStringAndSize(NULL, source_len); 487 | if (translate_object == NULL) { 488 | goto EXCEPT; 489 | } 490 | translate_dst = PyBytes_AS_STRING(translate_object); 491 | 492 | /* not interacting with Python objects from here, release the GIL */ 493 | Py_BEGIN_ALLOW_THREADS 494 | 495 | translate(source, translate_dst, source_len, alphabet); 496 | 497 | /* restore the GIL */ 498 | Py_END_ALLOW_THREADS 499 | 500 | if (source_use_buffer) { 501 | PyBuffer_Release(&buffer); 502 | Py_DECREF(in_object); 503 | } 504 | in_object = translate_object; 505 | if (get_buffer(in_object, &buffer) != 0) { 506 | Py_DECREF(in_object); 507 | return NULL; 508 | } 509 | source = buffer.buf; 510 | source_len = buffer.len; 511 | source_use_buffer = 1; 512 | } 513 | 514 | /* No overflow check needed, exact out_len recomputed at the end */ 515 | /* out_len is ceildiv(len / 4) * 3 when len % 4 != 0*/ 516 | /* else out_len is (ceildiv(len / 4) + 1) * 3 */ 517 | out_len = (size_t)((source_len / 4) * 3) + 3U; 518 | if (return_bytearray) { 519 | out_object = PyByteArray_FromStringAndSize(NULL, (Py_ssize_t)out_len); 520 | } 521 | else { 522 | out_object = PyBytes_FromStringAndSize(NULL, (Py_ssize_t)out_len); 523 | } 524 | if (out_object == NULL) { 525 | goto EXCEPT; 526 | } 527 | if (return_bytearray) { 528 | dest = PyByteArray_AS_STRING(out_object); 529 | } 530 | else { 531 | dest = PyBytes_AS_STRING(out_object); 532 | } 533 | 534 | if (!validation) { 535 | int result; 536 | 537 | /* not interacting with Python objects from here, release the GIL */ 538 | Py_BEGIN_ALLOW_THREADS 539 | 540 | result = decode_novalidate(source, source_len, dest, &out_len); 541 | 542 | /* restore the GIL */ 543 | Py_END_ALLOW_THREADS 544 | 545 | if (result != 0) { 546 | PyErr_SetString(state->binAsciiError, "Incorrect padding"); 547 | goto EXCEPT; 548 | } 549 | } 550 | else if (use_alphabet) { 551 | /* TODO, make this more efficient */ 552 | const Py_ssize_t src_slice = 16 * 1024; 553 | const size_t dst_slice = (src_slice / 4) * 3; 554 | char cache[16 * 1024]; 555 | Py_ssize_t len = source_len; 556 | const char* src = source; 557 | char* dst = dest; 558 | int result = 1; 559 | 560 | /* not interacting with Python objects from here, release the GIL */ 561 | Py_BEGIN_ALLOW_THREADS 562 | 563 | int const libbase64_simd_flag = state->libbase64_simd_flag; 564 | while (len > src_slice) { 565 | size_t dst_len = dst_slice; 566 | 567 | translate(src, cache, src_slice, alphabet); 568 | result = base64_decode(cache, src_slice, dst, &dst_len, libbase64_simd_flag); 569 | if (result <= 0) { 570 | break; 571 | } 572 | 573 | len -= src_slice; 574 | src += src_slice; 575 | out_len -= dst_slice; 576 | dst += dst_slice; 577 | } 578 | if (result > 0) { 579 | translate(src, cache, len, alphabet); 580 | result = base64_decode(cache, len, dst, &out_len, libbase64_simd_flag); 581 | } 582 | 583 | /* restore the GIL */ 584 | Py_END_ALLOW_THREADS 585 | 586 | if (result <= 0) { 587 | PyErr_SetString(state->binAsciiError, "Non-base64 digit found"); 588 | goto EXCEPT; 589 | } 590 | out_len += (dst - (char*)dest); 591 | } 592 | else { 593 | int result; 594 | 595 | /* not interacting with Python objects from here, release the GIL */ 596 | Py_BEGIN_ALLOW_THREADS 597 | 598 | result = base64_decode(source, source_len, dest, &out_len, state->libbase64_simd_flag); 599 | 600 | /* restore the GIL */ 601 | Py_END_ALLOW_THREADS 602 | 603 | if (result <= 0) { 604 | PyErr_SetString(state->binAsciiError, "Non-base64 digit found"); 605 | goto EXCEPT; 606 | } 607 | } 608 | if (return_bytearray) { 609 | PyByteArray_Resize(out_object, (Py_ssize_t)out_len); 610 | } 611 | else { 612 | _PyBytes_Resize(&out_object, (Py_ssize_t)out_len); 613 | } 614 | goto FINALLY; 615 | EXCEPT: 616 | if (out_object != NULL) { 617 | Py_DECREF(out_object); 618 | out_object = NULL; 619 | } 620 | FINALLY: 621 | if (source_use_buffer) { 622 | PyBuffer_Release(&buffer); 623 | Py_DECREF(in_object); 624 | } 625 | return out_object; 626 | } 627 | 628 | static PyObject* pybase64_decode(PyObject* self, PyObject* args, PyObject *kwds) 629 | { 630 | return pybase64_decode_impl(self, args, kwds, 0); 631 | } 632 | 633 | static PyObject* pybase64_decode_as_bytearray(PyObject* self, PyObject* args, PyObject *kwds) 634 | { 635 | return pybase64_decode_impl(self, args, kwds, 1); 636 | } 637 | 638 | static PyObject* pybase64_encodebytes(PyObject* self, PyObject* in_object) 639 | { 640 | Py_buffer buffer; 641 | size_t out_len; 642 | PyObject* out_object; 643 | pybase64_state *state = (pybase64_state*)PyModule_GetState(self); 644 | if (state == NULL) { 645 | return NULL; 646 | } 647 | if (get_buffer(in_object, &buffer) != 0) { 648 | return NULL; 649 | } 650 | if (((buffer.format[0] != 'c') && (buffer.format[0] != 'b') && (buffer.format[0] != 'B')) || buffer.format[1] != '\0' ) { 651 | PyBuffer_Release(&buffer); 652 | return PyErr_Format(PyExc_TypeError, "expected single byte elements, not '%s' from %R", buffer.format, Py_TYPE(in_object)); 653 | } 654 | if (buffer.ndim != 1) { 655 | PyBuffer_Release(&buffer); 656 | return PyErr_Format(PyExc_TypeError, "expected 1-D data, not %d-D data from %R", buffer.ndim, Py_TYPE(in_object)); 657 | } 658 | if (buffer.len > (3 * (PY_SSIZE_T_MAX / 4))) { 659 | PyBuffer_Release(&buffer); 660 | return PyErr_NoMemory(); 661 | } 662 | 663 | out_len = (size_t)(((buffer.len + 2) / 3) * 4); 664 | if (out_len != 0U) { 665 | if ((((out_len - 1U) / 76U) + 1U) > (PY_SSIZE_T_MAX - out_len)) { 666 | PyBuffer_Release(&buffer); 667 | return PyErr_NoMemory(); 668 | } 669 | out_len += ((out_len - 1U) / 76U) + 1U; 670 | } 671 | 672 | out_object = PyBytes_FromStringAndSize(NULL, (Py_ssize_t)out_len); 673 | if (out_object == NULL) { 674 | PyBuffer_Release(&buffer); 675 | return NULL; 676 | } 677 | 678 | if (out_len > 0) 679 | { 680 | const size_t dst_slice = 77U; 681 | const Py_ssize_t src_slice = (Py_ssize_t)((dst_slice / 4U) * 3U); 682 | Py_ssize_t len = buffer.len; 683 | const char* src = (const char*)buffer.buf; 684 | char* dst = PyBytes_AS_STRING(out_object); 685 | size_t remainder; 686 | 687 | /* not interacting with Python objects from here, release the GIL */ 688 | Py_BEGIN_ALLOW_THREADS 689 | 690 | int const libbase64_simd_flag = state->libbase64_simd_flag; 691 | while (out_len > dst_slice) { 692 | size_t dst_len = dst_slice - 1U; 693 | 694 | base64_encode(src, src_slice, dst, &dst_len, libbase64_simd_flag); 695 | dst[dst_slice - 1U] = '\n'; 696 | 697 | len -= src_slice; 698 | src += src_slice; 699 | out_len -= dst_slice; 700 | dst += dst_slice; 701 | } 702 | remainder = out_len - 1; 703 | base64_encode(src, len, dst, &remainder, libbase64_simd_flag); 704 | dst[out_len - 1] = '\n'; 705 | 706 | /* restore the GIL */ 707 | Py_END_ALLOW_THREADS 708 | } 709 | 710 | PyBuffer_Release(&buffer); 711 | 712 | return out_object; 713 | } 714 | 715 | static PyObject* pybase64_get_simd_path(PyObject* self, PyObject* arg) 716 | { 717 | pybase64_state *state = (pybase64_state*)PyModule_GetState(self); 718 | if (state == NULL) { 719 | return NULL; 720 | } 721 | return PyLong_FromUnsignedLong(state->active_simd_flag); 722 | } 723 | 724 | static PyObject* pybase64_get_simd_flags_runtime(PyObject* self, PyObject* arg) 725 | { 726 | pybase64_state *state = (pybase64_state*)PyModule_GetState(self); 727 | if (state == NULL) { 728 | return NULL; 729 | } 730 | return PyLong_FromUnsignedLong(state->simd_flags); 731 | } 732 | 733 | static PyObject* pybase64_get_simd_flags_compile(PyObject* self, PyObject* arg) 734 | { 735 | uint32_t result = 0U; 736 | 737 | #if BASE64_WITH_NEON64 || BASE64_WITH_NEON32 738 | result |= PYBASE64_NEON; 739 | #endif 740 | 741 | #if BASE64_WITH_AVX512 742 | result |= PYBASE64_AVX512VBMI; 743 | #endif 744 | #if BASE64_WITH_AVX2 745 | result |= PYBASE64_AVX2; 746 | #endif 747 | #if BASE64_WITH_AVX 748 | result |= PYBASE64_AVX; 749 | #endif 750 | #if BASE64_WITH_SSE42 751 | result |= PYBASE64_SSE42; 752 | #endif 753 | #if BASE64_WITH_SSE41 754 | result |= PYBASE64_SSE41; 755 | #endif 756 | #if BASE64_WITH_SSSE3 757 | result |= PYBASE64_SSSE3; 758 | #endif 759 | return PyLong_FromUnsignedLong(result); 760 | } 761 | 762 | static void set_simd_path(pybase64_state* state, uint32_t flag) 763 | { 764 | flag &= state->simd_flags; /* clean-up with allowed flags */ 765 | 766 | if (0) { 767 | } 768 | #if BASE64_WITH_NEON64 769 | else if (flag & PYBASE64_NEON) { 770 | state->active_simd_flag = PYBASE64_NEON; 771 | state->libbase64_simd_flag = BASE64_FORCE_NEON64; 772 | } 773 | #endif 774 | #if BASE64_WITH_NEON32 775 | else if (flag & PYBASE64_NEON) { 776 | state->active_simd_flag = PYBASE64_NEON; 777 | state->libbase64_simd_flag = BASE64_FORCE_NEON32; 778 | } 779 | #endif 780 | 781 | #if BASE64_WITH_AVX512 782 | else if (flag & PYBASE64_AVX512VBMI) { 783 | state->active_simd_flag = PYBASE64_AVX512VBMI; 784 | state->libbase64_simd_flag = BASE64_FORCE_AVX512; 785 | } 786 | #endif 787 | #if BASE64_WITH_AVX2 788 | else if (flag & PYBASE64_AVX2) { 789 | state->active_simd_flag = PYBASE64_AVX2; 790 | state->libbase64_simd_flag = BASE64_FORCE_AVX2; 791 | } 792 | #endif 793 | #if BASE64_WITH_AVX 794 | else if (flag & PYBASE64_AVX) { 795 | state->active_simd_flag = PYBASE64_AVX; 796 | state->libbase64_simd_flag = BASE64_FORCE_AVX; 797 | } 798 | #endif 799 | #if BASE64_WITH_SSE42 800 | else if (flag & PYBASE64_SSE42) { 801 | state->active_simd_flag = PYBASE64_SSE42; 802 | state->libbase64_simd_flag = BASE64_FORCE_SSE42; 803 | } 804 | #endif 805 | #if BASE64_WITH_SSE41 806 | else if (flag & PYBASE64_SSE41) { 807 | state->active_simd_flag = PYBASE64_SSE41; 808 | state->libbase64_simd_flag = BASE64_FORCE_SSE41; 809 | } 810 | #endif 811 | #if BASE64_WITH_SSSE3 812 | else if (flag & PYBASE64_SSSE3) { 813 | state->active_simd_flag = PYBASE64_SSSE3; 814 | state->libbase64_simd_flag = BASE64_FORCE_SSSE3; 815 | } 816 | #endif 817 | else { 818 | state->active_simd_flag = PYBASE64_NONE; 819 | state->libbase64_simd_flag = BASE64_FORCE_PLAIN; 820 | } 821 | } 822 | 823 | static PyObject* pybase64_set_simd_path(PyObject* self, PyObject* arg) 824 | { 825 | pybase64_state *state = (pybase64_state*)PyModule_GetState(self); 826 | if (state == NULL) { 827 | return NULL; 828 | } 829 | set_simd_path(state, (uint32_t)PyLong_AsUnsignedLong(arg)); 830 | Py_RETURN_NONE; 831 | } 832 | 833 | static PyObject* pybase64_get_simd_name(PyObject* self, PyObject* arg) 834 | { 835 | uint32_t flags = (uint32_t)PyLong_AsUnsignedLong(arg); 836 | 837 | if (flags & PYBASE64_NEON) { 838 | return PyUnicode_FromString("NEON"); 839 | } 840 | 841 | if (flags & PYBASE64_AVX512VBMI) { 842 | return PyUnicode_FromString("AVX512VBMI"); 843 | } 844 | if (flags & PYBASE64_AVX2) { 845 | return PyUnicode_FromString("AVX2"); 846 | } 847 | if (flags & PYBASE64_AVX) { 848 | return PyUnicode_FromString("AVX"); 849 | } 850 | if (flags & PYBASE64_SSE42) { 851 | return PyUnicode_FromString("SSE42"); 852 | } 853 | if (flags & PYBASE64_SSE41) { 854 | return PyUnicode_FromString("SSE41"); 855 | } 856 | if (flags & PYBASE64_SSSE3) { 857 | return PyUnicode_FromString("SSSE3"); 858 | } 859 | if (flags & PYBASE64_SSE3) { 860 | return PyUnicode_FromString("SSE3"); 861 | } 862 | if (flags & PYBASE64_SSE2) { 863 | return PyUnicode_FromString("SSE2"); 864 | } 865 | 866 | if (flags != 0) { 867 | return PyUnicode_FromString("unknown"); 868 | } 869 | 870 | return PyUnicode_FromString("No SIMD"); 871 | } 872 | 873 | static PyObject* pybase64_import(const char* from, const char* object) 874 | { 875 | PyObject* subModules; 876 | PyObject* subModuleName; 877 | PyObject* moduleName; 878 | PyObject* imports; 879 | PyObject* importedObject; 880 | 881 | subModules = PyList_New(1); 882 | if (subModules == NULL) { 883 | return NULL; 884 | } 885 | moduleName = PyUnicode_FromString(from); 886 | if (moduleName == NULL) { 887 | Py_DECREF(subModules); 888 | return NULL; 889 | } 890 | subModuleName = PyUnicode_FromString(object); 891 | if (subModuleName == NULL) { 892 | Py_DECREF(moduleName); 893 | Py_DECREF(subModules); 894 | return NULL; 895 | } 896 | Py_INCREF(subModuleName); 897 | PyList_SET_ITEM(subModules, 0, subModuleName); 898 | imports = PyImport_ImportModuleLevelObject(moduleName, NULL, NULL, subModules, 0); 899 | Py_DECREF(moduleName); 900 | Py_DECREF(subModules); 901 | if (imports == NULL) { 902 | Py_DECREF(subModuleName); 903 | return NULL; 904 | } 905 | importedObject = PyObject_GetAttr(imports, subModuleName); 906 | Py_DECREF(subModuleName); 907 | Py_DECREF(imports); 908 | return importedObject; 909 | } 910 | 911 | static PyObject* pybase64_import_BinAsciiError() 912 | { 913 | PyObject* binAsciiError; 914 | 915 | binAsciiError = pybase64_import("binascii", "Error"); 916 | if (binAsciiError == NULL) { 917 | return NULL; 918 | } 919 | if (!PyObject_IsSubclass(binAsciiError, PyExc_Exception)) { 920 | Py_DECREF(binAsciiError); 921 | return NULL; 922 | } 923 | 924 | return binAsciiError; 925 | } 926 | 927 | static int _pybase64_exec(PyObject *m) 928 | { 929 | pybase64_state *state = (pybase64_state*)PyModule_GetState(m); 930 | if (state == NULL) { 931 | return -1; 932 | } 933 | 934 | state->binAsciiError = pybase64_import_BinAsciiError(); 935 | if (state->binAsciiError == NULL) { 936 | return -1; 937 | } 938 | 939 | Py_INCREF(state->binAsciiError); /* PyModule_AddObject steals a reference */ 940 | if (PyModule_AddObject(m, "_BinAsciiError", state->binAsciiError) != 0) { 941 | Py_DECREF(state->binAsciiError); 942 | return -1; 943 | } 944 | 945 | state->simd_flags = pybase64_get_simd_flags(); 946 | set_simd_path(state, state->simd_flags); 947 | 948 | return 0; 949 | } 950 | 951 | static int _pybase64_traverse(PyObject *m, visitproc visit, void *arg) 952 | { 953 | pybase64_state *state = (pybase64_state*)PyModule_GetState(m); 954 | if (state) { 955 | Py_VISIT(state->binAsciiError); 956 | } 957 | return 0; 958 | } 959 | 960 | static int _pybase64_clear(PyObject *m) 961 | { 962 | pybase64_state *state = (pybase64_state*)PyModule_GetState(m); 963 | if (state) { 964 | Py_CLEAR(state->binAsciiError); 965 | } 966 | return 0; 967 | } 968 | 969 | static void _pybase64_free(void *m) 970 | { 971 | _pybase64_clear((PyObject *)m); 972 | } 973 | 974 | static PyMethodDef _pybase64_methods[] = { 975 | { "b64encode", (PyCFunction)pybase64_encode, METH_VARARGS | METH_KEYWORDS, NULL }, 976 | { "b64encode_as_string", (PyCFunction)pybase64_encode_as_string, METH_VARARGS | METH_KEYWORDS, NULL }, 977 | { "b64decode", (PyCFunction)pybase64_decode, METH_VARARGS | METH_KEYWORDS, NULL }, 978 | { "b64decode_as_bytearray", (PyCFunction)pybase64_decode_as_bytearray, METH_VARARGS | METH_KEYWORDS, NULL }, 979 | { "encodebytes", (PyCFunction)pybase64_encodebytes, METH_O, NULL }, 980 | { "_get_simd_path", (PyCFunction)pybase64_get_simd_path, METH_NOARGS, NULL }, 981 | { "_set_simd_path", (PyCFunction)pybase64_set_simd_path, METH_O, NULL }, 982 | { "_get_simd_flags_compile", (PyCFunction)pybase64_get_simd_flags_compile, METH_NOARGS, NULL }, 983 | { "_get_simd_flags_runtime", (PyCFunction)pybase64_get_simd_flags_runtime, METH_NOARGS, NULL }, 984 | { "_get_simd_name", (PyCFunction)pybase64_get_simd_name, METH_O, NULL }, 985 | { NULL, NULL, 0, NULL } /* Sentinel */ 986 | }; 987 | 988 | static PyModuleDef_Slot _pybase64_slots[] = { 989 | {Py_mod_exec, _pybase64_exec}, 990 | #ifdef Py_mod_multiple_interpreters 991 | {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, 992 | #endif 993 | #ifdef Py_mod_gil 994 | {Py_mod_gil, Py_MOD_GIL_NOT_USED}, 995 | #endif 996 | {0, NULL} 997 | }; 998 | 999 | /* Initialize this module. */ 1000 | static struct PyModuleDef _pybase64_module = { 1001 | PyModuleDef_HEAD_INIT, 1002 | "pybase64._pybase64", 1003 | NULL, 1004 | sizeof(pybase64_state), 1005 | _pybase64_methods, 1006 | _pybase64_slots, 1007 | _pybase64_traverse, 1008 | _pybase64_clear, 1009 | _pybase64_free 1010 | }; 1011 | 1012 | PyMODINIT_FUNC 1013 | PyInit__pybase64(void) 1014 | { 1015 | return PyModuleDef_Init(&_pybase64_module); 1016 | } 1017 | -------------------------------------------------------------------------------- /src/pybase64/_pybase64.pyi: -------------------------------------------------------------------------------- 1 | from ._typing import Buffer 2 | 3 | def _get_simd_flags_compile() -> int: ... 4 | def _get_simd_flags_runtime() -> int: ... 5 | def _get_simd_name(flags: int) -> str: ... 6 | def _get_simd_path() -> int: ... 7 | def _set_simd_path(flags: int) -> None: ... 8 | def b64decode( 9 | s: str | Buffer, altchars: str | Buffer | None = None, validate: bool = False 10 | ) -> bytes: ... 11 | def b64decode_as_bytearray( 12 | s: str | Buffer, altchars: str | Buffer | None = None, validate: bool = False 13 | ) -> bytearray: ... 14 | def b64encode(s: Buffer, altchars: str | Buffer | None = None) -> bytes: ... 15 | def b64encode_as_string(s: Buffer, altchars: str | Buffer | None = None) -> str: ... 16 | def encodebytes(s: Buffer) -> bytes: ... 17 | -------------------------------------------------------------------------------- /src/pybase64/_pybase64_get_simd_flags.c: -------------------------------------------------------------------------------- 1 | #include "_pybase64_get_simd_flags.h" 2 | #include 3 | 4 | #if defined(__x86_64__) || defined(__i386__) || defined(_M_IX86) || defined(_M_X64) 5 | /* x86 version */ 6 | #if defined(_MSC_VER) 7 | #include /* cpuid */ 8 | static void cpuid_(uint32_t leaf, uint32_t subleaf, uint32_t* eax, uint32_t* ebx, uint32_t* ecx, uint32_t* edx) 9 | { 10 | int info[4]; 11 | __cpuidex(info, (int)leaf, (int)subleaf); 12 | *eax = (uint32_t)info[0]; 13 | *ebx = (uint32_t)info[1]; 14 | *ecx = (uint32_t)info[2]; 15 | *edx = (uint32_t)info[3]; 16 | } 17 | #elif defined(__GNUC__) 18 | #include /* __cpuid_count */ 19 | static void cpuid_(uint32_t leaf, uint32_t subleaf, uint32_t* eax, uint32_t* ebx, uint32_t* ecx, uint32_t* edx) 20 | { 21 | __cpuid_count(leaf, subleaf, *eax, *ebx, *ecx, *edx); 22 | } 23 | static uint64_t _xgetbv (uint32_t index) 24 | { 25 | uint32_t eax, edx; 26 | __asm__ __volatile__("xgetbv" : "=a"(eax), "=d"(edx) : "c"(index)); 27 | return ((uint64_t)edx << 32) | eax; 28 | } 29 | #else 30 | /* not supported yet */ 31 | static void cpuid_(uint32_t leaf, uint32_t subleaf, uint32_t* eax, uint32_t* ebx, uint32_t* ecx, uint32_t* edx) 32 | { 33 | (void)subleaf; 34 | (void)ebx; 35 | (void)ecx; 36 | (void)edx; 37 | 38 | if (leaf == 0) { 39 | *eax = 0U; /* max-level 0 */ 40 | } 41 | } 42 | static uint64_t _xgetbv (uint32_t index) 43 | { 44 | (void)index; 45 | return 0U; 46 | } 47 | #endif 48 | 49 | #define PB64_SSE2_BIT_LVL1_EDX (UINT32_C(1) << 26) 50 | #define PB64_SSE3_BIT_LVL1_ECX (UINT32_C(1) << 0) 51 | #define PB64_SSSE3_BIT_LVL1_ECX (UINT32_C(1) << 9) 52 | #define PB64_SSE41_BIT_LVL1_ECX (UINT32_C(1) << 19) 53 | #define PB64_SSE42_BIT_LVL1_ECX (UINT32_C(1) << 20) 54 | #define PB64_AVX_BIT_LVL1_ECX (UINT32_C(1) << 28) 55 | #define PB64_AVX2_BIT_LVL7_EBX (UINT32_C(1) << 5) 56 | #define PB64_AVX512F_BIT_LVL7_EBX (UINT32_C(1) << 16) 57 | #define PB64_AVX512VL_BIT_LVL7_EBX (UINT32_C(1) << 31) 58 | #define PB64_AVX512VBMI_BIT_LVL7_ECX (UINT32_C(1) << 1) 59 | #define PB64_OSXSAVE_BIT_LVL1_ECX (UINT32_C(1) << 27) 60 | 61 | #define PB64_XCR0_SSE_BIT (UINT64_C(1) << 1) 62 | #define PB64_XCR0_AVX_BIT (UINT64_C(1) << 2) 63 | #define PB64_XCR0_OPMASK_BIT (UINT64_C(1) << 5) 64 | #define PB64_XCR0_ZMM_HI256_BIT (UINT64_C(1) << 6) 65 | #define PB64_XCR0_HI16_ZMM_BIT (UINT64_C(1) << 7) 66 | 67 | #define PB64_XCR0_AVX_SUPPORT_MASK (PB64_XCR0_SSE_BIT | PB64_XCR0_AVX_BIT) 68 | #define PB64_XCR0_AVX512_SUPPORT_MASK (PB64_XCR0_AVX_SUPPORT_MASK | PB64_XCR0_OPMASK_BIT | PB64_XCR0_ZMM_HI256_BIT | PB64_XCR0_HI16_ZMM_BIT) 69 | 70 | #define PB64_CHECK(reg_, bits_) (((reg_) & (bits_)) == (bits_)) 71 | 72 | uint32_t pybase64_get_simd_flags(void) 73 | { 74 | uint32_t result = 0U; 75 | uint32_t eax, ebx, ecx, edx; 76 | uint32_t max_level; 77 | uint64_t xcr0 = 0U; 78 | 79 | /* get max level */ 80 | cpuid_(0U, 0U, &max_level, &ebx, &ecx, &edx); 81 | 82 | if (max_level >= 1) { 83 | cpuid_(1U, 0U, &eax, &ebx, &ecx, &edx); 84 | if (PB64_CHECK(edx, PB64_SSE2_BIT_LVL1_EDX)) { 85 | result |= PYBASE64_SSE2; 86 | } 87 | if (PB64_CHECK(ecx, PB64_SSE3_BIT_LVL1_ECX)) { 88 | result |= PYBASE64_SSE3; 89 | } 90 | if (PB64_CHECK(ecx, PB64_SSSE3_BIT_LVL1_ECX)) { 91 | result |= PYBASE64_SSSE3; 92 | } 93 | if (PB64_CHECK(ecx, PB64_SSE41_BIT_LVL1_ECX)) { 94 | result |= PYBASE64_SSE41; 95 | } 96 | if (PB64_CHECK(ecx, PB64_SSE42_BIT_LVL1_ECX)) { 97 | result |= PYBASE64_SSE42; 98 | } 99 | 100 | if (PB64_CHECK(ecx, PB64_OSXSAVE_BIT_LVL1_ECX)) { /* OSXSAVE (implies XSAVE/XRESTOR/XGETBV) */ 101 | xcr0 = _xgetbv(0U /* XFEATURE_ENABLED_MASK/XCR0 */); 102 | 103 | if (PB64_CHECK(xcr0, PB64_XCR0_AVX_SUPPORT_MASK)) { /* XMM/YMM saved by OS */ 104 | if (ecx & PB64_AVX_BIT_LVL1_ECX) { 105 | result |= PYBASE64_AVX; 106 | } 107 | } 108 | } 109 | } 110 | 111 | if (max_level >= 7) { 112 | cpuid_(7U, 0U, &eax, &ebx, &ecx, &edx); 113 | if (result & PYBASE64_AVX) { /* check AVX supported for YMM saved by OS */ 114 | if (PB64_CHECK(ebx, PB64_AVX2_BIT_LVL7_EBX)) { 115 | result |= PYBASE64_AVX2; 116 | } 117 | if (PB64_CHECK(xcr0, PB64_XCR0_AVX512_SUPPORT_MASK)) {/* OpMask/ZMM/ZMM16-31 saved by OS */ 118 | if (PB64_CHECK(ebx, PB64_AVX512F_BIT_LVL7_EBX) && PB64_CHECK(ebx, PB64_AVX512VL_BIT_LVL7_EBX) && PB64_CHECK(ecx, PB64_AVX512VBMI_BIT_LVL7_ECX)) { 119 | result |= PYBASE64_AVX512VBMI; 120 | } 121 | } 122 | } 123 | } 124 | 125 | return result; 126 | } 127 | #else 128 | /* default version */ 129 | uint32_t pybase64_get_simd_flags(void) 130 | { 131 | #if BASE64_WITH_NEON64 || defined(__ARM_NEON__) || defined(__ARM_NEON) 132 | return PYBASE64_NEON; 133 | #endif 134 | return 0U; 135 | } 136 | #endif 137 | -------------------------------------------------------------------------------- /src/pybase64/_pybase64_get_simd_flags.h: -------------------------------------------------------------------------------- 1 | #define PYBASE64_NONE 0x00000000 2 | 3 | #define PYBASE64_SSE2 0x00000001 4 | #define PYBASE64_SSE3 0x00000002 5 | #define PYBASE64_SSSE3 0x00000004 6 | #define PYBASE64_SSE41 0x00000008 7 | #define PYBASE64_SSE42 0x00000010 8 | #define PYBASE64_AVX 0x00000020 9 | #define PYBASE64_AVX2 0x00000040 10 | #define PYBASE64_AVX512VBMI 0x00000080 11 | 12 | #define PYBASE64_NEON 0x00010000 13 | 14 | #include 15 | 16 | uint32_t pybase64_get_simd_flags(void); 17 | -------------------------------------------------------------------------------- /src/pybase64/_typing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from typing import Protocol 5 | 6 | if sys.version_info < (3, 12): 7 | from typing_extensions import Buffer 8 | else: 9 | from collections.abc import Buffer 10 | 11 | 12 | class Decode(Protocol): 13 | __name__: str 14 | __module__: str 15 | 16 | def __call__( 17 | self, s: str | Buffer, altchars: str | Buffer | None = None, validate: bool = False 18 | ) -> bytes: ... 19 | 20 | 21 | class Encode(Protocol): 22 | __name__: str 23 | __module__: str 24 | 25 | def __call__(self, s: Buffer, altchars: Buffer | None = None) -> bytes: ... 26 | 27 | 28 | class EncodeBytes(Protocol): 29 | __name__: str 30 | __module__: str 31 | 32 | def __call__(self, s: Buffer) -> bytes: ... 33 | 34 | 35 | __all__ = ("Buffer", "Decode", "Encode", "EncodeBytes") 36 | -------------------------------------------------------------------------------- /src/pybase64/_version.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | _version = "1.4.1" 4 | -------------------------------------------------------------------------------- /src/pybase64/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayeut/pybase64/5addc85d3d5e777a07e8e4211dfe63c02db42d3f/src/pybase64/py.typed -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayeut/pybase64/5addc85d3d5e777a07e8e4211dfe63c02db42d3f/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from . import utils 6 | 7 | simd = utils.simd 8 | 9 | 10 | @pytest.fixture(autouse=True) 11 | def _autoskip_benchmark(request: pytest.FixtureRequest): 12 | marker = request.node.get_closest_marker("benchmark") 13 | benchmark_running = request.config.getoption("--codspeed", default=False) 14 | if marker is not None and not benchmark_running: 15 | pytest.skip("needs '--codspeed' to run") 16 | -------------------------------------------------------------------------------- /tests/test_benchmark.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import pybase64 4 | 5 | from . import utils 6 | 7 | pytestmark = pytest.mark.benchmark 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def encode_data_full(request: pytest.FixtureRequest) -> bytes: 12 | if not request.config.getoption("--codspeed", default=False): 13 | pytest.skip("needs '--codspeed' to run") 14 | data_ = bytearray(i % 256 for i in range(1024 * 1024)) 15 | data = bytearray(512 * 1024 * 1024) 16 | chunk_start = 0 17 | chunk_end = len(data_) 18 | while chunk_start < len(data): 19 | data[chunk_start:chunk_end] = data_ 20 | chunk_start += len(data_) 21 | chunk_end += len(data_) 22 | return bytes(data) 23 | 24 | 25 | @pytest.fixture(scope="module", params=(1, 1024, 1024 * 1024, 512 * 1024 * 1024)) 26 | def encode_data(request: pytest.FixtureRequest, encode_data_full: bytes) -> bytes: 27 | return encode_data_full[: request.param] 28 | 29 | 30 | @pytest.fixture(scope="module") 31 | def decode_data(encode_data: bytes) -> bytes: 32 | return pybase64.b64encode(encode_data) 33 | 34 | 35 | @utils.param_simd 36 | def test_encoding(simd: int, encode_data: bytearray) -> None: 37 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 38 | pybase64.b64encode(encode_data) 39 | 40 | 41 | @utils.param_simd 42 | def test_decoding(simd: int, decode_data: bytearray) -> None: 43 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 44 | pybase64.b64decode(decode_data, validate=True) 45 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | import sys 5 | from collections.abc import Iterator, Sequence 6 | from pathlib import Path 7 | 8 | import pytest 9 | 10 | import pybase64 11 | from pybase64.__main__ import main 12 | 13 | 14 | @pytest.fixture 15 | def emptyfile(tmp_path: Path) -> Iterator[str]: 16 | _file = tmp_path / "empty" 17 | _file.write_bytes(b"") 18 | yield str(_file) 19 | _file.unlink() 20 | 21 | 22 | @pytest.fixture 23 | def hellofile(tmp_path: Path) -> Iterator[str]: 24 | _file = tmp_path / "helloworld" 25 | _file.write_bytes(b"hello world !/?\n") 26 | yield str(_file) 27 | _file.unlink() 28 | 29 | 30 | def idfn_test_help(args: Sequence[str]) -> str: 31 | if len(args) == 0: 32 | return "(empty)" 33 | return " ".join(args) 34 | 35 | 36 | @pytest.mark.parametrize( 37 | "args", 38 | [ 39 | [], 40 | ["-h"], 41 | ["benchmark", "-h"], 42 | ["encode", "-h"], 43 | ["decode", "-h"], 44 | ], 45 | ids=idfn_test_help, 46 | ) 47 | def test_help(capsys: pytest.CaptureFixture[str], args: Sequence[str]) -> None: 48 | command = "pybase64" 49 | if len(args) == 2: 50 | command += f" {args[0]}" 51 | usage = f"usage: {command} [-h]" 52 | with pytest.raises(SystemExit) as exit_info: 53 | main(args) 54 | captured = capsys.readouterr() 55 | assert captured.err == "" 56 | assert captured.out.startswith(usage) 57 | assert exit_info.value.code == 0 58 | 59 | 60 | def test_version(capsys: pytest.CaptureFixture[str]) -> None: 61 | with pytest.raises(SystemExit) as exit_info: 62 | main(["-V"]) 63 | captured = capsys.readouterr() 64 | assert captured.err == "" 65 | assert captured.out.startswith("pybase64 " + pybase64.__version__) 66 | assert exit_info.value.code == 0 67 | 68 | 69 | def test_license(capsys: pytest.CaptureFixture[str]) -> None: 70 | restr = "\n".join(x + "\n[=]+\n.*Copyright.*\n[=]+\n" for x in ["pybase64", "libbase64"]) 71 | regex = re.compile("^" + restr + "$", re.DOTALL) 72 | with pytest.raises(SystemExit) as exit_info: 73 | main(["--license"]) 74 | captured = capsys.readouterr() 75 | assert captured.err == "" 76 | assert regex.match(captured.out) 77 | assert exit_info.value.code == 0 78 | 79 | 80 | def test_benchmark(capsys: pytest.CaptureFixture[str], emptyfile: str) -> None: 81 | main(["benchmark", "-d", "0.005", emptyfile]) 82 | captured = capsys.readouterr() 83 | assert captured.err == "" 84 | assert captured.out != "" 85 | 86 | 87 | @pytest.mark.parametrize( 88 | ("args", "expect"), 89 | [ 90 | ([], b"aGVsbG8gd29ybGQgIS8/Cg=="), 91 | (["-u"], b"aGVsbG8gd29ybGQgIS8_Cg=="), 92 | (["-a", ":,"], b"aGVsbG8gd29ybGQgIS8,Cg=="), 93 | ], 94 | ids=["0", "1", "2"], 95 | ) 96 | def test_encode( 97 | capsysbinary: pytest.CaptureFixture[bytes], hellofile: str, args: Sequence[str], expect: bytes 98 | ) -> None: 99 | main(["encode", *args, hellofile]) 100 | captured = capsysbinary.readouterr() 101 | assert captured.err == b"" 102 | assert captured.out == expect 103 | 104 | 105 | def test_encode_ouputfile( 106 | capsys: pytest.CaptureFixture[str], emptyfile: str, hellofile: str 107 | ) -> None: 108 | main(["encode", "-o", hellofile, emptyfile]) 109 | captured = capsys.readouterr() 110 | assert captured.err == "" 111 | assert captured.out == "" 112 | with open(hellofile, "rb") as f: 113 | data = f.read() 114 | assert data == b"" 115 | 116 | 117 | @pytest.mark.parametrize( 118 | ("args", "b64string"), 119 | [ 120 | ([], b"aGVsbG8gd29ybGQgIS8/Cg=="), 121 | (["-u"], b"aGVsbG8gd29ybGQgIS8_Cg=="), 122 | (["-a", ":,"], b"aGVsbG8gd29ybGQgIS8,Cg=="), 123 | (["--no-validation"], b"aGVsbG8gd29yb GQgIS8/Cg==\n"), 124 | ], 125 | ids=["0", "1", "2", "3"], 126 | ) 127 | def test_decode( 128 | capsysbinary: pytest.CaptureFixture[bytes], 129 | tmp_path: Path, 130 | args: Sequence[str], 131 | b64string: bytes, 132 | ) -> None: 133 | input_file = tmp_path / "in" 134 | input_file.write_bytes(b64string) 135 | main(["decode", *args, str(input_file)]) 136 | captured = capsysbinary.readouterr() 137 | assert captured.err == b"" 138 | assert captured.out == b"hello world !/?\n" 139 | 140 | 141 | @pytest.mark.skipif( 142 | sys.platform.startswith(("emscripten", "ios")), reason="subprocess not supported" 143 | ) 144 | def test_subprocess() -> None: 145 | import subprocess 146 | 147 | process = subprocess.Popen( 148 | [sys.executable, "-m", "pybase64", "encode", "-"], 149 | bufsize=4096, 150 | stdin=subprocess.PIPE, 151 | stdout=subprocess.PIPE, 152 | stderr=subprocess.PIPE, 153 | ) 154 | out, err = process.communicate() 155 | process.wait() 156 | assert process.returncode == 0 157 | assert out == b"" 158 | assert err == b"" 159 | -------------------------------------------------------------------------------- /tests/test_pybase64.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import base64 4 | import sys 5 | from base64 import encodebytes as b64encodebytes 6 | from binascii import Error as BinAsciiError 7 | from collections.abc import Callable 8 | from enum import IntEnum 9 | from typing import Any 10 | 11 | import pytest 12 | 13 | import pybase64 14 | from pybase64._typing import Buffer, Decode, Encode 15 | 16 | from . import utils 17 | 18 | 19 | def b64encode_as_string(s: Buffer, altchars: str | Buffer | None = None) -> bytes: 20 | """helper returning bytes instead of string for tests""" 21 | return pybase64.b64encode_as_string(s, altchars).encode("ascii") 22 | 23 | 24 | def b64decode_as_bytearray( 25 | s: str | Buffer, altchars: str | Buffer | None = None, validate: bool = False 26 | ) -> bytes: 27 | """helper returning bytes instead of bytearray for tests""" 28 | return bytes(pybase64.b64decode_as_bytearray(s, altchars, validate)) 29 | 30 | 31 | param_encode_functions = pytest.mark.parametrize("efn", [pybase64.b64encode, b64encode_as_string]) 32 | param_decode_functions = pytest.mark.parametrize( 33 | "dfn", [pybase64.b64decode, b64decode_as_bytearray] 34 | ) 35 | 36 | 37 | class AltCharsId(IntEnum): 38 | STD = 0 39 | URL = 1 40 | ALT1 = 2 41 | ALT2 = 3 42 | ALT3 = 4 43 | 44 | 45 | altchars_lut = [b"+/", b"-_", b"@&", b"+,", b";/"] 46 | enc_helper_lut: list[Callable[[Buffer], bytes]] = [ 47 | pybase64.standard_b64encode, 48 | pybase64.urlsafe_b64encode, 49 | ] 50 | ref_enc_helper_lut: list[Callable[[Buffer], bytes]] = [ 51 | base64.standard_b64encode, 52 | base64.urlsafe_b64encode, 53 | ] 54 | dec_helper_lut: list[Callable[[str | Buffer], bytes]] = [ 55 | pybase64.standard_b64decode, 56 | pybase64.urlsafe_b64decode, 57 | ] 58 | ref_dec_helper_lut: list[Callable[[str | Buffer], bytes]] = [ 59 | base64.standard_b64decode, 60 | base64.urlsafe_b64decode, 61 | ] 62 | 63 | std = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/A" 64 | std = std * (32 * 16) 65 | std_len_minus_12 = len(std) - 12 66 | 67 | test_vectors_b64_list = [ 68 | # rfc4648 test vectors 69 | b"", 70 | b"Zg==", 71 | b"Zm8=", 72 | b"Zm9v", 73 | b"Zm9vYg==", 74 | b"Zm9vYmE=", 75 | b"Zm9vYmFy", 76 | # custom test vectors 77 | std[std_len_minus_12:], 78 | std, 79 | std[:72], 80 | std[:76], 81 | std[:80], 82 | std[:148], 83 | std[:152], 84 | std[:156], 85 | ] 86 | 87 | test_vectors_b64 = [] 88 | for altchars in altchars_lut: 89 | trans = bytes.maketrans(b"+/", altchars) 90 | test_vectors_b64.append([vector.translate(trans) for vector in test_vectors_b64_list]) 91 | 92 | test_vectors_bin = [] 93 | for altchars in altchars_lut: 94 | test_vectors_bin.append( 95 | [base64.b64decode(vector, altchars) for vector in test_vectors_b64_list] 96 | ) 97 | 98 | 99 | param_vector = pytest.mark.parametrize("vector_id", range(len(test_vectors_bin[0]))) 100 | 101 | 102 | param_validate = pytest.mark.parametrize("validate", [False, True], ids=["novalidate", "validate"]) 103 | 104 | 105 | param_altchars = pytest.mark.parametrize("altchars_id", list(AltCharsId), ids=lambda x: x.name) 106 | 107 | 108 | param_altchars_helper = pytest.mark.parametrize( 109 | "altchars_id", [AltCharsId.STD, AltCharsId.URL], ids=lambda x: x.name 110 | ) 111 | 112 | 113 | @utils.param_simd 114 | def test_version(simd: int) -> None: 115 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 116 | assert pybase64.get_version().startswith(pybase64.__version__) 117 | 118 | 119 | @utils.param_simd 120 | @param_vector 121 | @param_altchars_helper 122 | def test_enc_helper(altchars_id: int, vector_id: int, simd: int) -> None: 123 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 124 | vector = test_vectors_bin[altchars_id][vector_id] 125 | test = enc_helper_lut[altchars_id](vector) 126 | base = ref_enc_helper_lut[altchars_id](vector) 127 | assert test == base 128 | 129 | 130 | @utils.param_simd 131 | @param_vector 132 | @param_altchars_helper 133 | def test_dec_helper(altchars_id: int, vector_id: int, simd: int) -> None: 134 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 135 | vector = test_vectors_b64[altchars_id][vector_id] 136 | test = dec_helper_lut[altchars_id](vector) 137 | base = ref_dec_helper_lut[altchars_id](vector) 138 | assert test == base 139 | 140 | 141 | @utils.param_simd 142 | @param_vector 143 | @param_altchars_helper 144 | def test_dec_helper_unicode(altchars_id: int, vector_id: int, simd: int) -> None: 145 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 146 | vector = test_vectors_b64[altchars_id][vector_id] 147 | test = dec_helper_lut[altchars_id](str(vector, "utf-8")) 148 | base = ref_dec_helper_lut[altchars_id](str(vector, "utf-8")) 149 | assert test == base 150 | 151 | 152 | @utils.param_simd 153 | @param_vector 154 | @param_altchars_helper 155 | def test_rnd_helper(altchars_id: int, vector_id: int, simd: int) -> None: 156 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 157 | vector = test_vectors_b64[altchars_id][vector_id] 158 | test = dec_helper_lut[altchars_id](vector) 159 | test = enc_helper_lut[altchars_id](test) 160 | assert test == vector 161 | 162 | 163 | @utils.param_simd 164 | @param_vector 165 | @param_altchars_helper 166 | def test_rnd_helper_unicode(altchars_id: int, vector_id: int, simd: int) -> None: 167 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 168 | vector = test_vectors_b64[altchars_id][vector_id] 169 | test = dec_helper_lut[altchars_id](str(vector, "utf-8")) 170 | test = enc_helper_lut[altchars_id](test) 171 | assert test == vector 172 | 173 | 174 | @utils.param_simd 175 | @param_vector 176 | def test_encbytes(vector_id: int, simd: int) -> None: 177 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 178 | vector = test_vectors_bin[AltCharsId.STD][vector_id] 179 | test = pybase64.encodebytes(vector) 180 | base = b64encodebytes(vector) 181 | assert test == base 182 | 183 | 184 | @utils.param_simd 185 | @param_vector 186 | @param_altchars 187 | @param_encode_functions 188 | def test_enc(efn: Encode, altchars_id: int, vector_id: int, simd: int) -> None: 189 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 190 | vector = test_vectors_bin[altchars_id][vector_id] 191 | altchars = altchars_lut[altchars_id] 192 | test = efn(vector, altchars) 193 | base = base64.b64encode(vector, altchars) 194 | assert test == base 195 | 196 | 197 | @utils.param_simd 198 | @param_vector 199 | @param_altchars 200 | @param_validate 201 | @param_decode_functions 202 | def test_dec(dfn: Decode, altchars_id: int, vector_id: int, validate: bool, simd: int) -> None: 203 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 204 | vector = test_vectors_b64[altchars_id][vector_id] 205 | altchars = altchars_lut[altchars_id] 206 | if validate: 207 | base = base64.b64decode(vector, altchars, validate) 208 | else: 209 | base = base64.b64decode(vector, altchars) 210 | test = dfn(vector, altchars, validate) 211 | assert test == base 212 | 213 | 214 | @utils.param_simd 215 | @param_vector 216 | @param_altchars 217 | @param_validate 218 | @param_decode_functions 219 | def test_dec_unicode( 220 | dfn: Decode, altchars_id: int, vector_id: int, validate: bool, simd: int 221 | ) -> None: 222 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 223 | vector = str(test_vectors_b64[altchars_id][vector_id], "utf-8") 224 | altchars = None if altchars_id == AltCharsId.STD else str(altchars_lut[altchars_id], "utf-8") 225 | if validate: 226 | base = base64.b64decode(vector, altchars, validate) 227 | else: 228 | base = base64.b64decode(vector, altchars) 229 | test = dfn(vector, altchars, validate) 230 | assert test == base 231 | 232 | 233 | @utils.param_simd 234 | @param_vector 235 | @param_altchars 236 | @param_validate 237 | @param_encode_functions 238 | @param_decode_functions 239 | def test_rnd( 240 | dfn: Decode, efn: Encode, altchars_id: int, vector_id: int, validate: bool, simd: int 241 | ) -> None: 242 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 243 | vector = test_vectors_b64[altchars_id][vector_id] 244 | altchars = altchars_lut[altchars_id] 245 | test = dfn(vector, altchars, validate) 246 | test = efn(test, altchars) 247 | assert test == vector 248 | 249 | 250 | @utils.param_simd 251 | @param_vector 252 | @param_altchars 253 | @param_validate 254 | @param_encode_functions 255 | @param_decode_functions 256 | def test_rnd_unicode( 257 | dfn: Decode, efn: Encode, altchars_id: int, vector_id: int, validate: bool, simd: int 258 | ) -> None: 259 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 260 | vector = test_vectors_b64[altchars_id][vector_id] 261 | altchars = altchars_lut[altchars_id] 262 | test = dfn(str(vector, "utf-8"), altchars, validate) 263 | test = efn(test, altchars) 264 | assert test == vector 265 | 266 | 267 | @utils.param_simd 268 | @param_vector 269 | @param_altchars 270 | @param_validate 271 | @param_decode_functions 272 | def test_invalid_padding_dec( 273 | dfn: Decode, altchars_id: int, vector_id: int, validate: bool, simd: int 274 | ) -> None: 275 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 276 | vector = test_vectors_b64[altchars_id][vector_id][1:] 277 | if len(vector) > 0: 278 | altchars = altchars_lut[altchars_id] 279 | with pytest.raises(BinAsciiError): 280 | dfn(vector, altchars, validate) 281 | 282 | 283 | params_invalid_altchars_values = [ 284 | [b"", AssertionError], 285 | [b"-", AssertionError], 286 | [b"-__", AssertionError], 287 | [3.0, TypeError], 288 | ["-€", ValueError], 289 | [memoryview(b"- _")[::2], BufferError], 290 | ] 291 | params_invalid_altchars = pytest.mark.parametrize( 292 | ("altchars", "exception"), 293 | params_invalid_altchars_values, 294 | ids=[str(i) for i in range(len(params_invalid_altchars_values))], 295 | ) 296 | 297 | 298 | @utils.param_simd 299 | @params_invalid_altchars 300 | @param_encode_functions 301 | def test_invalid_altchars_enc( 302 | efn: Encode, altchars: Any, exception: type[BaseException], simd: int 303 | ) -> None: 304 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 305 | with pytest.raises(exception): 306 | efn(b"ABCD", altchars) 307 | 308 | 309 | @utils.param_simd 310 | @params_invalid_altchars 311 | @param_decode_functions 312 | def test_invalid_altchars_dec( 313 | dfn: Decode, altchars: Any, exception: type[BaseException], simd: int 314 | ) -> None: 315 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 316 | with pytest.raises(exception): 317 | dfn(b"ABCD", altchars) 318 | 319 | 320 | @utils.param_simd 321 | @params_invalid_altchars 322 | @param_decode_functions 323 | def test_invalid_altchars_dec_validate( 324 | dfn: Decode, altchars: Any, exception: type[BaseException], simd: int 325 | ) -> None: 326 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 327 | with pytest.raises(exception): 328 | dfn(b"ABCD", altchars, True) 329 | 330 | 331 | params_invalid_data_novalidate_values = [ 332 | [b"A@@@@FG", None, BinAsciiError], 333 | ["ABC€", None, ValueError], 334 | [3.0, None, TypeError], 335 | [memoryview(b"ABCDEFGH")[::2], None, BufferError], 336 | ] 337 | params_invalid_data_validate_values = [ 338 | [b"\x00\x00\x00\x00", None, BinAsciiError], 339 | [b"A@@@@FGHIJKLMNOPQRSTUVWXYZabcdef", b"-_", BinAsciiError], 340 | [b"A@@@=FGHIJKLMNOPQRSTUVWXYZabcdef", b"-_", BinAsciiError], 341 | [b"A@@=@FGHIJKLMNOPQRSTUVWXYZabcdef", b"-_", BinAsciiError], 342 | [b"A@@@@FGHIJKLMNOPQRSTUVWXYZabcde@=", b"-_", BinAsciiError], 343 | [b"A@@@@FGHIJKLMNOPQRSTUVWXYZabcd@==", b"-_", BinAsciiError], 344 | [b"A@@@@FGH" * 10000, b"-_", BinAsciiError], 345 | # [std, b'-_', BinAsciiError], TODO does no fail with base64 module 346 | ] 347 | params_invalid_data_all = pytest.mark.parametrize( 348 | ("vector", "altchars", "exception"), 349 | params_invalid_data_novalidate_values + params_invalid_data_validate_values, 350 | ids=[ 351 | str(i) 352 | for i in range( 353 | len(params_invalid_data_novalidate_values) + len(params_invalid_data_validate_values) 354 | ) 355 | ], 356 | ) 357 | params_invalid_data_novalidate = pytest.mark.parametrize( 358 | ("vector", "altchars", "exception"), 359 | params_invalid_data_novalidate_values, 360 | ids=[str(i) for i in range(len(params_invalid_data_novalidate_values))], 361 | ) 362 | params_invalid_data_validate = pytest.mark.parametrize( 363 | ("vector", "altchars", "exception"), 364 | params_invalid_data_validate_values, 365 | ids=[str(i) for i in range(len(params_invalid_data_validate_values))], 366 | ) 367 | 368 | 369 | @utils.param_simd 370 | @params_invalid_data_novalidate 371 | @param_decode_functions 372 | def test_invalid_data_dec( 373 | dfn: Decode, vector: Any, altchars: Buffer | None, exception: type[BaseException], simd: int 374 | ) -> None: 375 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 376 | with pytest.raises(exception): 377 | dfn(vector, altchars) 378 | 379 | 380 | @utils.param_simd 381 | @params_invalid_data_validate 382 | @param_decode_functions 383 | def test_invalid_data_dec_skip( 384 | dfn: Decode, vector: Any, altchars: Buffer | None, exception: type[BaseException], simd: int 385 | ) -> None: 386 | utils.unused_args(exception, simd) # simd is a parameter in order to control the order of tests 387 | test = dfn(vector, altchars) 388 | if sys.implementation.name == "graalpy" and vector.startswith((b"A@@@=F", b"A@@=@")): 389 | pytest.xfail(reason="graalpy fails decoding those entries") # pragma: no cover 390 | base = base64.b64decode(vector, altchars) 391 | assert test == base 392 | 393 | 394 | @utils.param_simd 395 | @params_invalid_data_all 396 | @param_decode_functions 397 | def test_invalid_data_dec_validate( 398 | dfn: Decode, vector: Any, altchars: Buffer | None, exception: type[BaseException], simd: int 399 | ) -> None: 400 | utils.unused_args(simd) # simd is a parameter in order to control the order of tests 401 | with pytest.raises(exception): 402 | dfn(vector, altchars, True) 403 | 404 | 405 | params_invalid_data_enc_values = [ 406 | ["this is a test", TypeError], 407 | [memoryview(b"abcd")[::2], BufferError], 408 | ] 409 | params_invalid_data_encodebytes_values = [ 410 | *params_invalid_data_enc_values, 411 | [memoryview(b"abcd").cast("B", (2, 2)), TypeError], 412 | [memoryview(b"abcd").cast("I"), TypeError], 413 | ] 414 | params_invalid_data_enc = pytest.mark.parametrize( 415 | ("vector", "exception"), 416 | params_invalid_data_enc_values, 417 | ids=[str(i) for i in range(len(params_invalid_data_enc_values))], 418 | ) 419 | params_invalid_data_encodebytes = pytest.mark.parametrize( 420 | ("vector", "exception"), 421 | params_invalid_data_encodebytes_values, 422 | ids=[str(i) for i in range(len(params_invalid_data_encodebytes_values))], 423 | ) 424 | 425 | 426 | @params_invalid_data_enc 427 | @param_encode_functions 428 | def test_invalid_data_enc(efn: Encode, vector: Any, exception: type[BaseException]) -> None: 429 | with pytest.raises(exception): 430 | efn(vector) 431 | 432 | 433 | @params_invalid_data_encodebytes 434 | def test_invalid_data_encodebytes(vector: Any, exception: type[BaseException]) -> None: 435 | with pytest.raises(exception): 436 | pybase64.encodebytes(vector) 437 | 438 | 439 | @param_encode_functions 440 | def test_invalid_args_enc_0(efn: Encode) -> None: 441 | with pytest.raises(TypeError): 442 | efn() # type: ignore[call-arg] 443 | 444 | 445 | @param_decode_functions 446 | def test_invalid_args_dec_0(dfn: Decode) -> None: 447 | with pytest.raises(TypeError): 448 | dfn() # type: ignore[call-arg] 449 | 450 | 451 | def test_flags(request: pytest.FixtureRequest) -> None: 452 | cpu = request.config.getoption("--sde-cpu", skip=True) 453 | assert { 454 | "p4p": 1 | 2, # SSE3 455 | "mrm": 1 | 2 | 4, # SSSE3 456 | "pnr": 1 | 2 | 4 | 8, # SSE41 457 | "nhm": 1 | 2 | 4 | 8 | 16, # SSE42 458 | "snb": 1 | 2 | 4 | 8 | 16 | 32, # AVX 459 | "hsw": 1 | 2 | 4 | 8 | 16 | 32 | 64, # AVX2 460 | "spr": 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, # AVX512VBMI 461 | }[cpu] == utils.runtime_flags 462 | 463 | 464 | @param_encode_functions 465 | def test_enc_multi_dimensional(efn: Encode) -> None: 466 | source = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV" 467 | vector = memoryview(source).cast("B", (4, len(source) // 4)) 468 | assert vector.c_contiguous 469 | test = efn(vector, None) 470 | base = base64.b64encode(source) 471 | assert test == base 472 | 473 | 474 | @param_decode_functions 475 | def test_dec_multi_dimensional(dfn: Decode) -> None: 476 | source = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV" 477 | vector = memoryview(source).cast("B", (4, len(source) // 4)) 478 | assert vector.c_contiguous 479 | test = dfn(vector, None) 480 | base = base64.b64decode(source) 481 | assert test == base 482 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from collections.abc import Iterator 5 | from typing import Any 6 | 7 | import pytest 8 | 9 | import pybase64 10 | 11 | _has_extension = hasattr(pybase64, "_set_simd_path") 12 | assert _has_extension or os.environ.get("CIBUILDWHEEL", "0") == "0" 13 | 14 | compile_flags = [0] 15 | runtime_flags = 0 16 | if _has_extension: 17 | runtime_flags = pybase64._get_simd_flags_runtime() # type: ignore[attr-defined] 18 | flags = pybase64._get_simd_flags_compile() # type: ignore[attr-defined] 19 | for i in range(31): 20 | if flags & (1 << i): 21 | compile_flags += [(1 << i)] 22 | 23 | 24 | def _get_simd_name(simd_id: int) -> str: 25 | if _has_extension: 26 | simd_flag = compile_flags[simd_id] 27 | simd_name = "C" if simd_flag == 0 else pybase64._get_simd_name(simd_flag) # type: ignore[attr-defined] 28 | else: 29 | simd_name = "PY" 30 | return simd_name 31 | 32 | 33 | param_simd = pytest.mark.parametrize( 34 | "simd", range(len(compile_flags)), ids=lambda x: _get_simd_name(x), indirect=True 35 | ) 36 | 37 | 38 | @pytest.fixture 39 | def simd(request: pytest.FixtureRequest) -> Iterator[int]: 40 | simd_id = request.param 41 | if not _has_extension: 42 | assert simd_id == 0 43 | yield simd_id 44 | return 45 | 46 | flag = compile_flags[simd_id] 47 | if flag != 0 and not flag & runtime_flags: # pragma: no branch 48 | simd_name = _get_simd_name(simd_id) # pragma: no cover 49 | pytest.skip(f"{simd_name!r} SIMD extension not available") # pragma: no cover 50 | old_flag = pybase64._get_simd_path() # type: ignore[attr-defined] 51 | pybase64._set_simd_path(flag) # type: ignore[attr-defined] 52 | assert pybase64._get_simd_path() == flag # type: ignore[attr-defined] 53 | yield simd_id 54 | pybase64._set_simd_path(old_flag) # type: ignore[attr-defined] 55 | 56 | 57 | def unused_args(*args: Any) -> None: # noqa: ARG001 58 | return None 59 | --------------------------------------------------------------------------------