Skip to content

Commit ada248e

Browse files
v0.4.0: add pluggable archive extraction (v15)
Add expand_archive() and expand_archive_streaming() for extracting ZIP/TAR archives directly into MFS with auto-detection and conflict handling. - add _archive.py with ArchiveAdapter base class, ZipAdapter, and TarAdapter - expand_archive() provides atomic extraction via import_tree() - expand_archive_streaming() provides low-memory streaming extraction - add _sanitize_archive_path() for Zip Slip prevention - add on_conflict parameter for duplicate and collision handling - re-export ArchiveAdapter, expand_archive, expand_archive_streaming from __init__ - add 67 tests across 5 new test files (total: 436) - update README, README_ja, TESTING, TESTING_ja, CHANGELOG, and examples - add archive-related Non-Goals to README compatibility section
1 parent 7c65452 commit ada248e

15 files changed

Lines changed: 1707 additions & 46 deletions

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.4.0] - 2026-03-21
11+
12+
### Added
13+
- `ArchiveAdapter`: pluggable base class for archive format support
14+
- `expand_archive()`: atomic archive expansion via `import_tree()`,
15+
with auto-detection and optional `adapters` / `adapter` parameters
16+
- `expand_archive_streaming()`: streaming archive expansion via `open()`/`write()`,
17+
with O(single-file) peak memory and `on_conflict="skip"` for incremental extraction
18+
- Built-in `ZipAdapter` and `TarAdapter` (standard library only)
19+
- Archive path sanitization: absolute paths, `../` traversal sequences,
20+
and backslash paths are silently stripped before extraction (Zip Slip protection)
21+
- `on_conflict` parameter controls archive-internal duplicates and MFS existing
22+
file collisions; existing directory collisions always raise `IsADirectoryError`
23+
- 67 new tests across 5 files for archive expansion feature (total: 436)
24+
25+
### Changed
26+
- `dmemfs.__version__` and project version bumped to `0.4.0`
27+
1028
## [0.3.0] - 2026-03-09
1129

1230
### Added

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Languages: [English](https://github.com/nightmarewalker/D-MemFS/blob/main/README
1717

1818
| Metric | Details |
1919
|---|---|
20-
| 🧪 **Robustness** | 369 tests with 97% code coverage |
20+
| 🧪 **Robustness** | 436 tests with 97% code coverage |
2121
| 🔒 **Verified Safety** | 98, 100×4 — top scores across all security categories (Socket.dev) |
2222
| 🌟 **Community** | [Discussed on `r/Python`](https://www.reddit.com/r/Python/comments/1rrqr8z/i_built_an_inmemory_virtual_filesystem_for_python/) with highly positive reception |
2323
---
@@ -34,16 +34,16 @@ Languages: [English](https://github.com/nightmarewalker/D-MemFS/blob/main/README
3434
- Async wrapper (`AsyncMemoryFileSystem`) powered by `asyncio.to_thread`
3535
- Zero runtime dependencies (standard library only)
3636
- **No admin/root privileges required** — works on locked-down CI runners, containers, and shared machines where OS-level RAM disks are not an option
37-
- **369 tests, 97% coverage** across 3 OS (Linux / Windows / macOS) × 3 Python versions (3.11–3.13, including free-threaded 3.13t)
37+
- **436 tests, 97% coverage** across 3 OS (Linux / Windows / macOS) × 3 Python versions (3.11–3.13, including free-threaded 3.13t)
3838

3939
This is useful when `io.BytesIO` is too primitive (single buffer), and OS-level RAM disks/tmpfs are impractical (permissions, container policy, Windows driver friction). Ideal for **CI pipeline acceleration** — eliminate disk I/O from test suites and data processing without any infrastructure changes.
4040

4141
**Note on Architectural Boundary:** This is strictly an in-process tool. External subprocesses (CLI tools) cannot access these files via standard OS paths. If your pipeline relies heavily on passing files to external binaries, an OS-level RAM disk (`tmpfs`) is the correct tool. D-MemFS shines when accelerating Python-native test suites or internal data pipelines.
4242

4343
---
4444

45-
### Archive Extraction In-Memory
46-
Extract large ZIP or TAR archives entirely in-memory to process their contents on the fly. Prevent disk wear (TBW) and eliminate the risk of leaving garbage files behind.
45+
### Archive Extraction
46+
Extract ZIP/TAR archives directly into D-MemFS using the built-in `expand_archive()` (atomic, all-or-nothing) or `expand_archive_streaming()` (low-memory, incremental). Custom archive formats are supported via the pluggable `ArchiveAdapter` interface. A low-level manual extraction example using `open()`/`write()` is also included as a reference for advanced use cases.
4747
* 📝 **Tutorial:** [`examples/archive_extraction.md`](examples/archive_extraction.md)
4848

4949
### CI/CD Pipelines & Test Debugging
@@ -110,6 +110,12 @@ except MFSQuotaExceededError as e:
110110
- `stat`, `stats`, `get_size`
111111
- `export_as_bytesio`, `export_tree`, `iter_export_tree`, `import_tree`
112112

113+
### Archive Extraction Functions
114+
115+
- `expand_archive(mfs, source, dest, *, on_conflict, adapter, adapters)` — atomic extraction via `import_tree()`
116+
- `expand_archive_streaming(mfs, source, dest, *, on_conflict, adapter, adapters)` — streaming extraction, returns write count
117+
- `ArchiveAdapter` — base class for pluggable archive format support (built-in: `ZipAdapter`, `TarAdapter`)
118+
113119
**Constructor parameters:**
114120
- `max_quota` (default `256 MiB`): byte quota for file data
115121
- `max_nodes` (default `None`): optional cap on total node count (files + directories). Raises `MFSNodeLimitExceededError` when exceeded.
@@ -322,6 +328,9 @@ uvx --with-requirements requirements.txt pdoc dmemfs -o docs/api
322328
- No symlink/hardlink support — intentionally omitted to eliminate path traversal loops and structural complexity (same rationale as `pathlib.PurePath`).
323329
- No direct `pathlib.Path` / `os.PathLike` API — MFS paths are virtual and must not be confused with host filesystem paths. Accepting `os.PathLike` would allow third-party libraries or a plain `open()` call to silently treat an MFS virtual path as a real OS path, potentially issuing unintended syscalls against the host filesystem. All paths must be plain `str` with POSIX-style absolute notation (e.g. `"/data/file.txt"`).
324330
- No kernel filesystem integration (intentionally in-process only)
331+
- No exhaustive archive format support — core handles zip and tar (standard library) only. For other formats (7z, RAR, etc.), you can write your own adapter. See [`examples/archive_extraction.md`](examples/archive_extraction.md) for details.
332+
- No password-protected / encrypted archive support
333+
- Archive extraction functions are sync-only. Use `asyncio.to_thread()` in async code.
325334

326335
Auto-promotion behavior:
327336

README_ja.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
| 指標 | 実績 |
1919
|---|---|
20-
| 🧪 **堅牢性** | 369テスト、カバレッジ97% |
20+
| 🧪 **堅牢性** | 436テスト、カバレッジ97% |
2121
| 🔒 **安全性の検証** | 98, 100×4 — 全セキュリティカテゴリでトップスコア(Socket.dev) |
2222
| 🌟 **コミュニティ** | [`r/Python` にて議論され、高い評価を獲得](https://www.reddit.com/r/Python/comments/1rrqr8z/i_built_an_inmemory_virtual_filesystem_for_python/) |
2323
---
@@ -34,16 +34,16 @@
3434
- `asyncio.to_thread` ベースの Async ラッパー(`AsyncMemoryFileSystem`
3535
- ランタイム依存ゼロ(標準ライブラリのみ)
3636
- **管理者権限不要** — OS レベルの RAM ディスクが使えない CI ランナー、コンテナ、共有マシンでもそのまま動作
37-
- **369テスト、カバレッジ97%** — 3 OS(Linux / Windows / macOS)× 3 Python バージョン(3.11〜3.13、フリースレッド 3.13t 含む)で検証済み
37+
- **436テスト、カバレッジ97%** — 3 OS(Linux / Windows / macOS)× 3 Python バージョン(3.11〜3.13、フリースレッド 3.13t 含む)で検証済み
3838

3939
`io.BytesIO` が単一バッファで不足する場合や、OSレベルのRAMディスク/tmpfsが使いにくい環境(権限制約、コンテナポリシー、Windowsの運用負荷)で有効です。**CI パイプラインの高速化**にも最適——インフラ変更なしにテストやデータ処理からディスク I/O を排除できます。
4040

4141
**アーキテクチャの境界に関する注意:** 本ライブラリは完全にプロセス内のツールです。外部のサブプロセス(CLIツールなど)は、標準的なOSのパスを経由してこれらのファイルにアクセスすることはできません。外部バイナリへのファイル受け渡しを多用するパイプラインの場合は、OSレベルのRAMディスク(`tmpfs`)が適しています。D-MemFSは、Pythonネイティブなテストスイートや内部データパイプラインの高速化において真価を発揮します。
4242

4343
---
4444

45-
### アーカイブのインメモリ解凍
46-
巨大なZIPやTARアーカイブをすべてメモリ上で解凍し、オンザフライで内容を処理します。ディスクの摩耗(TBW)を防ぎ、ゴミファイルが残るリスクを排除します
45+
### アーカイブ展開
46+
組み込みの `expand_archive()`(アトミック・All-or-Nothing)または `expand_archive_streaming()`(低メモリ・差分展開対応)で ZIP/TAR アーカイブを直接 D-MemFS 上に展開します。プラガブルな `ArchiveAdapter` インターフェースにより、カスタムアーカイブ形式への対応も可能です。低レベルの `open()`/`write()` による手動展開例も、高度なユースケースの参考として同梱しています
4747
* 📝 **チュートリアル:** [`examples/archive_extraction.md`](examples/archive_extraction.md)
4848

4949
### CI/CDパイプラインとテストのデバッグ
@@ -110,6 +110,12 @@ except MFSQuotaExceededError as e:
110110
- `stat`, `stats`, `get_size`
111111
- `export_as_bytesio`, `export_tree`, `iter_export_tree`, `import_tree`
112112

113+
### アーカイブ展開関数
114+
115+
- `expand_archive(mfs, source, dest, *, on_conflict, adapter, adapters)``import_tree()` 経由のアトミック展開
116+
- `expand_archive_streaming(mfs, source, dest, *, on_conflict, adapter, adapters)` — ストリーミング展開、書き込み回数を返却
117+
- `ArchiveAdapter` — プラガブルなアーカイブ形式サポート用基底クラス(組み込み: `ZipAdapter`, `TarAdapter`
118+
113119
**コンストラクタパラメータ:**
114120
- `max_quota`(デフォルト `256 MiB`): ファイルデータのバイトクォータ
115121
- `max_nodes`(デフォルト `None`): ノード数の上限(ファイル+ディレクトリ)。超過時は `MFSNodeLimitExceededError`
@@ -320,6 +326,9 @@ uvx --with-requirements requirements.txt pdoc dmemfs -o docs/api
320326
- シンボリックリンク/ハードリンク非対応 — パストラバーサルループや構造の複雑化を排除するため意図的に省略(`pathlib.PurePath` と同じ設計方針)。
321327
- `pathlib.Path` / `os.PathLike` 直接対応なし — MFS のパスは仮想パスであり、ホストファイルシステムのパスと混同されてはならない。`os.PathLike` を受け入れると、サードパーティライブラリや素の `open()` 呼び出しが MFS の仮想パスを実 OS のパスと誤認し、ホストファイルシステムに対して意図しないシステムコールを発行するリスクがある。すべてのパスは POSIX 絶対表記の `str`(例: `"/data/file.txt"`)で指定すること。
322328
- カーネルFS統合なし(意図的にプロセス内完結)
329+
- アーカイブ形式の網羅的サポートなし — コアが対応するのは zip と tar(標準ライブラリ)のみ。7z・RAR 等には、独自のアダプタを作成することで対応可能。詳細は [`examples/archive_extraction.md`](examples/archive_extraction.md) を参照。
330+
- パスワード付き/暗号化アーカイブ非対応
331+
- アーカイブ展開関数は sync 専用。async 環境では `asyncio.to_thread()` を使用すること。
323332

324333
自動昇格の挙動:
325334

TESTING.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ uvx --with-requirements requirements.txt pdoc dmemfs -o docs/api
166166

167167
## Test File Mapping Table
168168

169-
File breakdown of all tests (369 items):
169+
File breakdown of all tests (436 items):
170170

171171
| File Path | Test Count | Covered Area |
172172
|---|---:|---|
@@ -183,6 +183,8 @@ File breakdown of all tests (369 items):
183183
| `tests/unit/test_package_metadata.py` | 1 | Package metadata consistency |
184184
| `tests/unit/test_memory_info.py` | 8 | v14: `get_available_memory_bytes()` OS dependent memory detection |
185185
| `tests/unit/test_memory_guard.py` | 8 | v14: `NullGuard`/`InitGuard`/`PerWriteGuard` strategy patterns |
186+
| `tests/unit/test_archive_sanitize.py` | 12 | v15: `_sanitize_archive_path()` Zip Slip prevention |
187+
| `tests/unit/test_archive_adapter.py` | 13 | v15: `ArchiveAdapter` base class, `can_handle()`, `_detect()` |
186188
| `tests/integration/test_open_modes.py` | 21 | `open()` modes behavior (rb/wb/ab/r+b/xb) |
187189
| `tests/integration/test_mkdir_listdir.py` | 32 | `mkdir`, `listdir`, `exists`, `is_dir`, `glob`, `walk` |
188190
| `tests/integration/test_rename_move.py` | 34 | `rename`, `move`, `copy`, `copy_tree` |
@@ -192,13 +194,16 @@ File breakdown of all tests (369 items):
192194
| `tests/integration/test_concurrency.py` | 7 | Multi-threaded concurrent access |
193195
| `tests/integration/test_async.py` | 22 | `AsyncMemoryFileSystem` asynchronous API |
194196
| `tests/integration/test_memory_guard_integration.py` | 12 | v14: MemoryGuard integration tests, MemoryError messages |
197+
| `tests/integration/test_archive_expand.py` | 19 | v15: `expand_archive()` atomic extraction integration tests |
198+
| `tests/integration/test_archive_streaming.py` | 20 | v15: `expand_archive_streaming()` streaming extraction integration tests |
195199
| `tests/scenarios/test_usecase_etl_staging.py` | 4 | ETL staging, incremental updates, concurrent writing |
196200
| `tests/scenarios/test_usecase_archive_like.py` | 5 | Archive operations, import_tree/export_tree roundtrip |
197201
| `tests/scenarios/test_usecase_sqlite_snapshot.py` | 3 | SQLite serialize/deserialize, quota limits |
198202
| `tests/scenarios/test_usecase_restricted_env.py` | 3 | Use cases in quota-restricted environments |
203+
| `tests/scenarios/test_usecase_archive_expand.py` | 3 | v15: UC-1 archive expand → transform → repack scenarios |
199204
| `tests/stress/test_threaded_stress.py` | 6 | Multi-threaded stress tests |
200205
| `tests/property/test_hypothesis.py` | 5 | Hypothesis property-based tests |
201-
| **Total** | **369** | |
206+
| **Total** | **436** | |
202207

203208

204209
See `.github/workflows/test.yml` for GitHub Actions settings.

TESTING_ja.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ tests/
6161
│ ├── test_text_handle.py
6262
│ ├── test_package_metadata.py
6363
│ ├── test_memory_info.py # v14: OS 依存メモリ検出
64-
│ └── test_memory_guard.py # v14: MemoryGuard 戦略
64+
│ ├── test_memory_guard.py # v14: MemoryGuard 戦略
65+
│ ├── test_archive_sanitize.py # v15: _sanitize_archive_path() Zip Slip 防止
66+
│ └── test_archive_adapter.py # v15: ArchiveAdapter 基底クラス・_detect()
6567
├── integration/ # MemoryFileSystem 公開 API の結合テスト
6668
│ ├── test_open_modes.py
6769
│ ├── test_mkdir_listdir.py
@@ -71,12 +73,15 @@ tests/
7173
│ ├── test_stats.py
7274
│ ├── test_concurrency.py
7375
│ ├── test_async.py # v11: AsyncMemoryFileSystem
74-
│ └── test_memory_guard_integration.py # v14: MemoryGuard 統合テスト
76+
│ ├── test_memory_guard_integration.py # v14: MemoryGuard 統合テスト
77+
│ ├── test_archive_expand.py # v15: expand_archive() 結合テスト
78+
│ └── test_archive_streaming.py # v15: expand_archive_streaming() 結合テスト
7579
├── scenarios/ # 代表ユースケースのシナリオテスト
7680
│ ├── test_usecase_sqlite_snapshot.py
7781
│ ├── test_usecase_etl_staging.py
7882
│ ├── test_usecase_archive_like.py
79-
│ └── test_usecase_restricted_env.py
83+
│ ├── test_usecase_restricted_env.py
84+
│ └── test_usecase_archive_expand.py # v15: UC-1 アーカイブ展開シナリオ
8085
├── stress/ # 負荷・ストレステスト
8186
│ └── test_threaded_stress.py
8287
├── property/ # Hypothesis プロパティベーステスト
@@ -167,7 +172,7 @@ uvx --with-requirements requirements.txt pdoc dmemfs -o docs/api
167172

168173
## テストファイル対応表
169174

170-
全テスト(369件)のファイル別内訳:
175+
全テスト(436件)のファイル別内訳:
171176

172177
| ファイルパス | テスト数 | カバー領域 |
173178
|---|---:|---|
@@ -184,6 +189,8 @@ uvx --with-requirements requirements.txt pdoc dmemfs -o docs/api
184189
| `tests/unit/test_package_metadata.py` | 1 | パッケージメタデータの整合性 |
185190
| `tests/unit/test_memory_info.py` | 8 | v14: `get_available_memory_bytes()` OS 依存メモリ検出 |
186191
| `tests/unit/test_memory_guard.py` | 8 | v14: `NullGuard`/`InitGuard`/`PerWriteGuard` 戦略パターン |
192+
| `tests/unit/test_archive_sanitize.py` | 12 | v15: `_sanitize_archive_path()` Zip Slip 防止 |
193+
| `tests/unit/test_archive_adapter.py` | 13 | v15: `ArchiveAdapter` 基底クラス・`can_handle()``_detect()` |
187194
| `tests/integration/test_open_modes.py` | 21 | `open()` モード(rb/wb/ab/r+b/xb)の動作 |
188195
| `tests/integration/test_mkdir_listdir.py` | 32 | `mkdir``listdir``exists``is_dir``glob``walk` |
189196
| `tests/integration/test_rename_move.py` | 34 | `rename``move``copy``copy_tree` |
@@ -193,13 +200,16 @@ uvx --with-requirements requirements.txt pdoc dmemfs -o docs/api
193200
| `tests/integration/test_concurrency.py` | 7 | マルチスレッド並行アクセス |
194201
| `tests/integration/test_async.py` | 22 | `AsyncMemoryFileSystem` 非同期 API |
195202
| `tests/integration/test_memory_guard_integration.py` | 12 | v14: MemoryGuard 統合テスト・MemoryError メッセージ |
203+
| `tests/integration/test_archive_expand.py` | 19 | v15: `expand_archive()` アトミック展開 結合テスト |
204+
| `tests/integration/test_archive_streaming.py` | 20 | v15: `expand_archive_streaming()` ストリーミング展開 結合テスト |
196205
| `tests/scenarios/test_usecase_etl_staging.py` | 4 | ETL ステージング・増分更新・並行書き込み |
197206
| `tests/scenarios/test_usecase_archive_like.py` | 5 | アーカイブ操作・import_tree/export_tree ラウンドトリップ |
198207
| `tests/scenarios/test_usecase_sqlite_snapshot.py` | 3 | SQLite serialize/deserialize・クォータ制限 |
199208
| `tests/scenarios/test_usecase_restricted_env.py` | 3 | クォータ制限環境でのユースケース |
209+
| `tests/scenarios/test_usecase_archive_expand.py` | 3 | v15: UC-1 アーカイブ展開→加工→再パック シナリオ |
200210
| `tests/stress/test_threaded_stress.py` | 6 | マルチスレッド負荷テスト |
201211
| `tests/property/test_hypothesis.py` | 5 | Hypothesis プロパティベーステスト |
202-
| **合計** | **369** | |
212+
| **合計** | **436** | |
203213

204214

205215
GitHub Actions の設定は `.github/workflows/test.yml` を参照。

dmemfs/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import TYPE_CHECKING
22

3+
from ._archive import ArchiveAdapter, expand_archive, expand_archive_streaming
34
from ._exceptions import MFSNodeLimitExceededError, MFSQuotaExceededError
45
from ._fs import MemoryFileSystem
56
from ._handle import MemoryFileHandle
@@ -30,5 +31,9 @@ def __getattr__(name: str): # type: ignore[no-untyped-def]
3031
"MFSTextHandle",
3132
"AsyncMemoryFileSystem",
3233
"AsyncMemoryFileHandle",
34+
# v15: archive expansion
35+
"ArchiveAdapter",
36+
"expand_archive",
37+
"expand_archive_streaming",
3338
]
34-
__version__ = "0.3.0"
39+
__version__ = "0.4.0"

0 commit comments

Comments
 (0)