omnizip 0.3.19 → 0.3.21
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +95 -3
- data/TODO.refactor/00-overview.md +2 -2
- data/TODO.refactor/07-respond-to-replacement.md +225 -79
- data/lib/omnizip/algorithm.rb +2 -2
- data/lib/omnizip/algorithms/bzip2.rb +1 -0
- data/lib/omnizip/algorithms/deflate.rb +1 -0
- data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +3 -3
- data/lib/omnizip/algorithms/lzma.rb +11 -5
- data/lib/omnizip/algorithms/lzma2/encoder.rb +2 -0
- data/lib/omnizip/algorithms/lzma2/xz_encoder_adapter.rb +2 -0
- data/lib/omnizip/algorithms/lzma2.rb +2 -0
- data/lib/omnizip/algorithms/ppmd7.rb +2 -0
- data/lib/omnizip/algorithms/ppmd_base.rb +2 -0
- data/lib/omnizip/algorithms/registration.rb +1 -1
- data/lib/omnizip/algorithms/zstandard/encoder.rb +57 -18
- data/lib/omnizip/algorithms/zstandard/match_finder.rb +9 -5
- data/lib/omnizip/algorithms/zstandard.rb +1 -0
- data/lib/omnizip/chunked/writer.rb +3 -0
- data/lib/omnizip/commands/archive_extract_command.rb +3 -1
- data/lib/omnizip/commands/archive_list_command.rb +4 -3
- data/lib/omnizip/commands/profile_show_command.rb +2 -0
- data/lib/omnizip/convenience.rb +2 -0
- data/lib/omnizip/extraction/filter_chain.rb +4 -0
- data/lib/omnizip/extraction/selective_extractor.rb +7 -0
- data/lib/omnizip/file_type.rb +4 -0
- data/lib/omnizip/formats/ole.rb +2 -0
- data/lib/omnizip/formats/rar/decompressor.rb +1 -0
- data/lib/omnizip/formats/rar/license_validator.rb +2 -0
- data/lib/omnizip/formats/rar/reader.rb +1 -2
- data/lib/omnizip/formats/rpm.rb +1 -0
- data/lib/omnizip/formats/seven_zip/writer.rb +8 -0
- data/lib/omnizip/formats/tar/reader.rb +2 -0
- data/lib/omnizip/formats/xar/reader.rb +2 -0
- data/lib/omnizip/formats/xar/writer.rb +1 -0
- data/lib/omnizip/formats/xz.rb +2 -0
- data/lib/omnizip/formats/xz_impl/block_decoder.rb +3 -0
- data/lib/omnizip/formats/xz_impl/stream_decoder.rb +8 -0
- data/lib/omnizip/formats/zip/reader.rb +2 -0
- data/lib/omnizip/implementations/seven_zip/lzma/encoder.rb +1 -0
- data/lib/omnizip/implementations/xz_utils/lzma2/decoder.rb +4 -9
- data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +1 -16
- data/lib/omnizip/io/buffered_input.rb +1 -0
- data/lib/omnizip/io/buffered_output.rb +1 -0
- data/lib/omnizip/io/source.rb +4 -0
- data/lib/omnizip/io/stream_manager.rb +3 -1
- data/lib/omnizip/link_handler.rb +2 -0
- data/lib/omnizip/metadata/metadata_validator.rb +2 -0
- data/lib/omnizip/models/filter_config.rb +2 -0
- data/lib/omnizip/version.rb +1 -1
- data/lib/omnizip/zip/file.rb +1 -1
- data/lib/omnizip/zip/input_stream.rb +2 -1
- metadata +1 -3
- data/lib/omnizip/algorithms/sevenzip_lzma2.rb +0 -119
- data/lib/omnizip/algorithms/xz_lzma2.rb +0 -114
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5d0052490f16345b27296100499706bddd9936ea4a4b839dc34ea862bef32d77
|
|
4
|
+
data.tar.gz: f33f511d967b2a4f5d881b0e9205c72e57a9744480fdcb914b5ee44df9a14983
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 722b0ece40f17d907a74df2fd9a333945f5060101f6c816bc613c56b1e653d907b31fc803afe40b840217e70dd6301304e0c39660071a0fab20d3df4e6a9c884
|
|
7
|
+
data.tar.gz: f3810c7f1d35b12a88dbc2651945ff18d66399b42f5bb3db1299aece2545fceb9e6336025d2e3555d4b8aa4608f3991091ddd33dca0d575a1b6153da0b4c6a02
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,100 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.3.19] - 2026-08-25
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- 7-Zip LZMA2 encoder corrupted every chunk after the first: pos_state was
|
|
14
|
+
computed from the chunk-relative position while decoders count positions
|
|
15
|
+
continuously across chunk boundaries. The stale duplicated encoder is now a
|
|
16
|
+
thin subclass of the fixed XZ Utils encoder (also picking up its state-reset,
|
|
17
|
+
64 KiB chunk-cap and symbol-queue fixes).
|
|
18
|
+
|
|
19
|
+
## [0.3.18] - 2026-08-25
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Zstandard long-distance matching (LDM): a sparse hash table over the whole
|
|
23
|
+
frame finds matches beyond the 128 KiB block window at levels >= 19 for
|
|
24
|
+
multi-block inputs, bringing zstd-22 output to reference parity
|
|
25
|
+
(0.146 vs omnizip-rs 0.1456 on the benchmark corpus).
|
|
26
|
+
|
|
27
|
+
## [0.3.17] - 2026-08-25
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- Zstandard greedy parser: rep0 fast-path and backward match extension into
|
|
31
|
+
the pending literals (default-level text ratio 0.177 -> 0.167).
|
|
32
|
+
|
|
33
|
+
## [0.3.16] - 2026-08-24
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- Zstandard Compressed_Blocks now carry a sequences section: LZ77 match
|
|
37
|
+
finder (greedy/lazy/lazy2) and LL/OF/ML FSE sequence encoding with
|
|
38
|
+
Predefined/FSE_Compressed table selection. Text ratio 0.49 -> ~0.17.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- Migrated ConversionOptions, ParallelOptions, ProgressOptions and EtaResult
|
|
42
|
+
models to lutaml-model (breaking change for hand-rolled hash access).
|
|
43
|
+
|
|
44
|
+
## [0.3.15] - 2026-08-24
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
- Zstandard codec rebuilt per RFC 8878: FSE tables from stream, FSE Huffman
|
|
48
|
+
weights, treeless literals, repeat mode, XXH64 frame checksums, window
|
|
49
|
+
descriptor; `compress` with default options now really compresses instead
|
|
50
|
+
of emitting stored frames (issues #27, BUGREPORT 01-10).
|
|
51
|
+
- XZ LZMA2 encoder: chunking, repeat-offset state and wire format repaired
|
|
52
|
+
(issue #26; missing `tempfile` require, symbol-buffer overflow at ~10 MB).
|
|
53
|
+
|
|
54
|
+
## [0.3.14] - 2026-07-25
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
- Documented the Algorithms vs Implementations boundary; deepened the
|
|
58
|
+
Algorithm base class with a class-level facade and IO::Source/Sink.
|
|
59
|
+
- Extracted `Omnizip::Parallel::Engine`.
|
|
60
|
+
|
|
61
|
+
## [0.3.13] - 2026-07-24
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
- Reverted the v0.3.12 require-omnizip-everywhere band-aid in favor of
|
|
65
|
+
per-file entry-point requires.
|
|
66
|
+
|
|
67
|
+
## [0.3.12] - 2026-07-24
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
- Circular dependency at gemspec load (`version.rb` no longer requires
|
|
71
|
+
omnizip); missing entry-point requires at the top of internal files.
|
|
72
|
+
|
|
73
|
+
## [0.3.11] - 2026-07-22
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
- Format files broke when required directly by external code; LinkHandler
|
|
77
|
+
symlink gating; BCJ filter autoload.
|
|
78
|
+
|
|
79
|
+
## [0.3.10] - 2026-07-21
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
- Migrated CompressionOptions and AlgorithmMetadata to lutaml-model;
|
|
83
|
+
decoupled Convenience from ZIP via ArchiveHandler; replaced `respond_to?`
|
|
84
|
+
type checks with `is_a?`.
|
|
85
|
+
|
|
86
|
+
## [0.3.9] - 2026-03-24
|
|
87
|
+
|
|
88
|
+
### Fixed
|
|
89
|
+
- Windows unrar detection; 7-Zip SDK version validation (major version only).
|
|
90
|
+
|
|
91
|
+
## [0.3.8] - 2026-02-24
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
- Formats now auto-register when their files are loaded directly.
|
|
95
|
+
|
|
96
|
+
## [0.3.7] - 2026-02-23
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
- XZ dictionary-size bounds checking (OOM prevention); block header parser
|
|
100
|
+
autoload.
|
|
101
|
+
|
|
8
102
|
## [0.3.6] - 2026-02-23
|
|
9
103
|
|
|
10
104
|
### Changed
|
|
@@ -18,8 +112,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
18
112
|
- Fixed RAR format `verify` and `repair` convenience methods
|
|
19
113
|
- Fixed library loading to ensure convenience methods are available at startup
|
|
20
114
|
|
|
21
|
-
## [Unreleased]
|
|
22
|
-
|
|
23
115
|
### Added
|
|
24
116
|
- **XAR Format Support**: Full read/write support for XAR (eXtensible ARchive) format
|
|
25
117
|
- XAR is primarily used on macOS for software packages (.pkg files) and installers
|
|
@@ -594,4 +686,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
594
686
|
- Native compression support documented
|
|
595
687
|
- All 6 compression methods explained
|
|
596
688
|
- Performance characteristics detailed
|
|
597
|
-
- Real-world usage examples
|
|
689
|
+
- Real-world usage examples
|
|
@@ -26,7 +26,7 @@ as absolute rules:
|
|
|
26
26
|
| 04 | `04-filter-base-consolidation.md` | medium | **DONE** |
|
|
27
27
|
| 05 | `05-send-private-methods.md` | high | **DONE** |
|
|
28
28
|
| 06 | `06-instance-variable-access.md` | high | **DONE** |
|
|
29
|
-
| 07 | `07-respond-to-replacement.md` | medium | **
|
|
29
|
+
| 07 | `07-respond-to-replacement.md` | medium | **DONE** — 11 type checks converted; 45 feature-detection sites annotated `# allowed:` and enforced by a spec; 1 deferred type check in `algorithms/lzma.rb` pending `LzmaOptions` unification |
|
|
30
30
|
| 08 | `08-spec-doubles.md` | low | **DONE** |
|
|
31
31
|
| 09 | `09-cli-shared-module.md` | low | **DONE** |
|
|
32
32
|
| 10 | `10-format-detector-ocp.md` | medium | **DONE** |
|
|
@@ -43,7 +43,7 @@ as absolute rules:
|
|
|
43
43
|
| `.send` on private methods | 17 | **0** |
|
|
44
44
|
| `instance_variable_set`/`get` | 54 | **0** |
|
|
45
45
|
| `double()` in specs | 1 | **0** |
|
|
46
|
-
| `respond_to?` | 111 |
|
|
46
|
+
| `respond_to?` | 60 at track 07 start (111 originally) | 49 — 46 annotated `# allowed:` (45 feature detection + 1 deferred type check) plus 3 doc comments |
|
|
47
47
|
| Hand-rolled model `to_h` | 26 | 24 (2 migrated to lutaml-model) |
|
|
48
48
|
|
|
49
49
|
## Architecture summary
|
|
@@ -1,91 +1,237 @@
|
|
|
1
1
|
# 07 — Replace `respond_to?` with proper typing
|
|
2
2
|
|
|
3
3
|
Priority: **medium**.
|
|
4
|
-
Status: **
|
|
4
|
+
Status: **DONE**, with one known exception — `algorithms/lzma.rb:200` is a type
|
|
5
|
+
check we chose not to convert, not feature detection. See "The one exception".
|
|
5
6
|
|
|
6
7
|
## Rule
|
|
7
8
|
|
|
8
9
|
> NEVER use `respond_to?` for type checking. Use `is_a?` for type checks,
|
|
9
10
|
> or better yet, design the type hierarchy so the check isn't needed.
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
`
|
|
28
|
-
`
|
|
29
|
-
`
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
`
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
12
|
+
## Outcome
|
|
13
|
+
|
|
14
|
+
`lib/` went from 60 `respond_to?` occurrences to 49:
|
|
15
|
+
|
|
16
|
+
| | count |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Converted to a real type check | 11 |
|
|
19
|
+
| Retained as genuine feature detection, annotated `# allowed:` | 45 |
|
|
20
|
+
| Retained as a **deferred type check**, annotated `# allowed:` | 1 |
|
|
21
|
+
| Prose inside doc comments (not code) | 3 |
|
|
22
|
+
|
|
23
|
+
46 sites carry an `# allowed:` annotation, but they are not all the same kind of
|
|
24
|
+
thing. 45 are feature detection on objects we do not control. **One is a type
|
|
25
|
+
check we decided not to convert** — counting it with the other 45 would overstate
|
|
26
|
+
how complete this track is, so it is broken out here and below.
|
|
27
|
+
|
|
28
|
+
The three prose occurrences are in `entry.rb`, `io/source.rb` and
|
|
29
|
+
`parallel/job_scheduler.rb`, where they document the pattern that was removed.
|
|
30
|
+
They are kept deliberately — rewording them so a `grep` returns zero would be
|
|
31
|
+
dishonest.
|
|
32
|
+
|
|
33
|
+
`spec/omnizip/respond_to_annotation_spec.rb` enforces the convention. It lexes
|
|
34
|
+
`lib/` with `Ripper` and fails on any `respond_to?` **identifier token** that
|
|
35
|
+
lacks a `# allowed: <reason>` **comment token** on its own line or the one
|
|
36
|
+
above. Being lexer-based rather than substring-based, a `# allowed:` inside a
|
|
37
|
+
string literal cannot satisfy it and a `respond_to?` inside a heredoc cannot
|
|
38
|
+
trip it.
|
|
39
|
+
|
|
40
|
+
Note what that spec does and does not do: it enforces that a reason is
|
|
41
|
+
**stated**, not that the reason is **good**. It cannot tell feature detection
|
|
42
|
+
from a deferred type check, which is exactly why the one exception is recorded
|
|
43
|
+
here in prose rather than left for the spec to imply.
|
|
44
|
+
|
|
45
|
+
## The governing distinction
|
|
46
|
+
|
|
47
|
+
A check is a **type check** (convert it) when every runtime type is enumerable
|
|
48
|
+
from the source. It is **feature detection** (annotate it) when a third party
|
|
49
|
+
can register or pass something we cannot enumerate.
|
|
50
|
+
|
|
51
|
+
The second half is what this pass got wrong at first. omnizip's world is open:
|
|
52
|
+
|
|
53
|
+
- `FilterRegistry.register` (`filter_registry.rb:30`) raises only on a nil name
|
|
54
|
+
or nil class. **No inheritance check.**
|
|
55
|
+
- `ProfileRegistry.register` (`profile_registry.rb:23`) requires only
|
|
56
|
+
`is_a?(CompressionProfile)`, not membership in the built-in set.
|
|
57
|
+
- `ArchiveHandler.register` (`archive_handler.rb:26`) accepts any object.
|
|
58
|
+
- The third positional `options` parameter on every `Algorithm#compress` /
|
|
59
|
+
`#decompress` is public and unconstrained.
|
|
60
|
+
|
|
61
|
+
So a check can look like a type check, and be one inside this repo today, while
|
|
62
|
+
still being reachable with a type we cannot name. Those stay, annotated.
|
|
63
|
+
|
|
64
|
+
## What was converted (11)
|
|
65
|
+
|
|
66
|
+
| site | conversion |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `algorithms/registration.rb:22` | guard dropped; `Algorithm.inherited` defines `register_algorithm` on every subclass |
|
|
69
|
+
| `algorithms/lzma/xz_utils_decoder.rb:291` | guard dropped; `@range_decoder ||=` runs earlier in the same method |
|
|
70
|
+
| `commands/archive_list_command.rb:72`, `:105` | `archive.entries`; the private builder returns one of three readers, all of which expose it |
|
|
71
|
+
| `implementations/seven_zip/lzma2/encoder.rb`, `implementations/xz_utils/lzma2/encoder.rb` | both `StringCompat` modules deleted, `byteslice` inlined at all three call sites — see below |
|
|
72
|
+
| `commands/archive_extract_command.rb:225` | `archive.is_a?(Omnizip::Zip::File)`; closed 4-way set, only ZIP closes |
|
|
73
|
+
| `implementations/xz_utils/lzma2/decoder.rb:420` | guard dropped; `@lzma_decoder` is always an `XzUtilsDecoder`, which exposes `range_decoder` |
|
|
74
|
+
| `io/stream_manager.rb:74` | `@source.close if @owned`; `@owned` is only true for a path the manager itself opened as a `File` |
|
|
75
|
+
| `algorithms/xz_lzma2.rb:59`, `algorithms/sevenzip_lzma2.rb:64` | routed through `Omnizip::IO::Source.for`, which accepts any `#read` duck |
|
|
76
|
+
|
|
77
|
+
The String branch in those last two was deliberately **not** routed through the
|
|
78
|
+
adapter: `Source::StringSource#read` treats a String that exists on disk as a
|
|
79
|
+
file path, so routing it would silently start reading files off disk whenever
|
|
80
|
+
input collided with a filename.
|
|
81
|
+
|
|
82
|
+
### The `StringCompat` modules
|
|
83
|
+
|
|
84
|
+
Each held `if "".respond_to?(:byteslice)` with a `string.bytes[start, length]`
|
|
85
|
+
fallback, under a comment claiming Ruby 3.0-3.1 lacks `String#byteslice`.
|
|
86
|
+
**That comment was simply wrong** — `String#byteslice` has existed since Ruby
|
|
87
|
+
1.9.3, so the fallback was unreachable on every Ruby that could ever have run
|
|
88
|
+
this gem.
|
|
89
|
+
|
|
90
|
+
Deleting only the `respond_to?` branch would have left a module named `Compat`
|
|
91
|
+
whose sole purpose was that branch, forwarding one call to `String#byteslice`
|
|
92
|
+
and nothing else — and two of the three call sites still carried a "for Ruby
|
|
93
|
+
3.0-3.1 compatibility" comment that would have become a lie. So both modules
|
|
94
|
+
were deleted outright and `temp_buffer.byteslice(0, out_pos.value)` inlined at
|
|
95
|
+
all three call sites (`xz_utils/lzma2/encoder.rb` ×2,
|
|
96
|
+
`seven_zip/lzma2/encoder.rb` ×1).
|
|
97
|
+
|
|
98
|
+
The separate comment at the two XZ Utils call sites is **kept**: it explains why
|
|
99
|
+
`byteslice` rather than `[]`, citing
|
|
100
|
+
https://bugs.ruby-lang.org/issues/15985 (the `[]` operator can return extra
|
|
101
|
+
bytes around NULs). That reason is real and outlives the compat shim.
|
|
102
|
+
|
|
103
|
+
## What was retained as feature detection (45)
|
|
104
|
+
|
|
105
|
+
Grouped by what the object actually is. `algorithms/lzma.rb:200` is deliberately
|
|
106
|
+
excluded from these counts and covered in the next section.
|
|
107
|
+
|
|
108
|
+
- **Caller-supplied IO** (24) — `formats/xz_impl/stream_decoder.rb` (8),
|
|
109
|
+
`formats/xz_impl/block_decoder.rb` (3), `algorithms/lzma/xz_utils_decoder.rb`
|
|
110
|
+
(2), `file_type.rb` (3), `io/source.rb` (4), `io/stream_manager.rb` (2),
|
|
111
|
+
`io/buffered_input.rb`, `io/buffered_output.rb`. Ruby has no `Seekable` or
|
|
112
|
+
`Ungettable` interface to type against; `StringIO`, `File`, `Socket`,
|
|
113
|
+
`Tempfile` and a user's own duck all differ in exactly `seek` / `pos` / `size`
|
|
114
|
+
/ `ungetbyte` / `set_encoding` / `close`.
|
|
115
|
+
- **Public `options` parameter** (7) — `algorithms/lzma.rb` (4),
|
|
116
|
+
`algorithms/bzip2.rb`, `algorithms/deflate.rb`, `algorithms/zstandard.rb`.
|
|
117
|
+
- **Open registries** (4) — `models/filter_config.rb`,
|
|
118
|
+
`commands/profile_show_command.rb`, `convenience.rb` (2).
|
|
119
|
+
- **Other caller-supplied objects** (10) — foreign archives and entries in
|
|
120
|
+
`extraction/selective_extractor.rb` (5) and `extraction/filter_chain.rb` (3),
|
|
121
|
+
an output sink in `implementations/seven_zip/lzma/encoder.rb`, and a metadata
|
|
122
|
+
object in `metadata/metadata_validator.rb`.
|
|
123
|
+
|
|
124
|
+
## The one exception (1)
|
|
125
|
+
|
|
126
|
+
`algorithms/lzma.rb:200` is annotated *"type check deferred, not feature
|
|
127
|
+
detection"*. That annotation is accurate and the code is honest, but it means
|
|
128
|
+
**track 07 is complete except for this single site**. It is a type check that
|
|
129
|
+
survives, not a capability probe.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
level = if options.respond_to?(:level)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Two unrelated value objects reach `build_encoder_options` carrying `#level`:
|
|
136
|
+
`Models::CompressionOptions` and
|
|
137
|
+
`Formats::Rar::Rar5::Solid::SolidEncoder::LzmaOptions` (`solid_encoder.rb:95`,
|
|
138
|
+
reaching this method from `:52`). They share no ancestor. Rewriting the check to
|
|
139
|
+
`is_a?(Models::CompressionOptions)` would send solid-RAR5 compression down the
|
|
140
|
+
`else` branch and pin its level to 5 instead of the configured 1–5 — a silent
|
|
141
|
+
regression, which is why the conversion was rejected rather than attempted.
|
|
142
|
+
|
|
143
|
+
**What would close it:** unify `SolidEncoder::LzmaOptions`,
|
|
144
|
+
`Compression::LZMA::LzmaOptions` (`rar5/compression/lzma.rb:184`) and
|
|
145
|
+
`Models::CompressionOptions` into one type. That is RAR5 work, it breaks
|
|
146
|
+
`solid_encoder_spec.rb` and `lzma_spec.rb` assertions on `dict_size`, and
|
|
147
|
+
`CompressionOptions` names the attribute `dictionary_size`. Once those are one
|
|
148
|
+
type, this site converts to `is_a?` in a single line and track 07 is
|
|
149
|
+
unconditionally done.
|
|
150
|
+
|
|
151
|
+
Three cheaper-looking alternatives were considered and rejected: reaching from
|
|
152
|
+
`algorithms/` into `formats/rar/rar5/solid/` with a two-armed `is_a?`; a shared
|
|
153
|
+
`LevelledOptions` mixin introduced for exactly two call sites; and replacing
|
|
154
|
+
`LzmaOptions` with `CompressionOptions` outright, which the specs block.
|
|
155
|
+
|
|
156
|
+
## Corrections to the previous version of this document
|
|
157
|
+
|
|
158
|
+
- The count was **60**, not 109.
|
|
159
|
+
- Category B (`job.respond_to?(:size)` in `parallel/job_scheduler.rb`) and
|
|
160
|
+
category E (`opts.respond_to?(:"#{k}=")`) were **already done**.
|
|
161
|
+
- Category D was wrong. No `File.respond_to?(:symlink)` / `(:link)` sites
|
|
162
|
+
remained and `link_handler.rb` had none at all. The survivors filed under D
|
|
163
|
+
were `io.respond_to?(:pos)` / `(:seek)` in `file_type.rb`, which are **IO**
|
|
164
|
+
capability checks. The proposed `Platform.seekable?(io)` was rejected:
|
|
165
|
+
seekability is not a platform property, and the helper would still have
|
|
166
|
+
contained `respond_to?`, merely relocated.
|
|
167
|
+
- `formats/ole/` contained **zero** occurrences. The capability cluster was
|
|
168
|
+
`formats/xz_impl/` plus `algorithms/lzma/xz_utils_decoder.rb`.
|
|
169
|
+
- Category C was partly done already: `Omnizip::Entry` existed and was included
|
|
170
|
+
by 12 entry classes. Converting `extraction/filter_chain.rb` to use it was
|
|
171
|
+
attempted and rejected — below.
|
|
172
|
+
|
|
173
|
+
## Rejected: converting `extraction/filter_chain.rb`
|
|
174
|
+
|
|
175
|
+
`FilterChain#extract_filename` still walks `name` / `path` / `filename`. Routing
|
|
176
|
+
it through `Omnizip::Entry` looked like the intended fix, but a foreign entry
|
|
177
|
+
with `name == "secret.txt"` and `to_s == "opaque"` matches `*.txt` today and
|
|
178
|
+
would not afterwards. Because these filters drive **exclusion** as well as
|
|
179
|
+
inclusion, a filter that stops matching means a file the caller meant to block
|
|
180
|
+
gets extracted. `Extraction.extract_with_filter` is public, so this is
|
|
181
|
+
reachable. The conversion needs every entry reaching `FilterChain` to include
|
|
182
|
+
`Omnizip::Entry`, which the public API cannot guarantee.
|
|
183
|
+
|
|
184
|
+
## Bug found while doing this work
|
|
185
|
+
|
|
186
|
+
Auditing `IO::Source` for this track surfaced a live `NameError` in shipped
|
|
187
|
+
code. `io/source.rb` had **no `require` statements at all**, while `Source.for`
|
|
188
|
+
and `Sink.for` both `case` on `::IO, ::StringIO, ::Tempfile`. After a bare
|
|
189
|
+
`require "omnizip"` neither `StringIO` nor `Tempfile` is defined, so any input
|
|
190
|
+
that was not already an `::IO` raised:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
$ ruby -Ilib -e 'require "omnizip"
|
|
194
|
+
sink = Object.new; def sink.write(d); end
|
|
195
|
+
Omnizip::Formats::Xz.create("hello", sink)'
|
|
196
|
+
NameError: uninitialized constant Tempfile
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The adapter had 12 production call sites across `Formats::Xz`, `Formats::Lzip`,
|
|
200
|
+
`Formats::LzmaAlone` and `Algorithms::LZMA2`. The failure moved with the caller
|
|
201
|
+
— whichever constant the load path happened to pull in first appeared to work —
|
|
202
|
+
which is why it survived.
|
|
203
|
+
|
|
204
|
+
Fixed in its own commit across 15 files: 12 requires added to 11 files that
|
|
205
|
+
referenced a constant without requiring it, and 4 requires relocated out of
|
|
206
|
+
method and class bodies into the file prologue.
|
|
207
|
+
|
|
208
|
+
`spec/omnizip/constant_require_spec.rb` enforces the invariant statically —
|
|
209
|
+
`Ripper.lex` finds the constant references, `Ripper.sexp` confirms the matching
|
|
210
|
+
require is a top-level unconditional statement in the prologue, accepting both
|
|
211
|
+
`require "x"` and `require("x")`. `spec/omnizip/clean_load_spec.rb` adds four
|
|
212
|
+
subprocess regression proofs. Those must shell out: an in-process example passes
|
|
213
|
+
on the broken library, because `spec_helper` and sibling specs load both
|
|
214
|
+
constants as a side effect. That is the same masking that hid the bug.
|
|
87
215
|
|
|
88
216
|
## Acceptance
|
|
89
217
|
|
|
90
|
-
`grep -rn "respond_to?" lib/`
|
|
91
|
-
|
|
218
|
+
- `grep -rn "respond_to?" lib/` → 49: 46 annotated code sites (45 feature
|
|
219
|
+
detection + 1 deferred type check) and 3 doc comments.
|
|
220
|
+
- `spec/omnizip/respond_to_annotation_spec.rb` passes, and fails loudly on any
|
|
221
|
+
new unannotated occurrence.
|
|
222
|
+
- The one deferred type check is tracked in "The one exception" above, not
|
|
223
|
+
quietly folded into the feature-detection count.
|
|
224
|
+
|
|
225
|
+
## Follow-ups
|
|
226
|
+
|
|
227
|
+
- Unify `SolidEncoder::LzmaOptions`, `Compression::LZMA::LzmaOptions` and
|
|
228
|
+
`Models::CompressionOptions`, which would let `algorithms/lzma.rb:200`
|
|
229
|
+
convert.
|
|
230
|
+
- Normalize algorithm `options` to a single type. `Algorithm.compress(data,
|
|
231
|
+
level: 9)` forwards a Hash, so `bzip2`, `deflate` and `zstandard` silently
|
|
232
|
+
drop the level today — reached from `formats/zip/writer.rb:390`,
|
|
233
|
+
`zip/output_stream.rb:278` and `parallel/parallel_compressor.rb:104`. This
|
|
234
|
+
changes compressed output, so it needs its own pass. It would delete most of
|
|
235
|
+
the `options` annotations.
|
|
236
|
+
- Give `FilterChain` a guaranteed `Omnizip::Entry` contract, which would let
|
|
237
|
+
`extract_filename` convert.
|
data/lib/omnizip/algorithm.rb
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
# See the COPYING file for the complete text of the license.
|
|
17
17
|
#
|
|
18
18
|
|
|
19
|
+
require "stringio"
|
|
20
|
+
|
|
19
21
|
module Omnizip
|
|
20
22
|
# Abstract base class for compression algorithms.
|
|
21
23
|
#
|
|
@@ -23,8 +25,6 @@ module Omnizip
|
|
|
23
25
|
# the required methods. Algorithms are automatically registered with the
|
|
24
26
|
# AlgorithmRegistry when defined.
|
|
25
27
|
class Algorithm
|
|
26
|
-
require "stringio"
|
|
27
|
-
|
|
28
28
|
attr_reader :options, :filter
|
|
29
29
|
|
|
30
30
|
# Initialize algorithm with options.
|
|
@@ -288,9 +288,7 @@ check_rc_finished: true)
|
|
|
288
288
|
end
|
|
289
289
|
|
|
290
290
|
# Read range decoder init bytes (must happen after set_input sets correct stream)
|
|
291
|
-
|
|
292
|
-
@range_decoder.read_init_bytes
|
|
293
|
-
end
|
|
291
|
+
@range_decoder.read_init_bytes if @range_decoder.init_bytes_remaining.positive?
|
|
294
292
|
|
|
295
293
|
# Main decoding loop
|
|
296
294
|
# XZ Utils pattern (lzma_decoder.c:305-306):
|
|
@@ -439,6 +437,7 @@ check_rc_finished: true)
|
|
|
439
437
|
next_byte = stream.getbyte
|
|
440
438
|
if next_byte
|
|
441
439
|
# Put the byte back
|
|
440
|
+
# allowed: caller-supplied stream; peek-back is optional
|
|
442
441
|
stream.ungetbyte(next_byte) if stream.respond_to?(:ungetbyte)
|
|
443
442
|
raise Omnizip::DecompressionError,
|
|
444
443
|
"LZMA_Alone file has data after the end-of-payload marker. The file may be corrupted or contain concatenated streams."
|
|
@@ -452,6 +451,7 @@ check_rc_finished: true)
|
|
|
452
451
|
begin
|
|
453
452
|
next_byte = stream.getbyte
|
|
454
453
|
if next_byte
|
|
454
|
+
# allowed: caller-supplied stream; peek-back is optional
|
|
455
455
|
stream.ungetbyte(next_byte) if stream.respond_to?(:ungetbyte)
|
|
456
456
|
raise Omnizip::DecompressionError,
|
|
457
457
|
"LZMA_Alone file has more compressed data than expected. The uncompressed size field (#{@uncompressed_size} bytes) appears to be too small."
|
|
@@ -155,6 +155,7 @@ module Omnizip
|
|
|
155
155
|
decoder_opts[:uncompressed_size] =
|
|
156
156
|
@uncompressed_size
|
|
157
157
|
end
|
|
158
|
+
# allowed: options is a public parameter; the body needs #key? and #[]
|
|
158
159
|
decoder_opts[:uncompressed_size] ||= options[:size] if options.respond_to?(:key?) && options.key?(:size)
|
|
159
160
|
|
|
160
161
|
decoder = Decoder.new(input_stream, decoder_opts)
|
|
@@ -174,6 +175,7 @@ module Omnizip
|
|
|
174
175
|
opts = {}
|
|
175
176
|
|
|
176
177
|
# Handle Hash-like options
|
|
178
|
+
# allowed: options is a public parameter; the body needs #[] and #key?
|
|
177
179
|
if options.respond_to?(:[])
|
|
178
180
|
opts[:lc] = options[:lc] if options[:lc]
|
|
179
181
|
opts[:lp] = options[:lp] if options[:lp]
|
|
@@ -192,13 +194,16 @@ module Omnizip
|
|
|
192
194
|
end
|
|
193
195
|
|
|
194
196
|
# Handle level from both Hash and CompressionOptions
|
|
197
|
+
# Models::CompressionOptions and SolidEncoder::LzmaOptions both expose
|
|
198
|
+
# #level and share no ancestor; converting needs them unified first.
|
|
199
|
+
# allowed: type check deferred, not feature detection
|
|
195
200
|
level = if options.respond_to?(:level)
|
|
196
|
-
options.level
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
5
|
|
201
|
+
options.level
|
|
202
|
+
# allowed: reader-style options are handled above; this arm needs #[]
|
|
203
|
+
elsif options.respond_to?(:[])
|
|
204
|
+
options[:level]
|
|
201
205
|
end
|
|
206
|
+
level ||= 5
|
|
202
207
|
|
|
203
208
|
opts[:dict_size] ||= dictionary_size_for_level(level)
|
|
204
209
|
|
|
@@ -218,6 +223,7 @@ module Omnizip
|
|
|
218
223
|
opts = {}
|
|
219
224
|
|
|
220
225
|
# Handle Hash-like options - pass through all decoder-relevant options
|
|
226
|
+
# allowed: options is a public parameter; the body needs #key? and #[]
|
|
221
227
|
if options.respond_to?(:key?)
|
|
222
228
|
if options.key?(:sdk_compatible)
|
|
223
229
|
opts[:sdk_compatible] =
|
|
@@ -19,7 +19,7 @@ end
|
|
|
19
19
|
|
|
20
20
|
# Now register all algorithms
|
|
21
21
|
Omnizip::AlgorithmRegistry.register(:lzma, Omnizip::Algorithms::LZMA)
|
|
22
|
-
Omnizip::Algorithms::LZMA2.register_algorithm
|
|
22
|
+
Omnizip::Algorithms::LZMA2.register_algorithm
|
|
23
23
|
Omnizip::AlgorithmRegistry.register(:ppmd7, Omnizip::Algorithms::PPMd7)
|
|
24
24
|
Omnizip::AlgorithmRegistry.register(:ppmd8, Omnizip::Algorithms::PPMd8)
|
|
25
25
|
Omnizip::AlgorithmRegistry.register(:bzip2, Omnizip::Algorithms::BZip2)
|