| 12 | |
| 13 | |
| 14 | def extract_range( |
| 15 | source_path: str, |
| 16 | offset: int, |
| 17 | length: int, |
| 18 | output_path: str, |
| 19 | *, |
| 20 | chunk_size: int = 1024 * 1024, |
| 21 | ) -> None: |
| 22 | _validate_range(offset, length, chunk_size) |
| 23 | source_fd = _open_source(source_path) |
| 24 | position = offset |
| 25 | remaining = length |
| 26 | |
| 27 | try: |
| 28 | with open(output_path, "wb") as output_file: |
| 29 | while remaining > 0: |
| 30 | to_read = min(chunk_size, remaining) |
| 31 | chunk = _safe_pread(source_fd, to_read, position, source_path) |
| 32 | if not chunk: |
| 33 | break |
| 34 | output_file.write(chunk) |
| 35 | read_len = len(chunk) |
| 36 | remaining -= read_len |
| 37 | position += read_len |
| 38 | except PermissionError as error: |
| 39 | raise BlockExtractionError( |
| 40 | f"Cannot write output file {output_path}: {error}", |
| 41 | f"Permission denied: cannot write {output_path}.", |
| 42 | ) from error |
| 43 | except OSError as error: |
| 44 | raise BlockExtractionError( |
| 45 | f"Cannot write output file {output_path}: {error}", |
| 46 | f"Cannot write output file {output_path}.", |
| 47 | ) from error |
| 48 | finally: |
| 49 | os.close(source_fd) |
| 50 | |
| 51 | if remaining > 0: |
| 52 | # Partial reads at EOF/device end are treated as explicit failures so |
| 53 | # callers never mistake truncated output for a successful extraction. |
| 54 | raise BlockExtractionError( |
| 55 | "Requested range exceeds available data.", |
| 56 | f"Cannot read requested range from {source_path}: reached end of file/device.", |
| 57 | ) |
| 58 | |
| 59 | |
| 60 | def read_block(source_path: str, block_size: int, block_index: int) -> bytes: |