|
| 1 | +# bitmath Architecture |
| 2 | + |
| 3 | +> This document exists to satisfy the OSPS Baseline Level 2 control |
| 4 | +> [OSPS-SA-01.01](https://www.bestpractices.dev/en/projects/12749/baseline-2) |
| 5 | +> (design documentation showing the actions and actors within the |
| 6 | +> system), in addition to being useful for downstream consumers and |
| 7 | +> contributors who want to understand how bitmath fits together. |
| 8 | +
|
| 9 | +## Contents |
| 10 | + |
| 11 | +- [Why this document exists](#why-this-document-exists) |
| 12 | +- [Runtime architecture](#runtime-architecture) |
| 13 | +- [Build and release pipeline](#build-and-release-pipeline) |
| 14 | +- [CI and quality gates](#ci-and-quality-gates) |
| 15 | +- [Trust boundaries](#trust-boundaries) |
| 16 | +- [Documentation pipeline](#documentation-pipeline) |
| 17 | +- [Maintenance](#maintenance) |
| 18 | + |
| 19 | +## Why this document exists |
| 20 | + |
| 21 | +This is the architecture map for bitmath. It exists so downstream |
| 22 | +consumers and future contributors can see, in one place, what the |
| 23 | +moving parts are and how they fit together. It covers two things: the |
| 24 | +runtime library (what runs in your process when you `import bitmath`), |
| 25 | +and the build and release pipeline (how that code gets from this |
| 26 | +repository onto your machine). |
| 27 | + |
| 28 | +It does not cover everything in the repository. Other concerns have |
| 29 | +their own homes: |
| 30 | + |
| 31 | +* [MAINTAINERS.md](MAINTAINERS.md): who has access to what. |
| 32 | +* [SECURITY.md](SECURITY.md): how to report a vulnerability. |
| 33 | +* [SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md): the standing |
| 34 | + threat model. |
| 35 | +* [bitmath on Read The Docs](https://bitmath.readthedocs.io/): API |
| 36 | + reference and tutorials. |
| 37 | + |
| 38 | +If you are wondering whether to update this file: yes, if you are |
| 39 | +adding a new public function, restructuring the class hierarchy, |
| 40 | +changing the build backend, or changing how releases get published. |
| 41 | +No, if you are fixing a bug or adding a test. |
| 42 | + |
| 43 | +## Runtime architecture |
| 44 | + |
| 45 | +### Component inventory |
| 46 | + |
| 47 | +The runtime library is a single Python module, `bitmath`, deliberately |
| 48 | +kept small. Almost all the logic lives in `bitmath/__init__.py`. The |
| 49 | +actors that show up at runtime: |
| 50 | + |
| 51 | +| Actor | Lives at | What it does | |
| 52 | +|-------|----------|--------------| |
| 53 | +| User code | The consumer's process | Imports bitmath, calls module-level functions, instantiates unit classes, does arithmetic | |
| 54 | +| Public module API | `bitmath.*` | Module-level entry points: `parse_string`, `parse_string_unsafe`, `best_prefix`, `getsize`, `listdir` (deprecated in 2.1.0), `query_capacity`, `query_device_capacity` | |
| 55 | +| `Bitmath` base class | `bitmath.Bitmath` | The arithmetic, comparison, bitwise, and formatting engine. Every concrete unit class inherits from it | |
| 56 | +| `Byte` family | `bitmath.Byte`, `KiB`, `MiB`, `GiB`, `TiB`, `PiB`, `EiB`, `kB`, `MB`, `GB`, `TB`, `PB`, `EB`, `ZB`, `YB` | Byte-based concrete unit classes (NIST binary and SI decimal) | |
| 57 | +| `Bit` family | `bitmath.Bit`, `Kib`, `Mib`, `Gib`, `Tib`, `Pib`, `Eib`, `kb`, `Mb`, `Gb`, `Tb`, `Pb`, `Eb`, `Zb`, `Yb` | Bit-based concrete unit classes (NIST binary and SI decimal) | |
| 58 | +| Constants | `bitmath.NIST`, `bitmath.SI`, `bitmath.NIST_PREFIXES`, `bitmath.SI_PREFIXES`, `bitmath.ALL_UNIT_TYPES` | Module-level lookup tables and system identifiers | |
| 59 | +| OS abstraction layer | Standard library: `shutil`, `os`, `fcntl` (Linux), `ctypes` (Windows) | Platform-specific calls used by the capacity-query functions | |
| 60 | + |
| 61 | +### Layered view |
| 62 | + |
| 63 | +``` |
| 64 | ++--------------------------------------------------------------+ |
| 65 | +| User code | |
| 66 | +| import bitmath; KiB(1024).MiB | |
| 67 | ++--------------------------------------------------------------+ |
| 68 | + | |
| 69 | + v |
| 70 | ++--------------------------------------------------------------+ |
| 71 | +| Public module API (bitmath.*) | |
| 72 | +| parse_string, best_prefix, getsize, query_capacity, etc. | |
| 73 | ++--------------------------------------------------------------+ |
| 74 | + | |
| 75 | + v |
| 76 | ++--------------------------------------------------------------+ |
| 77 | +| Library core | |
| 78 | +| Bitmath base class | |
| 79 | +| Byte family (KiB, MiB, kB, MB, ...) | |
| 80 | +| Bit family (Kib, Mib, kb, Mb, ...) | |
| 81 | +| _norm_value: normalize every value to bits internally | |
| 82 | ++--------------------------------------------------------------+ |
| 83 | + | |
| 84 | + v (only for getsize / listdir / |
| 85 | + | query_capacity / |
| 86 | + | query_device_capacity) |
| 87 | ++--------------------------------------------------------------+ |
| 88 | +| OS abstraction layer | |
| 89 | +| shutil.disk_usage -- cross-platform capacity | |
| 90 | +| fcntl.ioctl -- Linux block device capacity | |
| 91 | +| ctypes / Win32 API -- Windows block device capacity | |
| 92 | +| macOS: NotImplementedError (System Integrity Protection) | |
| 93 | ++--------------------------------------------------------------+ |
| 94 | +``` |
| 95 | + |
| 96 | +### Actions |
| 97 | + |
| 98 | +The things that happen at runtime, in roughly the order a typical |
| 99 | +consumer triggers them: |
| 100 | + |
| 101 | +1. **Construction.** A user creates a bitmath instance (`KiB(1024)`, |
| 102 | + `Byte(bits=8192)`, `parse_string("4.5 GiB")`). The constructor |
| 103 | + passes the value through `_norm_value`, which converts everything |
| 104 | + to an internal bits representation using the class-level |
| 105 | + `_base_value` and `_unit_value` constants. After construction, the |
| 106 | + instance carries one canonical value. |
| 107 | + |
| 108 | +2. **Conversion.** A user requests a different unit |
| 109 | + (`KiB(1024).MiB`). Each unit class exposes properties that compute |
| 110 | + their own value from the internal bits representation. No |
| 111 | + conversion is cached; every property access does the math fresh. |
| 112 | + |
| 113 | +3. **Arithmetic.** Operators (`+`, `-`, `*`, `/`, `//`, `%`, |
| 114 | + `divmod`) operate on the internal bits representation and return |
| 115 | + a new instance. Mixed-unit arithmetic (`KiB(1) + MiB(1)`) |
| 116 | + resolves through the bits representation, so the result is |
| 117 | + unit-correct. |
| 118 | + |
| 119 | +4. **Comparison and bitwise.** Comparisons (`<`, `>`, `==`) and |
| 120 | + bitwise operations (`&`, `|`, `^`, `~`, `<<`, `>>`) work on the |
| 121 | + internal bits representation. |
| 122 | + |
| 123 | +5. **Formatting.** `str()`, `repr()`, and `format()` (including |
| 124 | + f-string usage) produce human-readable output. The `best_prefix` |
| 125 | + function picks the most readable unit for a given byte value. |
| 126 | + |
| 127 | +6. **Parsing.** `parse_string` and `parse_string_unsafe` accept a |
| 128 | + string like `"4.5 GiB"` and return a bitmath instance. The strict |
| 129 | + form raises on ambiguous unit; the unsafe form picks a default |
| 130 | + system (SI by default) when the unit is ambiguous. |
| 131 | + |
| 132 | +7. **Filesystem inspection.** `getsize` accepts a path or |
| 133 | + `pathlib.Path` and returns a bitmath instance for the file size. |
| 134 | + `listdir` (deprecated in 2.1.0) walks a tree and yields sizes. |
| 135 | + |
| 136 | +8. **Capacity querying.** `query_capacity(path)` returns a |
| 137 | + `Capacity(total, used, free)` named tuple of `Byte` instances. |
| 138 | + This uses `shutil.disk_usage` and works on every supported |
| 139 | + platform without elevated privileges. `query_device_capacity(fd)` |
| 140 | + returns the raw physical block-device capacity and requires |
| 141 | + elevated privileges on Linux (root) and Windows (administrator); |
| 142 | + macOS raises `NotImplementedError`. |
| 143 | + |
| 144 | +### Data flow |
| 145 | + |
| 146 | +Untrusted input enters the library at exactly three runtime places: |
| 147 | +the string parsers (`parse_string`, `parse_string_unsafe`), the |
| 148 | +filesystem helpers (`getsize`, `listdir`), and the capacity functions |
| 149 | +(`query_capacity`, `query_device_capacity`). For everything else the |
| 150 | +input is a Python number or another bitmath instance, both of which |
| 151 | +are inherently trusted. The trust boundaries section below covers |
| 152 | +what bitmath does and does not validate at each entry point. See |
| 153 | +[SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md) for the full threat |
| 154 | +model. |
| 155 | + |
| 156 | +## Build and release pipeline |
| 157 | + |
| 158 | +### Component inventory |
| 159 | + |
| 160 | +| Actor | Lives at | What it does | |
| 161 | +|-------|----------|--------------| |
| 162 | +| Source repository | github.com/timlnx/bitmath | Single source of truth for all code, docs, and pipeline configuration | |
| 163 | +| `VERSION` file | Repository root | Single source of the project version; read by Hatchling and the Makefile | |
| 164 | +| `pyproject.toml` | Repository root | PEP 517/518/621 project metadata; declares Hatchling as the build backend | |
| 165 | +| Hatchling | Build-time only | PyPA-blessed build backend; produces the wheel and sdist artifacts | |
| 166 | +| `Makefile` | Repository root | Local automation; targets like `make ci`, `make build`, `make docs` | |
| 167 | +| GitHub Actions runners | `.github/workflows/*.yml` | Hosted CI execution environment | |
| 168 | +| `publish.yml` workflow | `.github/workflows/publish.yml` | Two-job workflow: build artifacts, then publish via OIDC | |
| 169 | +| PyPI Trusted Publishing | PyPI's OIDC verifier | Validates the OIDC token from the `publish.yml` workflow and authorizes the upload | |
| 170 | +| PyPI | pypi.org | The distribution channel; serves wheels and sdists over HTTPS to `pip` | |
| 171 | +| Read The Docs | bitmath.readthedocs.io | Hosts the rendered documentation; rebuilds on push to master | |
| 172 | + |
| 173 | +### Pipeline view |
| 174 | + |
| 175 | +``` |
| 176 | + +------------------------+ |
| 177 | + | Developer | |
| 178 | + | (see MAINTAINERS.md) | |
| 179 | + +-----------+------------+ |
| 180 | + | git push, git tag |
| 181 | + v |
| 182 | + +------------------------+ |
| 183 | + | GitHub repository | |
| 184 | + | timlnx/bitmath | |
| 185 | + | branch protection on | |
| 186 | + | master (17 required | |
| 187 | + | checks, enforce | |
| 188 | + | admins) | |
| 189 | + +-----------+------------+ |
| 190 | + | release: published |
| 191 | + v |
| 192 | + +------------------------+ |
| 193 | + | publish.yml workflow | |
| 194 | + | | |
| 195 | + | Job 1: build | |
| 196 | + | pip install build | |
| 197 | + | python -m build | |
| 198 | + | -> dist/ artifacts | |
| 199 | + | | |
| 200 | + | Job 2: publish | |
| 201 | + | id-token: write | |
| 202 | + | PyPI OIDC exchange | |
| 203 | + +-----------+------------+ |
| 204 | + | OIDC-authenticated upload |
| 205 | + v |
| 206 | + +------------------------+ |
| 207 | + | PyPI | |
| 208 | + | pypi.org/project | |
| 209 | + | /bitmath/ | |
| 210 | + | wheel + sdist | |
| 211 | + | + PEP 740 | |
| 212 | + | attestations | |
| 213 | + +-----------+------------+ |
| 214 | + | pip install bitmath |
| 215 | + v |
| 216 | + +------------------------+ |
| 217 | + | Consumer's process | |
| 218 | + +------------------------+ |
| 219 | +``` |
| 220 | + |
| 221 | +### Actions |
| 222 | + |
| 223 | +1. **Version bump.** I edit `VERSION` to the new SemVer string. |
| 224 | + `pyproject.toml` reads this dynamically via `[tool.hatch.version]`; |
| 225 | + the Makefile reads it for docs and man pages. There is no other |
| 226 | + place to update. |
| 227 | + |
| 228 | +2. **Changelog.** I add the section for the new version to |
| 229 | + `NEWS.rst`. |
| 230 | + |
| 231 | +3. **Tag and push.** A signed tag is pushed to GitHub. |
| 232 | + |
| 233 | +4. **Create a GitHub Release.** Releasing from the tag fires the |
| 234 | + `release: published` event. The `publish.yml` workflow has no |
| 235 | + other trigger; it cannot fire on pull requests or direct pushes. |
| 236 | + |
| 237 | +5. **Build job.** Runs on a clean ubuntu-latest runner with Python |
| 238 | + 3.12. Installs `build`, runs `python -m build`, uploads the |
| 239 | + `dist/` directory as a workflow artifact. |
| 240 | + |
| 241 | +6. **Publish job.** Depends on the build job. Downloads the artifact. |
| 242 | + Requests an OIDC token (`id-token: write`) from GitHub Actions and |
| 243 | + exchanges it with PyPI for short-lived upload credentials via |
| 244 | + Trusted Publishing. Uploads the wheel and sdist. No long-lived |
| 245 | + PyPI API token exists anywhere in the project. |
| 246 | + |
| 247 | +7. **Consumer install.** A consumer runs `pip install bitmath`. |
| 248 | + `pip` fetches the wheel over HTTPS from PyPI, verifies the package |
| 249 | + against the published SHA-256 hash, and installs into their |
| 250 | + environment. |
| 251 | + |
| 252 | +## CI and quality gates |
| 253 | + |
| 254 | +CI runs continuously on `master` and on every pull request. These |
| 255 | +actors influence what is merged but do not produce released |
| 256 | +artifacts. They are summarized here for completeness; the source of |
| 257 | +truth for each is the corresponding workflow file in |
| 258 | +`.github/workflows/`. |
| 259 | + |
| 260 | +| Actor | Workflow | What it gates | |
| 261 | +|-------|----------|---------------| |
| 262 | +| pytest matrix | `python.yml` | Full test suite across Python 3.9–3.13 on macOS, Ubuntu, Windows | |
| 263 | +| Bandit | `bandit.yml` | Python security smell analysis across `bitmath/` and `tests/` | |
| 264 | +| CodeQL | `codeql.yml` | Semantic analysis for Python vulnerabilities | |
| 265 | +| OSSF Scorecard | `scorecard.yml` | Weekly repository security posture report | |
| 266 | +| Dependabot | `.github/dependabot.yml` | Automated dependency update pull requests | |
| 267 | + |
| 268 | +The first three are required status checks on the `master` branch. |
| 269 | +Merges are blocked until they pass. `enforce_admins: true` so I |
| 270 | +cannot bypass them. |
| 271 | + |
| 272 | +## Trust boundaries |
| 273 | + |
| 274 | +bitmath validates input only at the places untrusted data enters the |
| 275 | +library. The ingress points are: |
| 276 | + |
| 277 | +* **String parsing** (`parse_string`, `parse_string_unsafe`). |
| 278 | + Regex-driven, no `eval`, raises `ValueError` on anything |
| 279 | + malformed. Hypothesis-based fuzzing tests exercise this path. |
| 280 | +* **Filesystem paths** (`getsize`, `listdir`). bitmath does no path |
| 281 | + sanitization. The consumer is responsible for validating paths |
| 282 | + before passing them in. This is documented behavior; sanitization |
| 283 | + rules depend on the consumer's threat model in ways the library |
| 284 | + cannot anticipate. |
| 285 | +* **File descriptors** (`query_device_capacity`). Operates only on |
| 286 | + descriptors the consumer already opened. The ioctl arguments are |
| 287 | + fixed constants; no untrusted input flows into the kernel call. |
| 288 | +* **The build pipeline.** `publish.yml` cannot be triggered by a |
| 289 | + pull request, only by a release event. Untrusted contributor code |
| 290 | + never touches PyPI credentials. Branch protection prevents direct |
| 291 | + pushes to master from bypassing CI. |
| 292 | + |
| 293 | +See [SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md) for the full |
| 294 | +threat model and the specific mitigations at each boundary. |
| 295 | + |
| 296 | +## Documentation pipeline |
| 297 | + |
| 298 | +The documentation lives in `docsite/source/`. Sphinx builds it. |
| 299 | +`index.rst.in` is the source template; `index.rst` is generated from |
| 300 | +`index.rst.in` plus `README.rst` by `make docs`. Editing `index.rst` |
| 301 | +directly is wrong because the next `make docs` will overwrite it. |
| 302 | + |
| 303 | +Read The Docs runs the same build automatically on push to master |
| 304 | +and publishes the result at https://bitmath.readthedocs.io/. |
| 305 | + |
| 306 | +## Maintenance |
| 307 | + |
| 308 | +This document is reviewed at every minor release. The contract: |
| 309 | + |
| 310 | +* New public API → update the Runtime architecture component |
| 311 | + inventory and the actions list. |
| 312 | +* New CI workflow or build pipeline change → update Build and |
| 313 | + release pipeline and/or CI and quality gates. |
| 314 | +* New trust boundary → update Trust boundaries here and the threat |
| 315 | + model in [SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md). |
| 316 | +* Build-backend or package-tooling change → update Build and release |
| 317 | + pipeline. |
| 318 | + |
| 319 | +If this document drifts out of date, that is a defect worth |
| 320 | +[opening an issue](https://github.com/timlnx/bitmath/issues/new) or |
| 321 | +a PR. |
0 commit comments