zipwire 0.0.1__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {zipwire-0.0.1 → zipwire-0.1.0}/.github/workflows/ci.yml +30 -2
  2. zipwire-0.1.0/.readthedocs.yaml +25 -0
  3. zipwire-0.1.0/AGENTS.md +53 -0
  4. {zipwire-0.0.1 → zipwire-0.1.0}/PKG-INFO +15 -14
  5. {zipwire-0.0.1 → zipwire-0.1.0}/README.md +7 -7
  6. {zipwire-0.0.1 → zipwire-0.1.0}/docs/api.rst +21 -2
  7. {zipwire-0.0.1 → zipwire-0.1.0}/docs/backends.rst +69 -8
  8. {zipwire-0.0.1 → zipwire-0.1.0}/docs/index.rst +1 -0
  9. zipwire-0.1.0/docs/requirements.txt +4 -0
  10. zipwire-0.1.0/docs/security.rst +96 -0
  11. {zipwire-0.0.1 → zipwire-0.1.0}/pyproject.toml +24 -12
  12. {zipwire-0.0.1 → zipwire-0.1.0}/src/zipwire/__init__.py +4 -1
  13. {zipwire-0.0.1 → zipwire-0.1.0}/src/zipwire/__main__.py +40 -9
  14. zipwire-0.1.0/src/zipwire/_async.py +227 -0
  15. {zipwire-0.0.1 → zipwire-0.1.0}/src/zipwire/_constants.py +11 -0
  16. {zipwire-0.0.1 → zipwire-0.1.0}/src/zipwire/_decompress.py +39 -20
  17. {zipwire-0.0.1 → zipwire-0.1.0}/src/zipwire/_errors.py +13 -0
  18. {zipwire-0.0.1 → zipwire-0.1.0}/src/zipwire/_parser.py +1 -7
  19. zipwire-0.1.0/src/zipwire/_sync.py +224 -0
  20. zipwire-0.1.0/src/zipwire/_types.py +105 -0
  21. {zipwire-0.0.1 → zipwire-0.1.0}/src/zipwire/_version.py +2 -2
  22. {zipwire-0.0.1 → zipwire-0.1.0}/src/zipwire/backends/__init__.py +4 -4
  23. zipwire-0.1.0/src/zipwire/backends/_aiohttp.py +86 -0
  24. zipwire-0.1.0/src/zipwire/backends/_httpx2.py +157 -0
  25. zipwire-0.1.0/src/zipwire/backends/_requests.py +92 -0
  26. zipwire-0.1.0/src/zipwire/backends/_urllib3.py +93 -0
  27. {zipwire-0.0.1 → zipwire-0.1.0}/tests/conftest.py +50 -8
  28. {zipwire-0.0.1 → zipwire-0.1.0}/tests/test_async.py +34 -0
  29. zipwire-0.1.0/tests/test_backends.py +405 -0
  30. zipwire-0.1.0/tests/test_cli.py +226 -0
  31. {zipwire-0.0.1 → zipwire-0.1.0}/tests/test_decompress.py +117 -0
  32. zipwire-0.1.0/tests/test_integration.py +101 -0
  33. zipwire-0.1.0/tests/test_parser.py +262 -0
  34. {zipwire-0.0.1 → zipwire-0.1.0}/tests/test_sync.py +34 -0
  35. {zipwire-0.0.1 → zipwire-0.1.0}/tests/test_zipinfo.py +71 -1
  36. {zipwire-0.0.1 → zipwire-0.1.0}/tox.ini +9 -4
  37. zipwire-0.0.1/AGENTS.md +0 -29
  38. zipwire-0.0.1/src/zipwire/_async.py +0 -173
  39. zipwire-0.0.1/src/zipwire/_sync.py +0 -163
  40. zipwire-0.0.1/src/zipwire/_types.py +0 -57
  41. zipwire-0.0.1/src/zipwire/backends/_aiohttp.py +0 -55
  42. zipwire-0.0.1/src/zipwire/backends/_httpx2_async.py +0 -55
  43. zipwire-0.0.1/src/zipwire/backends/_httpx2_sync.py +0 -54
  44. zipwire-0.0.1/src/zipwire/backends/_requests.py +0 -54
  45. zipwire-0.0.1/src/zipwire/backends/_urllib3.py +0 -59
  46. zipwire-0.0.1/tests/test_backends.py +0 -255
  47. zipwire-0.0.1/tests/test_parser.py +0 -109
  48. {zipwire-0.0.1 → zipwire-0.1.0}/.github/dependabot.yml +0 -0
  49. {zipwire-0.0.1 → zipwire-0.1.0}/.github/workflows/codeql.yml +0 -0
  50. {zipwire-0.0.1 → zipwire-0.1.0}/.github/workflows/release.yml +0 -0
  51. {zipwire-0.0.1 → zipwire-0.1.0}/.github/workflows/scorecard.yml +0 -0
  52. {zipwire-0.0.1 → zipwire-0.1.0}/.gitignore +0 -0
  53. {zipwire-0.0.1 → zipwire-0.1.0}/LICENSE +0 -0
  54. {zipwire-0.0.1 → zipwire-0.1.0}/SECURITY.md +0 -0
  55. {zipwire-0.0.1 → zipwire-0.1.0}/docs/Makefile +0 -0
  56. {zipwire-0.0.1 → zipwire-0.1.0}/docs/conf.py +0 -0
  57. {zipwire-0.0.1 → zipwire-0.1.0}/docs/quickstart.rst +0 -0
  58. {zipwire-0.0.1 → zipwire-0.1.0}/src/zipwire/_zipinfo.py +0 -0
  59. {zipwire-0.0.1 → zipwire-0.1.0}/src/zipwire/py.typed +0 -0
  60. {zipwire-0.0.1 → zipwire-0.1.0}/tests/__init__.py +0 -0
  61. {zipwire-0.0.1 → zipwire-0.1.0}/uv.lock +0 -0
@@ -34,7 +34,8 @@ jobs:
34
34
  strategy:
35
35
  fail-fast: false
36
36
  matrix:
37
- python-version: ["3.11", "3.12", "3.13", "3.14"]
37
+ python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
38
+ continue-on-error: ${{ matrix.python-version == '3.15' }}
38
39
  steps:
39
40
  - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
40
41
  with:
@@ -43,7 +44,34 @@ jobs:
43
44
  with:
44
45
  enable-cache: true
45
46
  - run: uv sync --group dev --python ${{ matrix.python-version }}
46
- - run: uv run pytest
47
+ - run: uv run coverage run -m pytest
48
+ - run: uv run pytest -m integration -v
49
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
50
+ with:
51
+ name: coverage-${{ matrix.python-version }}
52
+ path: .coverage.*
53
+ include-hidden-files: true
54
+
55
+ coverage:
56
+ runs-on: ubuntu-latest
57
+ needs: test
58
+ permissions:
59
+ contents: read
60
+ steps:
61
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
62
+ with:
63
+ persist-credentials: false
64
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
65
+ with:
66
+ enable-cache: true
67
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
68
+ with:
69
+ pattern: coverage-*
70
+ merge-multiple: true
71
+ - run: uv tool install "coverage[toml]>=7.0"
72
+ - run: coverage combine
73
+ - run: coverage report
74
+ - run: coverage xml
47
75
 
48
76
  docs:
49
77
  runs-on: ubuntu-latest
@@ -0,0 +1,25 @@
1
+ # Read the Docs configuration file
2
+ # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3
+
4
+ # Required
5
+ version: 2
6
+
7
+ # Set the OS, Python version, and other tools you might need
8
+ build:
9
+ os: ubuntu-24.04
10
+ tools:
11
+ python: "3.13"
12
+
13
+ # Build documentation in the "docs/" directory with Sphinx
14
+ sphinx:
15
+ configuration: docs/conf.py
16
+
17
+ # Declare the Python requirements required to build your documentation
18
+ # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
19
+ python:
20
+ install:
21
+ - requirements: docs/requirements.txt
22
+ - method: pip
23
+ path: .
24
+ extra_requirements:
25
+ - all
@@ -0,0 +1,53 @@
1
+ # zipwire
2
+
3
+ Read and extract files from remote ZIP archives over HTTP range requests.
4
+ The library parses ZIP central directory and local file headers without
5
+ downloading the entire archive, then streams and decompresses individual
6
+ entries on the fly.
7
+
8
+ ## Source layout
9
+
10
+ - `src/zipwire/` - main package (sync/async API, parsers, decompression)
11
+ - `src/zipwire/backends/` - HTTP backend adapters (urllib3, httpx2, aiohttp, requests)
12
+ - `tests/` - pytest test suite (async tests use `pytest-asyncio`)
13
+ - `docs/` - Sphinx documentation (Furo theme, MyST markdown)
14
+
15
+ ## Virtual environment rules
16
+
17
+ - **Always** use the `.venv` virtual environment in the project root.
18
+ - Create it with `uv venv .venv` if it does not exist.
19
+ - **Never** install packages into the system Python or any other environment.
20
+ - Install the project: `uv pip install -e ".[all]" --python .venv/bin/python`
21
+ - Install dev deps: `uv pip install "pytest>=8.0" "pytest-asyncio>=0.24" "pytest-httpserver>=1.1" "coverage[toml]>=7.0" --python .venv/bin/python`
22
+ - Run tests directly: `.venv/bin/python -m pytest tests/`
23
+ - Run any Python tool via `.venv/bin/python -m <tool>` or `.venv/bin/<tool>`.
24
+ - Use `uv pip install --python .venv/bin/python` for all pip operations.
25
+
26
+ ## Code style rules
27
+
28
+ - **No local imports.** All imports must be at the top of the file.
29
+ Do not use `from foo import bar` inside functions, methods, or test bodies.
30
+
31
+ ## Git rules
32
+
33
+ - **Always** sign off commits with `git commit -s`.
34
+ Every commit message must include a `Signed-off-by:` line.
35
+
36
+ ## Commands
37
+
38
+ ```bash
39
+ # lint (ruff check + format)
40
+ uvx --with tox-uv tox run -e lint
41
+
42
+ # run tests (single Python version)
43
+ uvx --with tox-uv tox run -e py314
44
+
45
+ # run tests directly with .venv
46
+ .venv/bin/python -m pytest tests/ -v
47
+
48
+ # run full test matrix
49
+ uvx --with tox-uv tox run -e py311,py312,py313,py314
50
+
51
+ # build docs
52
+ uvx --with tox-uv tox run -e docs
53
+ ```
@@ -1,14 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zipwire
3
- Version: 0.0.1
3
+ Version: 0.1.0
4
4
  Summary: Read and extract files from remote ZIP archives over HTTP range requests
5
5
  Project-URL: Homepage, https://github.com/tiran/zipwire
6
+ Project-URL: Documentation, https://zipwire.readthedocs.io/
6
7
  Project-URL: Source, https://github.com/tiran/zipwire
7
8
  Project-URL: Issues, https://github.com/tiran/zipwire/issues
8
9
  Author-email: Christian Heimes <christian@python.org>
9
10
  License-Expression: Apache-2.0
10
11
  License-File: LICENSE
11
- Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Development Status :: 4 - Beta
12
13
  Classifier: Intended Audience :: Developers
13
14
  Classifier: License :: OSI Approved :: Apache Software License
14
15
  Classifier: Programming Language :: Python :: 3
@@ -19,17 +20,17 @@ Classifier: Programming Language :: Python :: 3.14
19
20
  Classifier: Topic :: Software Development :: Libraries
20
21
  Classifier: Typing :: Typed
21
22
  Requires-Python: >=3.11
22
- Requires-Dist: urllib3>=2.0
23
+ Requires-Dist: urllib3>=2
23
24
  Provides-Extra: aiohttp
24
25
  Requires-Dist: aiohttp>=3.9; extra == 'aiohttp'
25
26
  Provides-Extra: all
26
27
  Requires-Dist: aiohttp>=3.9; extra == 'all'
27
- Requires-Dist: httpx2>=2.0; extra == 'all'
28
- Requires-Dist: requests>=2.31; extra == 'all'
28
+ Requires-Dist: httpx2[http2]>=2; extra == 'all'
29
+ Requires-Dist: requests>=2.28; extra == 'all'
29
30
  Provides-Extra: httpx2
30
- Requires-Dist: httpx2>=2.0; extra == 'httpx2'
31
+ Requires-Dist: httpx2[http2]>=2; extra == 'httpx2'
31
32
  Provides-Extra: requests
32
- Requires-Dist: requests>=2.31; extra == 'requests'
33
+ Requires-Dist: requests>=2.28; extra == 'requests'
33
34
  Description-Content-Type: text/markdown
34
35
 
35
36
  # zipwire
@@ -72,13 +73,13 @@ sync and async:
72
73
  pip install zipwire[httpx2]
73
74
  ```
74
75
 
75
- | Backend | Class | Mode | Install extra |
76
- |----------|--------------------|-------|---------------|
77
- | urllib3 | `Urllib3Reader` | sync | *(included)* |
78
- | httpx2 | `Httpx2SyncReader` | sync | `httpx2` |
79
- | httpx2 | `Httpx2AsyncReader`| async | `httpx2` |
80
- | requests | `RequestsReader` | sync | `requests` |
81
- | aiohttp | `AiohttpReader` | async | `aiohttp` |
76
+ | Backend | Class | Mode | HTTP | Install extra |
77
+ |----------|----------------------|-------|---------|---------------|
78
+ | urllib3 | `Urllib3Reader` | sync | 1.1 | *(included)* |
79
+ | httpx2 | `Httpx2SyncReader` | sync | 1.1, 2 | `httpx2` |
80
+ | httpx2 | `Httpx2AsyncReader` | async | 1.1, 2 | `httpx2` |
81
+ | requests | `RequestsReader` | sync | 1.1 | `requests` |
82
+ | aiohttp | `AiohttpReader` | async | 1.1 | `aiohttp` |
82
83
 
83
84
  Every backend accepts an optional pre-configured client or session so you can
84
85
  share connection pools, authentication, and retry configuration.
@@ -38,13 +38,13 @@ sync and async:
38
38
  pip install zipwire[httpx2]
39
39
  ```
40
40
 
41
- | Backend | Class | Mode | Install extra |
42
- |----------|--------------------|-------|---------------|
43
- | urllib3 | `Urllib3Reader` | sync | *(included)* |
44
- | httpx2 | `Httpx2SyncReader` | sync | `httpx2` |
45
- | httpx2 | `Httpx2AsyncReader`| async | `httpx2` |
46
- | requests | `RequestsReader` | sync | `requests` |
47
- | aiohttp | `AiohttpReader` | async | `aiohttp` |
41
+ | Backend | Class | Mode | HTTP | Install extra |
42
+ |----------|----------------------|-------|---------|---------------|
43
+ | urllib3 | `Urllib3Reader` | sync | 1.1 | *(included)* |
44
+ | httpx2 | `Httpx2SyncReader` | sync | 1.1, 2 | `httpx2` |
45
+ | httpx2 | `Httpx2AsyncReader` | async | 1.1, 2 | `httpx2` |
46
+ | requests | `RequestsReader` | sync | 1.1 | `requests` |
47
+ | aiohttp | `AiohttpReader` | async | 1.1 | `aiohttp` |
48
48
 
49
49
  Every backend accepts an optional pre-configured client or session so you can
50
50
  share connection pools, authentication, and retry configuration.
@@ -15,16 +15,31 @@ Core
15
15
  .. autoclass:: zipwire.RemoteZipInfo
16
16
  :show-inheritance:
17
17
 
18
+ Protocols and Types
19
+ -------------------
20
+
21
+ .. autoclass:: zipwire.SyncReader
22
+ :members:
23
+
24
+ .. autoclass:: zipwire.AsyncReader
25
+ :members:
26
+
27
+ .. autoclass:: zipwire.Headers
28
+ :members:
29
+
30
+ .. autoclass:: zipwire.Writable
31
+ :members:
32
+
18
33
  Backends
19
34
  --------
20
35
 
21
36
  .. autoclass:: zipwire.backends._urllib3.Urllib3Reader
22
37
  :members:
23
38
 
24
- .. autoclass:: zipwire.backends._httpx2_sync.Httpx2SyncReader
39
+ .. autoclass:: zipwire.backends._httpx2.Httpx2SyncReader
25
40
  :members:
26
41
 
27
- .. autoclass:: zipwire.backends._httpx2_async.Httpx2AsyncReader
42
+ .. autoclass:: zipwire.backends._httpx2.Httpx2AsyncReader
28
43
  :members:
29
44
 
30
45
  .. autoclass:: zipwire.backends._requests.RequestsReader
@@ -43,6 +58,7 @@ Exceptions
43
58
  ├── BadZipFile
44
59
  ├── UnsupportedCompression
45
60
  ├── CRCMismatch
61
+ ├── FileTooLarge
46
62
  ├── RangeRequestUnsupported
47
63
  └── FileNotFoundInZip (also inherits KeyError)
48
64
 
@@ -58,6 +74,9 @@ Exceptions
58
74
  .. autoexception:: zipwire.CRCMismatch
59
75
  :show-inheritance:
60
76
 
77
+ .. autoexception:: zipwire.FileTooLarge
78
+ :show-inheritance:
79
+
61
80
  .. autoexception:: zipwire.RangeRequestUnsupported
62
81
  :show-inheritance:
63
82
 
@@ -11,31 +11,37 @@ Available backends
11
11
 
12
12
  .. list-table::
13
13
  :header-rows: 1
14
- :widths: 15 25 10 20
14
+ :widths: 15 25 10 10 20
15
15
 
16
16
  * - Library
17
17
  - Class
18
18
  - Mode
19
+ - HTTP
19
20
  - Install extra
20
21
  * - urllib3
21
22
  - :class:`~zipwire.backends.Urllib3Reader`
22
23
  - sync
24
+ - 1.1
23
25
  - *(included)*
24
26
  * - httpx2
25
27
  - :class:`~zipwire.backends.Httpx2SyncReader`
26
28
  - sync
29
+ - 1.1, 2
27
30
  - ``httpx2``
28
31
  * - httpx2
29
32
  - :class:`~zipwire.backends.Httpx2AsyncReader`
30
33
  - async
34
+ - 1.1, 2
31
35
  - ``httpx2``
32
36
  * - requests
33
37
  - :class:`~zipwire.backends.RequestsReader`
34
38
  - sync
39
+ - 1.1
35
40
  - ``requests``
36
41
  * - aiohttp
37
42
  - :class:`~zipwire.backends.AiohttpReader`
38
43
  - async
44
+ - 1.1
39
45
  - ``aiohttp``
40
46
 
41
47
  Choosing a backend
@@ -47,7 +53,10 @@ Choosing a backend
47
53
  Best choice if your project already depends on aiohttp.
48
54
  - **Sync + async from one library** - install ``httpx2`` and use
49
55
  :class:`~zipwire.backends.Httpx2SyncReader` or
50
- :class:`~zipwire.backends.Httpx2AsyncReader`.
56
+ :class:`~zipwire.backends.Httpx2AsyncReader`. The ``httpx2`` extra includes
57
+ HTTP/2 support via the ``h2`` package. When ``h2`` is available, HTTP/2 is
58
+ enabled automatically; pass ``http2=False`` to force HTTP/1.1 or
59
+ ``http2=True`` to require it.
51
60
  - **Requests integration** - use :class:`~zipwire.backends.RequestsReader`
52
61
  if your project already uses ``requests`` and you want to share sessions,
53
62
  authentication, or retry configuration.
@@ -84,7 +93,7 @@ protocol and pass an instance to :class:`~zipwire.SyncRemoteZip` or
84
93
 
85
94
  from collections.abc import Iterator
86
95
 
87
- from zipwire import SyncReader, SyncRemoteZip
96
+ from zipwire import Headers, SyncReader, SyncRemoteZip
88
97
 
89
98
 
90
99
  class MyReader:
@@ -93,11 +102,15 @@ protocol and pass an instance to :class:`~zipwire.SyncRemoteZip` or
93
102
  def __init__(self, url: str) -> None:
94
103
  self._url = url
95
104
 
96
- def get_content_length(self) -> int:
97
- ... # HEAD request, return Content-Length
105
+ def head(self) -> Headers:
106
+ ... # HEAD request, check Accept-Ranges, return headers
98
107
 
99
- def read_range(self, offset: int, length: int) -> bytes:
100
- ... # GET with Range header
108
+ def read_range(
109
+ self,
110
+ offset: int,
111
+ length: int,
112
+ ) -> tuple[bytes, Headers]:
113
+ ... # GET with Range header, return (data, response_headers)
101
114
 
102
115
  def stream_range(self, offset: int, length: int) -> Iterator[bytes]:
103
116
  ... # GET with Range header, yield chunks
@@ -108,4 +121,52 @@ protocol and pass an instance to :class:`~zipwire.SyncRemoteZip` or
108
121
  assert isinstance(MyReader("..."), SyncReader) # runtime-checkable
109
122
 
110
123
  See the :class:`~zipwire.SyncReader` and :class:`~zipwire.AsyncReader`
111
- protocol definitions in the source for the full method signatures.
124
+ protocol definitions for the full method signatures.
125
+
126
+ Why only absolute byte ranges?
127
+ -------------------------------
128
+
129
+ HTTP range requests support two forms: absolute ranges
130
+ (``bytes=<start>-<end>``) and suffix ranges (``bytes=-<N>``). Suffix
131
+ ranges request the last *N* bytes without requiring the client to know
132
+ the total file size. While RFC 7233 defines both forms, support varies
133
+ across CDNs and cloud storage services:
134
+
135
+ .. list-table::
136
+ :header-rows: 1
137
+ :widths: 25 15 40
138
+
139
+ * - Service
140
+ - Suffix ranges
141
+ - Notes
142
+ * - AWS S3
143
+ - supported
144
+ - Full RFC 7233 compliance.
145
+ * - Google Cloud Storage
146
+ - supported
147
+ - Confirmed by GCS team.
148
+ * - Azure Blob Storage
149
+ - **not supported**
150
+ - Returns 200 with full object instead of 206.
151
+ * - Amazon CloudFront
152
+ - **not supported**
153
+ - Returns 200 with full object instead of 206.
154
+ * - Cloudflare R2
155
+ - supported
156
+ - Via S3-compatible and Workers API.
157
+ * - Fastly
158
+ - varies
159
+ - Some Fastly-fronted origins (e.g. PyPI) return 501.
160
+ * - Cloudflare CDN
161
+ - uncertain
162
+ - Undocumented; may pass through to origin.
163
+ * - Akamai
164
+ - likely supported
165
+ - General range support confirmed; suffix ranges undocumented.
166
+
167
+ Because suffix ranges silently break on several major platforms (returning
168
+ the full object or an error), zipwire uses only absolute byte ranges. On
169
+ first access it sends a HEAD request to learn the file size, then
170
+ computes absolute offsets for all subsequent range requests. This adds
171
+ one round-trip compared to a suffix-range approach but works reliably
172
+ everywhere.
@@ -7,6 +7,7 @@
7
7
 
8
8
  quickstart
9
9
  backends
10
+ security
10
11
 
11
12
  .. toctree::
12
13
  :maxdepth: 2
@@ -0,0 +1,4 @@
1
+ sphinx>=8.0
2
+ furo>=2024.1
3
+ sphinx-autobuild>=2024.1
4
+ myst-parser>=4.0
@@ -0,0 +1,96 @@
1
+ Security
2
+ ========
3
+
4
+ zipwire fetches data from remote servers and parses binary ZIP
5
+ structures. When processing archives from untrusted sources, keep the
6
+ following risks in mind.
7
+
8
+ Zip bombs
9
+ ---------
10
+
11
+ A *zip bomb* is a malicious archive containing entries that decompress
12
+ to a vastly larger size than their compressed representation, potentially
13
+ exhausting memory or disk space.
14
+
15
+ Both :meth:`~zipwire.SyncRemoteZip.read` and
16
+ :meth:`~zipwire.SyncRemoteZip.read_into` (and their async equivalents)
17
+ accept a ``max_file_size`` keyword argument that limits the uncompressed
18
+ size of a single entry:
19
+
20
+ .. code-block:: python
21
+
22
+ from zipwire import SyncRemoteZip
23
+ from zipwire.backends import Urllib3Reader
24
+
25
+ reader = Urllib3Reader("https://example.com/untrusted.zip")
26
+ with SyncRemoteZip(reader) as rz:
27
+ for info in rz.infolist():
28
+ data = rz.read(info, max_file_size=50 * 1024 * 1024) # 50 MiB
29
+
30
+ When the limit is exceeded, :exc:`~zipwire.FileTooLarge` is raised.
31
+
32
+ **Two layers of protection:**
33
+
34
+ 1. **Pre-check** -- before decompression starts, the entry's declared
35
+ ``file_size`` from the central directory is compared against the limit.
36
+ This catches honest zip bombs with no overhead.
37
+
38
+ 2. **Streaming enforcement** (:meth:`read_into` only) -- the streaming
39
+ decompressor tracks the actual decompressed output and aborts if it
40
+ exceeds the limit. This catches crafted archives that lie about their
41
+ uncompressed size in the metadata.
42
+
43
+ :meth:`read` performs only the pre-check (layer 1). Because it
44
+ decompresses the entire entry into memory in one step, it cannot enforce
45
+ the limit during decompression. Use :meth:`read_into` when processing
46
+ untrusted archives for defence in depth.
47
+
48
+ You can also inspect entries before extracting them:
49
+
50
+ .. code-block:: python
51
+
52
+ MAX_SIZE = 100 * 1024 * 1024 # 100 MiB
53
+
54
+ with SyncRemoteZip(reader) as rz:
55
+ for info in rz.infolist():
56
+ if info.file_size > MAX_SIZE:
57
+ print(f"Skipping {info.filename}: too large ({info.file_size} bytes)")
58
+ continue
59
+ data = rz.read(info)
60
+
61
+ Path traversal
62
+ --------------
63
+
64
+ ZIP archives can contain entries with absolute paths or ``../``
65
+ components (e.g. ``../../etc/passwd``). zipwire does **not** extract
66
+ files to disk, so this is not a direct risk. However, if you use
67
+ filenames from the archive to construct file paths, always sanitise them:
68
+
69
+ .. code-block:: python
70
+
71
+ import os
72
+
73
+ with SyncRemoteZip(reader) as rz:
74
+ for info in rz.infolist():
75
+ # Reject absolute paths and path traversal
76
+ if os.path.isabs(info.filename) or ".." in info.filename.split("/"):
77
+ print(f"Skipping suspicious path: {info.filename}")
78
+ continue
79
+ dest = os.path.join(output_dir, info.filename)
80
+ # Verify the resolved path is inside output_dir
81
+ if not os.path.realpath(dest).startswith(os.path.realpath(output_dir)):
82
+ print(f"Skipping path traversal: {info.filename}")
83
+ continue
84
+
85
+ Server trust
86
+ ------------
87
+
88
+ zipwire sends HTTP requests to a URL you provide. The server controls
89
+ what data is returned. A malicious server could return crafted responses
90
+ designed to exploit ZIP parsing bugs, cause excessive memory use, or
91
+ return different data for different range requests.
92
+
93
+ - Only fetch archives from servers you trust.
94
+ - Use HTTPS to prevent tampering in transit.
95
+ - Consider setting ``max_file_size`` even for trusted servers as a
96
+ safety net.
@@ -11,7 +11,7 @@ authors = [{ name = "Christian Heimes", email = "christian@python.org" }]
11
11
  requires-python = ">=3.11"
12
12
  readme = "README.md"
13
13
  classifiers = [
14
- "Development Status :: 3 - Alpha",
14
+ "Development Status :: 4 - Beta",
15
15
  "Intended Audience :: Developers",
16
16
  "License :: OSI Approved :: Apache Software License",
17
17
  "Programming Language :: Python :: 3",
@@ -22,34 +22,35 @@ classifiers = [
22
22
  "Topic :: Software Development :: Libraries",
23
23
  "Typing :: Typed",
24
24
  ]
25
- dependencies = ["urllib3>=2.0"]
25
+ dependencies = ["urllib3>=2"]
26
26
 
27
27
  [project.urls]
28
28
  Homepage = "https://github.com/tiran/zipwire"
29
+ Documentation = "https://zipwire.readthedocs.io/"
29
30
  Source = "https://github.com/tiran/zipwire"
30
31
  Issues = "https://github.com/tiran/zipwire/issues"
31
32
 
32
33
  [project.optional-dependencies]
33
- httpx2 = ["httpx2>=2.0"]
34
+ httpx2 = ["httpx2[http2]>=2"]
34
35
  aiohttp = ["aiohttp>=3.9"]
35
- requests = ["requests>=2.31"]
36
- all = ["httpx2>=2.0", "aiohttp>=3.9", "requests>=2.31"]
36
+ requests = ["requests>=2.28"]
37
+ all = ["httpx2[http2]>=2", "aiohttp>=3.9", "requests>=2.28"]
37
38
 
38
39
  [dependency-groups]
39
40
  dev = [
40
41
  "zipwire[all]",
41
- "pytest>=8.0",
42
+ "pytest>=8",
42
43
  "pytest-asyncio>=0.24",
43
- "pytest-httpserver>=1.1",
44
- "coverage[toml]>=7.0",
44
+ "pytest-httpserver>=1",
45
+ "coverage[toml]>=7",
45
46
  "ruff>=0.11",
46
47
  ]
47
48
  docs = [
48
49
  "zipwire[all]",
49
- "sphinx>=8.0",
50
- "furo>=2024.1",
51
- "sphinx-autobuild>=2024.1",
52
- "myst-parser>=4.0",
50
+ "sphinx>=8",
51
+ "furo>=2024",
52
+ "sphinx-autobuild>=2024",
53
+ "myst-parser>=4",
53
54
  ]
54
55
 
55
56
  [tool.hatch.version]
@@ -64,6 +65,8 @@ packages = ["src/zipwire"]
64
65
  [tool.pytest.ini_options]
65
66
  testpaths = ["tests"]
66
67
  asyncio_mode = "auto"
68
+ markers = ["integration: tests that hit real network services (deselect with '-m not integration')"]
69
+ addopts = "-m 'not integration'"
67
70
 
68
71
  [tool.ruff]
69
72
  line-length = 99
@@ -73,7 +76,16 @@ target-version = "py311"
73
76
  select = ["E", "W", "F", "I", "UP", "B", "SIM", "TCH", "RUF"]
74
77
 
75
78
  [tool.coverage.run]
79
+ branch = true
80
+ parallel = true
76
81
  source = ["zipwire"]
82
+ omit = ["*/_version.py"]
83
+
84
+ [tool.coverage.paths]
85
+ source = [
86
+ "src/zipwire",
87
+ ".tox/*/lib/python*/site-packages/zipwire",
88
+ ]
77
89
 
78
90
  [tool.coverage.report]
79
91
  show_missing = true
@@ -8,12 +8,13 @@ from zipwire._errors import (
8
8
  BadZipFile,
9
9
  CRCMismatch,
10
10
  FileNotFoundInZip,
11
+ FileTooLarge,
11
12
  RangeRequestUnsupported,
12
13
  UnsupportedCompression,
13
14
  ZipwireError,
14
15
  )
15
16
  from zipwire._sync import SyncRemoteZip
16
- from zipwire._types import AsyncReader, SyncReader, Writable
17
+ from zipwire._types import AsyncReader, Headers, SyncReader, Writable
17
18
  from zipwire._zipinfo import RemoteZipInfo
18
19
 
19
20
  __all__ = [
@@ -23,6 +24,8 @@ __all__ = [
23
24
  "CRCMismatch",
24
25
  "CompressionMethod",
25
26
  "FileNotFoundInZip",
27
+ "FileTooLarge",
28
+ "Headers",
26
29
  "RangeRequestUnsupported",
27
30
  "RemoteZipInfo",
28
31
  "SyncReader",