### Install pre-commit and Hooks Source: https://github.com/barseghyanartur/safezip/blob/main/docs/contributor_guidelines.md Installs uv, pre-commit, and then installs the git hooks for pre-commit. This ensures contributions adhere to project code quality standards. ```sh curl -LsSf https://astral.sh/uv/install.sh | sh # Install uv uv tool install pre-commit # Install pre-commit pre-commit install # Install hooks ``` -------------------------------- ### Install Project Dependencies Source: https://github.com/barseghyanartur/safezip/blob/main/docs/contributor_guidelines.md Installs all necessary dependencies for the project. This command should be run after creating the virtual environment. ```sh make install ``` -------------------------------- ### Install safezip using uv Source: https://github.com/barseghyanartur/safezip/blob/main/README.rst Install the safezip package using uv. No additional packages are required. ```sh uv pip install safezip ``` -------------------------------- ### Install SafeZip using pip or uv Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Install the safezip package using either pip or uv. No additional packages are required. ```sh uv pip install safezip ``` ```sh pip install safezip ``` -------------------------------- ### Install safezip using pip Source: https://github.com/barseghyanartur/safezip/blob/main/README.rst Install the safezip package using pip. No additional packages are required. ```sh pip install safezip ``` -------------------------------- ### Recursive Extraction Example Source: https://github.com/barseghyanartur/safezip/blob/main/CHANGELOG.rst Demonstrates how to use `SafeZipFile` or `safe_extract` for recursive extraction of nested zip files. It highlights the `recursive` and `max_nesting_depth` parameters. ```python SafeZipFile(..., recursive=True, max_nesting_depth=3) ``` -------------------------------- ### Get SafeZip Version Source: https://github.com/barseghyanartur/safezip/blob/main/docs/full_llms.md Retrieves the installed version of the SafeZip library. Returns 'unknown' if the version cannot be imported. ```python def _version() -> str: try: from safezip import __version__ return __version__ except ImportError: return "unknown" ``` -------------------------------- ### Start Python Interpreter with UV Source: https://github.com/barseghyanartur/safezip/wiki/ZIP-files-to-test-with Launch a Python interpreter within the Docker container using the `uv` package manager. This environment is used to run the SafeZip extraction code. ```shell uv run python ``` -------------------------------- ### Configure custom extraction limits with SafeZipFile Source: https://github.com/barseghyanartur/safezip/blob/main/README.rst Customize security limits when using the `SafeZipFile` context manager. This example demonstrates setting limits for individual file size, total archive size, number of files, decompression ratios, nesting depth, and symlink policy. ```python from safezip import SafeZipFile, SymlinkPolicy with SafeZipFile( "path/to/file.zip", max_file_size=100 * 1024 * 1024, # 100 MiB per member (default: 1 GiB) max_total_size=500 * 1024 * 1024, # 500 MiB total (default: 5 GiB) max_files=1_000, # (default: 10 000) max_per_member_ratio=50.0, # (default: 200) max_total_ratio=50.0, # (default: 200) max_nesting_depth=1, # (default: 3) symlink_policy=SymlinkPolicy.IGNORE, # (default: SymlinkPolicy.REJECT) ) as zf: ``` -------------------------------- ### Get SafeZip Version Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Retrieves the installed version of the SafeZip library. Returns 'unknown' if the version cannot be determined. ```python def _version() -> str: try: from safezip import __version__ return __version__ except ImportError: return "unknown" ``` -------------------------------- ### Test Compression Ratio Error with big_zipbomb.zip Source: https://github.com/barseghyanartur/safezip/wiki/ZIP-files-to-test-with This example demonstrates how safe_extract handles a ZIP bomb with an excessively high compression ratio, triggering a CompressionRatioError. Ensure the zip file is placed at the specified path. ```python >>> from safezip import safe_extract >>> safe_extract("/app/big_zipbomb.zip", "/app/output", recursive=True) Traceback (most recent call last): File "", line 1, in safe_extract("/app/big_zipbomb.zip", "/app/output", recursive=True) File "/app/src/safezip/_core.py", line 475, in safe_extract zf.extractall(destination) ~~~~~~~~~~~~~^^^^^^^^^^^^^ File "/app/src/safezip/_core.py", line 279, in extractall self._extract_one(info, base, counters, effective_pwd) ~~~~~~~~~~~~~~~~~ File "/app/src/safezip/_core.py", line 376, in _extract_one nested_zf.extractall(nested_dest, pwd=pwd) ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^ File "/app/src/safezip/_core.py", line 279, in extractall self._extract_one(info, base, counters, effective_pwd) ~~~~~~~~~~~~~~~~~ File "/app/src/safezip/_core.py", line 349, in _extract_one stream_extract_member( ~~~~~~~~~~~~~~~~~~~~~ self._zf, ^^^^^^^^^ ... pwd=pwd, ^^^^^^^^ ) File "/app/src/safezip/_streamer.py", line 128, in stream_extract_member raise CompressionRatioError( ... safezip._exceptions.CompressionRatioError: Cumulative compression ratio 550.7:1 exceeds max_total_ratio=200.0:1. ``` -------------------------------- ### Clone Project and Enter Directory Source: https://github.com/barseghyanartur/safezip/wiki/ZIP-files-to-test-with Clone the SafeZip repository and navigate into the project directory on your host system. This is a prerequisite for setting up the testing environment. ```shell git clone https://github.com/barseghyanartur/safezip.git && cd safezip ``` -------------------------------- ### Global Configuration via Environment Variables Source: https://context7.com/barseghyanartur/safezip/llms.txt Configure security limits globally using environment variables. Constructor arguments take precedence over environment variables, which in turn override built-in defaults. ```bash # Tighten limits for a high-security deployment export SAFEZIP_MAX_FILE_SIZE=52428800 # 50 MiB export SAFEZIP_MAX_TOTAL_SIZE=209715200 # 200 MiB export SAFEZIP_MAX_FILES=200 export SAFEZIP_MAX_PER_MEMBER_RATIO=50.0 export SAFEZIP_MAX_TOTAL_RATIO=50.0 export SAFEZIP_MAX_NESTING_DEPTH=1 export SAFEZIP_SYMLINK_POLICY=reject # reject | ignore | resolve_internal export SAFEZIP_RECURSIVE=false # true | false ``` -------------------------------- ### Build Package for Release Source: https://github.com/barseghyanartur/safezip/blob/main/docs/contributor_guidelines.md Compiles the project into a distributable package format, preparing it for release. ```sh make package-build ``` -------------------------------- ### Initialize SafeZipFile Context Manager Source: https://context7.com/barseghyanartur/safezip/llms.txt Use `SafeZipFile` as a context manager for more granular control over ZIP archive access. The Guard phase runs on initialization, blocking malformed or oversized archives early. Configure various security parameters, including password, symlink policy, and event callbacks. ```python import zipfile from safezip import SafeZipFile, SymlinkPolicy, SecurityEvent from safezip._exceptions import ( SafezipError, UnsafeZipError, FileSizeExceededError, TotalSizeExceededError, CompressionRatioError, FileCountExceededError, NestingDepthError, MalformedArchiveError, ) events = [] with SafeZipFile( "archive.zip", max_file_size=100 * 1024 * 1024, # 100 MiB max_total_size=500 * 1024 * 1024, # 500 MiB max_files=1_000, max_per_member_ratio=100.0, max_total_ratio=100.0, max_nesting_depth=2, symlink_policy=SymlinkPolicy.IGNORE, password=b"s3cr3t", on_security_event=lambda e: events.append(e.event_type), recursive=True, strip_special_bits=True, ) as zf: # Inspect without extracting print("Members:", zf.namelist()) # Members: ['data/report.csv', 'images/logo.png'] info = zf.getinfo("data/report.csv") print(f"Compressed: {info.compress_size}, Uncompressed: {info.file_size}") # Extract a single member dest_path = zf.extract("data/report.csv", "/tmp/out/") print(f"Extracted to: {dest_path}") # Extracted to: /tmp/out/data/report.csv # Extract all remaining members zf.extractall("/tmp/out/") print("Blocked events:", events) # Blocked events: [] ``` -------------------------------- ### Get Archive Stem - Python Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Strips the archive extension from a filename to get the base stem. Handles common archive extensions like .zip, .jar, .whl, etc. Non-archive extensions are returned unchanged. ```python def _archive_stem(name: str) -> str: """Strip the archive extension from *name*, returning the base stem. Handles single extensions only (ZIP archives do not use compound extensions like .tar.gz), but normalises consistently. Examples:: archive.zip → archive lib.whl → lib app.jar → app data.csv → data.csv (non-archive extension unchanged) """ p = Path(name) if p.suffix.lower() in _ARCHIVE_EXTENSIONS: return p.stem return name ``` -------------------------------- ### Test Overlap at Offset Zero Source: https://github.com/barseghyanartur/safezip/blob/main/docs/full_llms.md Ensures that overlapping entries starting at data_start=0 are still detected as bombs. ```python def test_overlap_at_offset_zero(self): """Entries with data_start=0 should still be detected as overlapping.""" lfh1 = _lfh(b"a", b"data") ``` -------------------------------- ### Create Virtual Environment Source: https://github.com/barseghyanartur/safezip/blob/main/docs/contributor_guidelines.md Creates a virtual environment for the project. This is a standard step for managing project dependencies. ```sh make create-venv ``` -------------------------------- ### Get SafeZipFile Info List Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Returns a list of ZipInfo objects for all members in the archive. This provides metadata for each file. ```python def infolist(self) -> list: """Return a list of ZipInfo objects for all archive members.""" return self._zf.infolist() ``` -------------------------------- ### SafeZip Documentation Structure Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Example of the reStructuredText (RST) markup used for structuring documentation, including different heading levels. ```text ===== title ===== header ====== sub-header ---------- sub-sub-header ~~~~~~~~~~~~~~ sub-sub-sub-header ^^^^^^^^^^^^^^^^^^ sub-sub-sub-sub-header ++++++++++++++++++++++ sub-sub-sub-sub-sub-header ************************** ``` -------------------------------- ### Make Test Release Source: https://github.com/barseghyanartur/safezip/blob/main/docs/contributor_guidelines.md Uploads the built package to the Python Package Index test server (test.pypi.org) for preliminary testing. ```sh make test-release ``` -------------------------------- ### Get SafeZipFile Member Info Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Retrieves the ZipInfo object for a specific member by its name. Useful for inspecting individual file metadata. ```python def getinfo(self, name: str) -> zipfile.ZipInfo: """Return a ZipInfo object for *name*. """ return self._zf.getinfo(name) ``` -------------------------------- ### Initialize SafeZip Tests Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Standard Python module initialization for tests. ```python """Tests for safezip.""" __author__ = "Artur Barseghyan " __copyright__ = "2026 Artur Barseghyan" __license__ = "MIT" ``` -------------------------------- ### Get SafeZipFile Member Names Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Returns a list of all member names within the zip archive. This is a safe subset of zipfile.ZipFile functionality. ```python def namelist(self) -> list: """Return a list of archive member names.""" return self._zf.namelist() ``` -------------------------------- ### Configure safezip with custom limits Source: https://github.com/barseghyanartur/safezip/blob/main/AGENTS.md Instantiate `SafeZipFile` with explicit limits for file size, total size, file count, compression ratios, nesting depth, and symlink policy. These can be overridden by environment variables. ```python from safezip import SafeZipFile, SymlinkPolicy SafeZipFile( "path/to/file.zip", max_file_size=1 * 1024**3, # 1 GiB per member max_total_size=5 * 1024**3, # 5 GiB total max_files=10_000, max_per_member_ratio=200.0, max_total_ratio=200.0, max_nesting_depth=3, symlink_policy=SymlinkPolicy.REJECT, ) ``` -------------------------------- ### CumulativeCounters Class Source: https://github.com/barseghyanartur/safezip/blob/main/docs/full_llms.md Tracks cumulative decompressed and compressed bytes across multiple zip members during extraction. Initialize this object before starting the extraction process and pass it to `stream_extract_member`. ```python class CumulativeCounters: """Tracks totals across all members in a single extractall/extract call.""" __slots__ = ("bytes_written", "compressed_bytes") def __init__(self) -> None: self.bytes_written: int = 0 self.compressed_bytes: int = 0 ``` -------------------------------- ### Verify Symlink Chain Integrity Source: https://github.com/barseghyanartur/safezip/blob/main/docs/full_llms.md Recursively verifies that a symlink chain, starting from a given path, stays entirely within the specified base directory. Detects and rejects cycles. ```python from pathlib import Path def _verify_symlink_chain(link_path: Path, base: Path) -> None: """Verify the full symlink chain from *link_path* stays inside *base*. Follows every link until a non-symlink is reached or an escape is detected. :raises UnsafeZipError: If any link in the chain exits *base*. """ visited = set() current = link_path while current.is_symlink(): real = str(current.resolve()) if real in visited: # Cycle detected; treat as unsafe raise UnsafeZipError( f"Symlink cycle detected at {current}: refusing to follow further." ) visited.add(real) try: ``` -------------------------------- ### Check Overlapping Files Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Identifies overlapping file data intervals within a list of file entries. Sorts entries by their start data offset to efficiently detect overlaps. ```python def check_overlapping_files( entries: List[FileEntry], ) -> List[Tuple[FileEntry, FileEntry]]: if not entries: return [] sorted_e = sorted(entries, key=lambda e: e.data_start) overlaps: List[Tuple[FileEntry, FileEntry]] = [] max_end = sorted_e[0].data_end max_end_entry = sorted_e[0] for e in sorted_e[1:]: if e.data_start < max_end: overlaps.append((max_end_entry, e)) if e.data_end > max_end: max_end = e.data_end max_end_entry = e return overlaps ``` -------------------------------- ### Initialize SafeZipFile with Limits Source: https://github.com/barseghyanartur/safezip/blob/main/docs/full_llms.md Initializes a SafeZipFile instance, applying security limits for file size, total size, file count, and nesting depth. Limits can be set via constructor arguments or environment variables. It also performs an initial archive validation upon opening. ```python class SafeZipFile: """A hardened, composition-based wrapper around :class:`zipfile.ZipFile`. All defences are enabled by default. Limits can be relaxed by passing explicit constructor arguments or by setting environment variables. .. note:: This class intentionally does **not** expose ``open()``, ``read()``, or any write-mode methods from the underlying ``zipfile.ZipFile``. Callers needing lower-level access must use ``zipfile.ZipFile`` directly, accepting the associated risks. """ def __init__( self, file: Union[str, os.PathLike, BinaryIO], mode: str = "r", *, max_file_size: Optional[int] = None, max_total_size: Optional[int] = None, max_files: Optional[int] = None, max_per_member_ratio: Optional[float] = None, max_total_ratio: Optional[float] = None, max_nesting_depth: Optional[int] = None, symlink_policy: Optional[SymlinkPolicy] = None, password: Optional[bytes] = None, on_security_event: SecurityEventCallback = None, _nesting_depth: int = 0, recursive: Optional[bool] = None, strip_special_bits: bool = True, ) -> None: # Resolve limits: constructor arg > env var > module-level default # Env vars are read at runtime to support test monkeypatching self._max_file_size = ( max_file_size if max_file_size is not None else _env_int("SAFEZIP_MAX_FILE_SIZE", _DEFAULT_MAX_FILE_SIZE) ) self._max_total_size = ( max_total_size if max_total_size is not None else _env_int("SAFEZIP_MAX_TOTAL_SIZE", _DEFAULT_MAX_TOTAL_SIZE) ) self._max_files = ( max_files if max_files is not None else _env_int("SAFEZIP_MAX_FILES", _DEFAULT_MAX_FILES) ) self._max_per_member_ratio = ( max_per_member_ratio if max_per_member_ratio is not None else _env_float( "SAFEZIP_MAX_PER_MEMBER_RATIO", _DEFAULT_MAX_PER_MEMBER_RATIO ) ) self._max_total_ratio = ( max_total_ratio if max_total_ratio is not None else _env_float("SAFEZIP_MAX_TOTAL_RATIO", _DEFAULT_MAX_TOTAL_RATIO) ) self._max_nesting_depth = ( max_nesting_depth if max_nesting_depth is not None else _env_int("SAFEZIP_MAX_NESTING_DEPTH", _DEFAULT_MAX_NESTING_DEPTH) ) self._symlink_policy = ( symlink_policy if symlink_policy is not None else _env_symlink_policy(_DEFAULT_SYMLINK_POLICY) ) self._recursive = ( recursive if recursive is not None else _env_bool("SAFEZIP_RECURSIVE", _DEFAULT_RECURSIVE) ) self._strip_special_bits = strip_special_bits self._password = password self._on_security_event = on_security_event self._archive_hash = _archive_hash(file) self._nesting_depth = _nesting_depth if _nesting_depth > self._max_nesting_depth: self._emit_event("nesting_depth_exceeded") log.warning( "Nesting depth limit exceeded", extra={ "event": "nesting_depth_exceeded", "nesting_depth": _nesting_depth, "max_nesting_depth": self._max_nesting_depth, "archive_hash": self._archive_hash, }, ) raise NestingDepthError( f"Nested archive depth {_nesting_depth} exceeds " f"max_nesting_depth={self._max_nesting_depth}." ) try: self._zf = zipfile.ZipFile(file, mode) except zipfile.BadZipFile as exc: raise MalformedArchiveError(f"Cannot open archive: {exc}") from exc # Run the Guard immediately on open try: validate_archive( self._zf, self._max_files, self._max_file_size, self._max_total_size ) except FileCountExceededError: self._emit_event("file_count_exceeded") raise except FileSizeExceededError: self._emit_event("declared_size_exceeded") raise except MalformedArchiveError: self._emit_event("malformed_archive") raise ``` -------------------------------- ### Coverage.py Configuration Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Configure coverage.py for reporting, including relative file paths and source directories. ```toml [tool.coverage.run] relative_files = true omit = [".tox/*"] source = ["safezip"] [tool.coverage.report] show_missing = true exclude_lines = [ "pragma: no cover", "@overload", ] ``` -------------------------------- ### Context Manager Methods Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Methods for using SafeZipFile as a context manager. ```APIDOC ## __enter__ ### Description Enter the runtime context related to this object. ### Method __enter__ ### Returns - self (SafeZipFile): The SafeZipFile object itself. ``` ```APIDOC ## __exit__ ### Description Exit the runtime context related to this object. ### Method __exit__ ### Arguments - *args: object: Arguments passed to the exit method. ### Returns - None ``` -------------------------------- ### Resolve Data Intervals Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Resolves the start and end data intervals for each file entry in a zip archive by reading the local file header. Handles cases where the header might be malformed or missing. ```python LFH_FIXED = 30 def resolve_data_intervals(mm: mmap.mmap, entries: List[FileEntry]) -> None: lfh_sig = b"PK\x03\x04" file_size = mm.size() for e in entries: if e.header_offset + LFH_FIXED > file_size: e.data_start = e.header_offset e.data_end = e.header_offset + e.compressed_size continue mm.seek(e.header_offset) lfh = mm.read(LFH_FIXED) if len(lfh) < LFH_FIXED or lfh[:4] != lfh_sig: e.data_start = e.header_offset e.data_end = e.header_offset + e.compressed_size continue lfh_fname_len = struct.unpack_from(">> safe_extract("/app/zblg_BAMSOFTWARE.zip", "/app/output", recursive=True) Traceback (most recent call last): File "", line 1, in safe_extract("/app/zblg_BAMSOFTWARE.zip", "/app/output", recursive=True) File "/app/src/safezip/_core.py", line 474, in safe_extract with SafeZipFile(archive, **kwargs) as zf: ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^ File "/app/src/safezip/_core.py", line 179, in __init__ validate_archive(self._zf, self._max_files, self._max_file_size) ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/app/src/safezip/_guard.py", line 149, in validate_archive raise FileCountExceededError( ... safezip._exceptions.FileCountExceededError: Archive contains 65,534 entries, which exceeds the limit of 10,000. ``` -------------------------------- ### Sphinx Source Tree Configuration Source: https://github.com/barseghyanartur/safezip/blob/main/docs/full_llms.md Configure Sphinx to generate a source tree documentation, specifying ignored files and ordering. ```toml [tool.sphinx-source-tree] ignore = [ "*.egg-info", "*.py,cover", "*.pyc", "*.pyo", ".DS_Store", ".coverage", ".coverage.*", ".git", ".hg", ".hypothesis", ".idea", ".mypy_cache", ".nox", ".pre-commit-config.yaml", ".pre-commit-hooks.yaml", ".pytest_cache", ".readthedocs.yaml", ".ruff_cache", ".secrets.baseline", ".svn", ".tox", ".venv", ".vscode", "CHANGELOG.rst", "CODE_OF_CONDUCT.rst", "LICENSE", "SECURITY.rst", "Thumbs.db", "__pycache__", "build", "codebin", "dist", "docs/Makefile", "docs/_build", "docs/_static", "docs/changelog.rst", "docs/code_of_conduct.rst", "docs/customization", "docs/make.bat", "docs/requirements.txt", "docs/security.rst", "docs/source_tree.rst", "docs/source_tree_full.rst", "env", "htmlcov", "node_modules", "venv", "ARCHITECTURE.rst", ".coderabbit.yaml", ".coveralls", "docs/full-llms.rst", "docs/llms.rst", "docs/contributor_guidelines.rst", "docs/package.rst", "docs/documentation.rst", "docs/index.rst", ] order = [ "README.rst", "CONTRIBUTING.rst", "AGENTS.md", ] [[tool.sphinx-source-tree.files]] output = "docs/full_llms.rst" title = "Full project source-tree" [[tool.sphinx-source-tree.files]] output = "docs/llms.rst" title = "Project source-tree" ignore = [ "*.egg-info", "*.py,cover", "*.pyc", "*.pyo", ".DS_Store", ".coverage", ".coverage.*", ".git", ".hg", ".hypothesis", ".idea", ".mypy_cache", ".nox", ".pre-commit-config.yaml", ".pre-commit-hooks.yaml", ".pytest_cache", ".readthedocs.yaml", ".ruff_cache", ".secrets.baseline", ".svn", ".tox", ".venv", ".vscode", "CHANGELOG.rst", "CODE_OF_CONDUCT.rst", "LICENSE", "SECURITY.rst", "Thumbs.db", "__pycache__", "build", "codebin", "dist", "docs/Makefile", "docs/_build", "docs/_static", "docs/changelog.rst", "docs/code_of_conduct.rst", "docs/customization", "docs/make.bat", "docs/requirements.txt", "docs/security.rst", "docs/source_tree.rst", "docs/source_tree_full.rst", "env", "htmlcov", "node_modules", "venv", "examples", "docs", "ARCHITECTURE.rst", ".coderabbit.yaml", ".coveralls", "docs/full-llms.rst", "docs/llms.rst", "docs/contributor_guidelines.rst", "docs/package.rst", "docs/documentation.rst", "docs/index.rst", ] ``` -------------------------------- ### Sphinx Source Tree Configuration Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Configure Sphinx to generate a source tree documentation, specifying ignored files and output paths. ```toml [tool.sphinx-source-tree] ignore = [ "*.egg-info", "*.py,cover", "*.pyc", "*.pyo", ".DS_Store", ".coverage", ".coverage.*", ".git", ".hg", ".hypothesis", ".idea", ".mypy_cache", ".nox", ".pre-commit-config.yaml", ".pre-commit-hooks.yaml", ".pytest_cache", ".readthedocs.yaml", ".ruff_cache", ".secrets.baseline", ".svn", ".tox", ".venv", ".vscode", "CHANGELOG.rst", "CODE_OF_CONDUCT.rst", "LICENSE", "SECURITY.rst", "Thumbs.db", "__pycache__", "build", "codebin", "dist", "docs/Makefile", "docs/_build", "docs/_static", "docs/changelog.rst", "docs/code_of_conduct.rst", "docs/customization", "docs/make.bat", "docs/requirements.txt", "docs/security.rst", "docs/source_tree.rst", "docs/source_tree_full.rst", "env", "htmlcov", "node_modules", "venv", "ARCHITECTURE.rst", ".coderabbit.yaml", ".coveralls", "docs/full-llms.rst", "docs/llms.rst", "docs/contributor_guidelines.rst", "docs/package.rst", "docs/documentation.rst", "docs/index.rst", ] order = [ "README.rst", "CONTRIBUTING.rst", "AGENTS.md", ] [[tool.sphinx-source-tree.files]] output = "docs/full_llms.rst" title = "Full project source-tree" [[tool.sphinx-source-tree.files]] output = "docs/llms.rst" title = "Project source-tree" ignore = [ "*.egg-info", "*.py,cover", "*.pyc", "*.pyo", ".DS_Store", ".coverage", ".coverage.*", ".git", ".hg", ".hypothesis", ".idea", ".mypy_cache", ".nox", ".pre-commit-config.yaml", ".pre-commit-hooks.yaml", ".pytest_cache", ".readthedocs.yaml", ".ruff_cache", ".secrets.baseline", ".svn", ".tox", ".venv", ".vscode", "CHANGELOG.rst", "CODE_OF_CONDUCT.rst", "LICENSE", "SECURITY.rst", "Thumbs.db", "__pycache__", "build", "codebin", "dist", "docs/Makefile", "docs/_build", "docs/_static", "docs/changelog.rst", "docs/code_of_conduct.rst", "docs/customization", "docs/make.bat", "docs/requirements.txt", "docs/security.rst", "docs/source_tree.rst", "docs/source_tree_full.rst", "env", "htmlcov", "node_modules", "venv", "examples", "docs", "ARCHITECTURE.rst", ".coderabbit.yaml", ".coveralls", "docs/full-llms.rst", "docs/llms.rst", "docs/contributor_guidelines.rst", "docs/package.rst", "docs/documentation.rst", "docs/index.rst", ] ``` -------------------------------- ### Running Tests Source: https://github.com/barseghyanartur/safezip/blob/main/docs/index.md Execute the test suite using the provided make commands. Tests are run inside Docker to ensure isolation. Specific Python versions can be targeted using `make test-env`. ```sh make test ``` ```sh make test-env ENV=py312 ``` -------------------------------- ### SafeZipFile Read-Only Inspection Methods Source: https://github.com/barseghyanartur/safezip/blob/main/docs/full_llms.md Provides safe access to zip file information without extraction. Includes methods to list member names, get ZipInfo objects, and retrieve specific member details. ```python def namelist(self) -> list: """Return a list of archive member names.""" return self._zf.namelist() def infolist(self) -> list: """Return a list of ZipInfo objects for all archive members.""" return self._zf.infolist() def getinfo(self, name: str) -> zipfile.ZipInfo: """Return a ZipInfo object for *name*. """ return self._zf.getinfo(name) ``` -------------------------------- ### Test File Size Exactly at Limit Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Verifies that an archive with a file size exactly at the limit is accepted. ```python def test_size_exactly_at_limit_passes(self, tmp_path): buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("data.bin", b"A" * 100) p = tmp_path / "exact.zip" p.write_bytes(buf.getvalue()) with SafeZipFile(p, max_file_size=100): pass ``` -------------------------------- ### Check Extra Field Quoting Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Detects potential quoting issues in extra fields of zip file entries. It checks if the extra field length is positive and if the data starts before the next entry's header offset. ```python def check_extra_field_quoting(entries: List[FileEntry]) -> List[FileEntry]: if not entries: return [] sorted_e = sorted(entries, key=lambda e: e.header_offset) suspicious: List[FileEntry] = [] for i, e in enumerate(sorted_e[:-1]): next_e = sorted_e[i + 1] eff_extra = e.lfh_extra_len if e.lfh_extra_len >= 0 else e.cdh_extra_len if eff_extra > 0 and e.data_start >= next_e.header_offset: suspicious.append(e) return suspicious ``` -------------------------------- ### Test Basic Extraction Source: https://github.com/barseghyanartur/safezip/blob/main/docs/llms.md Verifies that basic extraction of a ZIP archive to a destination directory works correctly, including nested directories. Asserts that the extracted files have the expected content and that the destination directory is created. ```python def test_extract_basic(self, simple_archive, tmp_path, capsys): """Basic extraction works.""" dest = tmp_path / "out" with patch("sys.argv", ["safezip", "extract", str(simple_archive), str(dest)]): with pytest.raises(SystemExit) as exc_info: main() assert exc_info.value.code == 0 assert (dest / "file1.txt").read_text() == "content1\n" assert (dest / "dir" / "file2.txt").read_text() == "content2\n" captured = capsys.readouterr() assert "Extracted to" in captured.out ``` -------------------------------- ### Test Nesting Depth Error with huge_zipbomb.zip Source: https://github.com/barseghyanartur/safezip/wiki/ZIP-files-to-test-with This example shows how safe_extract handles a deeply nested ZIP bomb, triggering a NestingDepthError. The archive depth exceeds the configured maximum. Ensure the zip file is present at the specified path. ```python >>> safe_extract("/app/huge_zipbomb.zip", "/app/output", recursive=True) Traceback (most recent call last): File "", line 1, in safe_extract("/app/huge_zipbomb.zip", "/app/output", recursive=True) File "/app/src/safezip/_core.py", line 475, in safe_extract zf.extractall(destination) ~~~~~~~~~~~~~^^^^^^^^^^^^^ File "/app/src/safezip/_core.py", line 279, in extractall self._extract_one(info, base, counters, effective_pwd) ~~~~~~~~~~~~~~~~~ File "/app/src/safezip/_core.py", line 376, in _extract_one nested_zf.extractall(nested_dest, pwd=pwd) ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^ File "/app/src/safezip/_core.py", line 279, in extractall self._extract_one(info, base, counters, effective_pwd) ~~~~~~~~~~~~~~~~~ File "/app/src/safezip/_core.py", line 376, in _extract_one nested_zf.extractall(nested_dest, pwd=pwd) ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^ File "/app/src/safezip/_core.py", line 279, in extractall self._extract_one(info, base, counters, effective_pwd) ~~~~~~~~~~~~~~~~~ File "/app/src/safezip/_core.py", line 376, in _extract_one nested_zf.extractall(nested_dest, pwd=pwd) ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^ File "/app/src/safezip/_core.py", line 279, in extractall self._extract_one(info, base, counters, effective_pwd) ~~~~~~~~~~~~~~~~~ File "/app/src/safezip/_core.py", line 362, in _extract_one with SafeZipFile( ~~~~~~~~~~~^ tmp, ^^^^ ... _nesting_depth=self._nesting_depth + 1, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) as nested_zf: File "/app/src/safezip/_core.py", line 167, in __init__ raise NestingDepthError( ... safezip._exceptions.NestingDepthError: Nested archive depth 4 exceeds max_nesting_depth=3. ``` -------------------------------- ### Resolve Data Intervals for File Entries Source: https://github.com/barseghyanartur/safezip/blob/main/docs/full_llms.md Resolves the start and end points of compressed data for each file entry in a zip archive. It reads the local file header to determine lengths and offsets, handling potential errors and edge cases. ```python def resolve_data_intervals(mm: mmap.mmap, entries: List[FileEntry]) -> None: lfh_sig = b"PK\x03\x04" file_size = mm.size() for e in entries: if e.header_offset + LFH_FIXED > file_size: e.data_start = e.header_offset e.data_end = e.header_offset + e.compressed_size continue mm.seek(e.header_offset) lfh = mm.read(LFH_FIXED) if len(lfh) < LFH_FIXED or lfh[:4] != lfh_sig: e.data_start = e.header_offset e.data_end = e.header_offset + e.compressed_size continue lfh_fname_len = struct.unpack_from(" self._max_nesting_depth: self._emit_event("nesting_depth_exceeded") log.warning( "Nesting depth limit exceeded", extra={ "event": "nesting_depth_exceeded", "nesting_depth": _nesting_depth, "max_nesting_depth": self._max_nesting_depth, "archive_hash": self._archive_hash, }, ) raise NestingDepthError( f"Nested archive depth {_nesting_depth} exceeds " f"max_nesting_depth={self._max_nesting_depth}." ) try: self._zf = zipfile.ZipFile(file, mode) except zipfile.BadZipFile as exc: raise MalformedArchiveError(f"Cannot open archive: {exc}") from exc # Run the Guard immediately on open try: validate_archive( self._zf, self._max_files, self._max_file_size, self._max_total_size ) except FileCountExceededError: self._emit_event("file_count_exceeded") raise except FileSizeExceededError: self._emit_event("declared_size_exceeded") raise except MalformedArchiveError: self._emit_event("malformed_archive") raise ``` -------------------------------- ### SafeZipFile Constructor Source: https://github.com/barseghyanartur/safezip/blob/main/docs/full_llms.md Initializes a SafeZipFile object with configurable security limits and policies. All defences are enabled by default and can be relaxed via constructor arguments or environment variables. ```APIDOC ## SafeZipFile.__init__ ### Description Initializes a SafeZipFile object, applying security checks to the zip archive. ### Method __init__ ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body - **file** (Union[str, os.PathLike, BinaryIO]) - Path to the zip file or a file-like object. - **mode** (str) - Mode to open the zip file (default: 'r'). - **max_file_size** (Optional[int]) - Maximum size for individual files within the archive. - **max_total_size** (Optional[int]) - Maximum total size of the archive. - **max_files** (Optional[int]) - Maximum number of files allowed in the archive. - **max_per_member_ratio** (Optional[float]) - Maximum ratio of a member's size to the total archive size. - **max_total_ratio** (Optional[float]) - Maximum ratio of the total archive size to its declared size. - **max_nesting_depth** (Optional[int]) - Maximum allowed nesting depth for zip archives. - **symlink_policy** (Optional[SymlinkPolicy]) - Policy for handling symbolic links. - **password** (Optional[bytes]) - Password for encrypted zip archives. - **on_security_event** (SecurityEventCallback) - Callback function for security events. - **_nesting_depth** (int) - Internal parameter for tracking nesting depth. - **recursive** (Optional[bool]) - Whether to process recursively (default: from env var SAFEZIP_RECURSIVE). - **strip_special_bits** (bool) - Whether to strip special bits from file headers (default: True). ### Request Example ```python from safezip import SafeZipFile # Example with default limits with SafeZipFile('my_archive.zip') as sf: pass # Example with custom limits with SafeZipFile( 'my_archive.zip', max_file_size=1024*1024, # 1MB max_files=100 ) as sf: pass ``` ### Response #### Success Response (None) Initializes the SafeZipFile object. Raises exceptions on security violations or invalid zip files. #### Response Example None ``` -------------------------------- ### Handling SafeZip Security Exceptions Source: https://context7.com/barseghyanartur/safezip/llms.txt Demonstrates how to catch various Safezip security violations using a hierarchy of exceptions. Allows for specific handling of different attack vectors like path traversal, ZIP bombs, or excessive file sizes, with a general catch-all for any SafezipError. ```python from safezip import ( safe_extract, SafezipError, UnsafeZipError, CompressionRatioError, FileSizeExceededError, TotalSizeExceededError, FileCountExceededError, NestingDepthError, MalformedArchiveError, ) try: safe_extract("untrusted.zip", "/var/output/") except UnsafeZipError as e: # Log and discard — never trust this archive again print(f"Traversal/symlink attack: {e}") except CompressionRatioError as e: print(f"ZIP bomb: {e}") except FileSizeExceededError as e: print(f"Member too large: {e}") except TotalSizeExceededError as e: print(f"Total too large: {e}") except FileCountExceededError as e: print(f"Too many files: {e}") except NestingDepthError as e: print(f"Nested archive too deep: {e}") except MalformedArchiveError as e: print(f"Corrupt or crafted archive: {e}") except SafezipError as e: # Catch-all for any future safezip violation print(f"Archive rejected: {e}") ```