omnizip 0.3.9 → 0.3.10

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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.refactor/00-overview.md +67 -0
  3. data/TODO.refactor/01-registry-base-class.md +81 -0
  4. data/TODO.refactor/02-error-hierarchy.md +57 -0
  5. data/TODO.refactor/03-autoload-entry-point.md +69 -0
  6. data/TODO.refactor/04-filter-base-consolidation.md +49 -0
  7. data/TODO.refactor/05-send-private-methods.md +64 -0
  8. data/TODO.refactor/06-instance-variable-access.md +69 -0
  9. data/TODO.refactor/07-respond-to-replacement.md +91 -0
  10. data/TODO.refactor/08-spec-doubles.md +27 -0
  11. data/TODO.refactor/09-cli-shared-module.md +35 -0
  12. data/TODO.refactor/10-format-detector-ocp.md +34 -0
  13. data/TODO.refactor/11-convenience-decoupling.md +38 -0
  14. data/TODO.refactor/12-thread-safety.md +8 -0
  15. data/TODO.refactor/13-lutaml-model-migration.md +82 -0
  16. data/TODO.refactor/14-add-missing-specs.md +25 -0
  17. data/lib/omnizip/algorithm.rb +0 -2
  18. data/lib/omnizip/algorithm_registry.rb +29 -57
  19. data/lib/omnizip/algorithms/lzma/dictionary.rb +4 -3
  20. data/lib/omnizip/algorithms/lzma/lzip_decoder.rb +2 -0
  21. data/lib/omnizip/algorithms/lzma/range_decoder.rb +1 -1
  22. data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +6 -5
  23. data/lib/omnizip/algorithms/lzma2/encoder.rb +0 -2
  24. data/lib/omnizip/algorithms/lzma2/lzma2_chunk.rb +0 -2
  25. data/lib/omnizip/algorithms/lzma2/properties.rb +0 -2
  26. data/lib/omnizip/algorithms/ppmd7/model.rb +1 -1
  27. data/lib/omnizip/algorithms/ppmd8/context.rb +0 -2
  28. data/lib/omnizip/algorithms/ppmd8/model.rb +1 -1
  29. data/lib/omnizip/algorithms/ppmd8.rb +0 -2
  30. data/lib/omnizip/algorithms/ppmd_base.rb +0 -2
  31. data/lib/omnizip/algorithms/sevenzip_lzma2.rb +0 -5
  32. data/lib/omnizip/algorithms/xz_lzma2.rb +0 -5
  33. data/lib/omnizip/algorithms/zstandard/frame/header.rb +3 -5
  34. data/lib/omnizip/algorithms.rb +1 -0
  35. data/lib/omnizip/archive_handler.rb +75 -0
  36. data/lib/omnizip/archive_handlers/tar_handler.rb +51 -0
  37. data/lib/omnizip/archive_handlers/zip_handler.rb +73 -0
  38. data/lib/omnizip/archive_handlers.rb +11 -0
  39. data/lib/omnizip/checksum_registry.rb +23 -96
  40. data/lib/omnizip/cli/shared.rb +31 -0
  41. data/lib/omnizip/cli.rb +6 -43
  42. data/lib/omnizip/commands/archive_create_command.rb +0 -6
  43. data/lib/omnizip/commands/archive_extract_command.rb +0 -4
  44. data/lib/omnizip/commands/archive_list_command.rb +0 -4
  45. data/lib/omnizip/commands/archive_repair_command.rb +0 -2
  46. data/lib/omnizip/commands/archive_verify_command.rb +0 -2
  47. data/lib/omnizip/commands/compress_command.rb +0 -2
  48. data/lib/omnizip/commands/decompress_command.rb +0 -2
  49. data/lib/omnizip/commands/list_command.rb +0 -2
  50. data/lib/omnizip/commands/metadata_command.rb +0 -5
  51. data/lib/omnizip/commands/parity_create_command.rb +0 -2
  52. data/lib/omnizip/commands/parity_repair_command.rb +0 -2
  53. data/lib/omnizip/commands/parity_verify_command.rb +0 -2
  54. data/lib/omnizip/commands/profile_list_command.rb +0 -2
  55. data/lib/omnizip/commands/profile_show_command.rb +0 -2
  56. data/lib/omnizip/convenience.rb +111 -234
  57. data/lib/omnizip/converter/seven_zip_to_zip_strategy.rb +1 -1
  58. data/lib/omnizip/crypto/aes256/cipher.rb +6 -1
  59. data/lib/omnizip/error.rb +22 -18
  60. data/lib/omnizip/filter.rb +5 -3
  61. data/lib/omnizip/filter_registry.rb +74 -98
  62. data/lib/omnizip/filters/bcj2.rb +0 -2
  63. data/lib/omnizip/filters/bcj_x86.rb +0 -2
  64. data/lib/omnizip/filters/filter_base.rb +4 -70
  65. data/lib/omnizip/filters/registry.rb +0 -2
  66. data/lib/omnizip/format_detector.rb +21 -12
  67. data/lib/omnizip/format_registry.rb +69 -53
  68. data/lib/omnizip/formats/bzip2_file.rb +0 -2
  69. data/lib/omnizip/formats/cpio/entry.rb +0 -2
  70. data/lib/omnizip/formats/cpio/reader.rb +0 -1
  71. data/lib/omnizip/formats/cpio/writer.rb +0 -1
  72. data/lib/omnizip/formats/cpio.rb +0 -1
  73. data/lib/omnizip/formats/format_spec_loader.rb +1 -1
  74. data/lib/omnizip/formats/gzip.rb +0 -2
  75. data/lib/omnizip/formats/iso/directory_record.rb +6 -3
  76. data/lib/omnizip/formats/iso/reader.rb +2 -2
  77. data/lib/omnizip/formats/iso.rb +0 -1
  78. data/lib/omnizip/formats/lzip.rb +5 -25
  79. data/lib/omnizip/formats/lzma_alone.rb +7 -25
  80. data/lib/omnizip/formats/msi/reader.rb +1 -0
  81. data/lib/omnizip/formats/msi/string_pool.rb +2 -3
  82. data/lib/omnizip/formats/msi.rb +1 -4
  83. data/lib/omnizip/formats/ole/allocation_table.rb +0 -1
  84. data/lib/omnizip/formats/ole/dirent.rb +9 -4
  85. data/lib/omnizip/formats/ole/header.rb +0 -1
  86. data/lib/omnizip/formats/ole/ranges_io.rb +1 -1
  87. data/lib/omnizip/formats/ole/storage.rb +5 -6
  88. data/lib/omnizip/formats/ole.rb +0 -1
  89. data/lib/omnizip/formats/rar/block_parser.rb +0 -1
  90. data/lib/omnizip/formats/rar/compression/ppmd/encoder.rb +1 -1
  91. data/lib/omnizip/formats/rar/decompressor.rb +1 -0
  92. data/lib/omnizip/formats/rar/header.rb +0 -1
  93. data/lib/omnizip/formats/rar/rar5/writer.rb +0 -3
  94. data/lib/omnizip/formats/rar/reader.rb +2 -3
  95. data/lib/omnizip/formats/rar/writer.rb +0 -1
  96. data/lib/omnizip/formats/rar.rb +0 -1
  97. data/lib/omnizip/formats/rpm/entry.rb +0 -1
  98. data/lib/omnizip/formats/rpm/header.rb +1 -6
  99. data/lib/omnizip/formats/rpm/lead.rb +37 -26
  100. data/lib/omnizip/formats/rpm/tag.rb +0 -1
  101. data/lib/omnizip/formats/rpm/writer.rb +0 -1
  102. data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +0 -1
  103. data/lib/omnizip/formats/seven_zip/coder_chain.rb +1 -2
  104. data/lib/omnizip/formats/seven_zip/encoded_header.rb +0 -1
  105. data/lib/omnizip/formats/seven_zip/encrypted_header.rb +0 -1
  106. data/lib/omnizip/formats/seven_zip/header.rb +0 -1
  107. data/lib/omnizip/formats/seven_zip/header_encryptor.rb +0 -1
  108. data/lib/omnizip/formats/seven_zip/header_writer.rb +0 -1
  109. data/lib/omnizip/formats/seven_zip/models/file_entry.rb +1 -1
  110. data/lib/omnizip/formats/seven_zip/parser.rb +1 -2
  111. data/lib/omnizip/formats/seven_zip/reader.rb +13 -2
  112. data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +0 -1
  113. data/lib/omnizip/formats/seven_zip/split_archive_writer.rb +0 -1
  114. data/lib/omnizip/formats/seven_zip/writer.rb +4 -6
  115. data/lib/omnizip/formats/seven_zip.rb +2 -4
  116. data/lib/omnizip/formats/tar/entry.rb +0 -2
  117. data/lib/omnizip/formats/tar/header.rb +0 -2
  118. data/lib/omnizip/formats/tar/reader.rb +0 -2
  119. data/lib/omnizip/formats/tar/writer.rb +0 -2
  120. data/lib/omnizip/formats/tar.rb +0 -1
  121. data/lib/omnizip/formats/xar/entry.rb +0 -1
  122. data/lib/omnizip/formats/xar/header.rb +0 -1
  123. data/lib/omnizip/formats/xar/reader.rb +0 -1
  124. data/lib/omnizip/formats/xar/toc.rb +1 -2
  125. data/lib/omnizip/formats/xar/writer.rb +11 -3
  126. data/lib/omnizip/formats/xar.rb +0 -1
  127. data/lib/omnizip/formats/xz/reader.rb +7 -3
  128. data/lib/omnizip/formats/xz.rb +3 -40
  129. data/lib/omnizip/formats/xz_impl/block_decoder.rb +4 -10
  130. data/lib/omnizip/formats/xz_impl/block_encoder.rb +0 -2
  131. data/lib/omnizip/formats/xz_impl/block_header_parser.rb +0 -2
  132. data/lib/omnizip/formats/xz_impl/index_decoder.rb +0 -1
  133. data/lib/omnizip/formats/xz_impl/index_encoder.rb +0 -1
  134. data/lib/omnizip/formats/xz_impl/stream_encoder.rb +1 -1
  135. data/lib/omnizip/formats/xz_impl/stream_footer.rb +0 -1
  136. data/lib/omnizip/formats/xz_impl/stream_footer_parser.rb +0 -1
  137. data/lib/omnizip/formats/xz_impl/stream_header.rb +0 -1
  138. data/lib/omnizip/formats/xz_impl/stream_header_parser.rb +0 -2
  139. data/lib/omnizip/formats/zip/central_directory_header.rb +2 -3
  140. data/lib/omnizip/formats/zip/end_of_central_directory.rb +0 -2
  141. data/lib/omnizip/formats/zip/local_file_header.rb +0 -2
  142. data/lib/omnizip/formats/zip/reader.rb +12 -2
  143. data/lib/omnizip/formats/zip/writer.rb +73 -1
  144. data/lib/omnizip/formats/zip/zip64_end_of_central_directory.rb +0 -2
  145. data/lib/omnizip/formats/zip/zip64_end_of_central_directory_locator.rb +0 -2
  146. data/lib/omnizip/formats/zip/zip64_extra_field.rb +0 -2
  147. data/lib/omnizip/formats/zip.rb +0 -1
  148. data/lib/omnizip/formats.rb +8 -24
  149. data/lib/omnizip/implementations/seven_zip/lzma/range_decoder.rb +2 -2
  150. data/lib/omnizip/implementations/seven_zip/lzma/range_encoder.rb +1 -1
  151. data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +0 -6
  152. data/lib/omnizip/implementations/xz_utils/lzma2/decoder.rb +3 -4
  153. data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +0 -1
  154. data/lib/omnizip/io/source.rb +136 -0
  155. data/lib/omnizip/io.rb +2 -0
  156. data/lib/omnizip/link_handler.rb +7 -3
  157. data/lib/omnizip/metadata/metadata_editor.rb +1 -1
  158. data/lib/omnizip/models/algorithm_metadata.rb +33 -60
  159. data/lib/omnizip/models/compression_options.rb +40 -53
  160. data/lib/omnizip/models/conversion_options.rb +9 -18
  161. data/lib/omnizip/models/parallel_options.rb +25 -0
  162. data/lib/omnizip/optimization_registry.rb +8 -44
  163. data/lib/omnizip/parallel/job_scheduler.rb +26 -8
  164. data/lib/omnizip/parallel/parallel_compressor.rb +10 -20
  165. data/lib/omnizip/parallel/parallel_extractor.rb +20 -11
  166. data/lib/omnizip/parity/par2_repairer.rb +5 -5
  167. data/lib/omnizip/parity/par2_verifier.rb +27 -22
  168. data/lib/omnizip/parity.rb +4 -4
  169. data/lib/omnizip/password/encryption_registry.rb +11 -50
  170. data/lib/omnizip/password/zip_crypto_strategy.rb +1 -4
  171. data/lib/omnizip/pipe/stream_decompressor.rb +2 -3
  172. data/lib/omnizip/profile/archive_profile.rb +0 -2
  173. data/lib/omnizip/profile/binary_profile.rb +0 -2
  174. data/lib/omnizip/profile/compression_profile.rb +0 -1
  175. data/lib/omnizip/profile/profile_detector.rb +0 -2
  176. data/lib/omnizip/profile/text_profile.rb +0 -2
  177. data/lib/omnizip/profile.rb +0 -2
  178. data/lib/omnizip/registry.rb +112 -0
  179. data/lib/omnizip/version.rb +1 -1
  180. data/lib/omnizip/zip/file.rb +20 -9
  181. data/lib/omnizip.rb +26 -58
  182. metadata +24 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb35ab3f87d9e1a1d4f96c65a86b9c5dcbd5df0f4da48443158dbe16d8e896e0
4
- data.tar.gz: f9c8deab498097486bb07092f479b5097959cbc7f6b6ca171bc79996b08b6b91
3
+ metadata.gz: d688703887c5ac25ffa43a48582dbb0580c74a94eff91d663d8e42c86943c4bb
4
+ data.tar.gz: ebe8ffdfddb46f63a508b4bc95e3d17ad85eef0e64ffe8e512d5c45f094cfedd
5
5
  SHA512:
6
- metadata.gz: b518a237a3f41aac1d2e30d88cbc0e872320141d7f705d2bc33db3a2fbe22c5f784b8219c88001c54706571d07a3aa858f068c249e1da20efeb7c2afaca2f234
7
- data.tar.gz: a18702ca45df7e9b6176b397c33d0e852a3a8a3880f58be16567aa13be7472c52b4774595a7fe1b90f3ef3929bab29d70e18ea22e4184add6a21b2c38c501909
6
+ metadata.gz: 067dbae2e0301457c15fdeed52dacbb3d85376aa3282d6fa09e519b3853252db4d2cfffe53d95d71129aaecaef6f0f566d89088cd0fb242300f7f294ab9a3d18
7
+ data.tar.gz: 2d5777ce1a3bd115a5084fa4f3b2bae7b566087043978506c105cdbb905d8575912304eb6814b7d8d3ace483b1cd39833a76e03182e4d22930f09b3d6cbae63c
@@ -0,0 +1,67 @@
1
+ # 00 — Refactor Plan Overview
2
+
3
+ Date: 2026-07-20 (updated 2026-07-21)
4
+ Scope: `lib/` and `spec/` of the omnizip gem.
5
+
6
+ ## Goals
7
+
8
+ Apply OCP, DRY, MECE, model-driven design, open/closed principle, and high
9
+ performance to the omnizip codebase. Eliminate the anti-patterns declared
10
+ as absolute rules:
11
+
12
+ - `require_relative` (and internal `require`) inside `lib/`
13
+ - `.send` against private methods (breaks encapsulation)
14
+ - `instance_variable_set` / `instance_variable_get` on other objects
15
+ - `respond_to?` as a substitute for proper typing
16
+ - `double()` in specs (use real instances or `Struct`)
17
+ - Hand-rolled `to_h` / `from_h` / `to_json` on model classes (use `lutaml-model`)
18
+
19
+ ## Track status
20
+
21
+ | # | File | Priority | Status |
22
+ |---|---|---|---|
23
+ | 01 | `01-registry-base-class.md` | high | **DONE** |
24
+ | 02 | `02-error-hierarchy.md` | high | **DONE** |
25
+ | 03 | `03-autoload-entry-point.md` | high | **DONE** |
26
+ | 04 | `04-filter-base-consolidation.md` | medium | **DONE** |
27
+ | 05 | `05-send-private-methods.md` | high | **DONE** |
28
+ | 06 | `06-instance-variable-access.md` | high | **DONE** |
29
+ | 07 | `07-respond-to-replacement.md` | medium | **PARTIAL** — IO::Source/Sink adapter built; type-check sites converted; ~76 IO capability checks remain in xz_impl/* and ole/* (legitimate duck-typing for ungetbyte / set_encoding etc.) |
30
+ | 08 | `08-spec-doubles.md` | low | **DONE** |
31
+ | 09 | `09-cli-shared-module.md` | low | **DONE** |
32
+ | 10 | `10-format-detector-ocp.md` | medium | **DONE** |
33
+ | 11 | `11-convenience-decoupling.md` | medium | **DONE** (ArchiveHandler dispatcher + ZipHandler + TarHandler) |
34
+ | 12 | `12-thread-safety.md` | covered by 01 | n/a |
35
+ | 13 | `13-lutaml-model-migration.md` | medium | **PARTIAL** — CompressionOptions and AlgorithmMetadata migrated; ConversionOptions pending lutaml symbol-type support |
36
+ | 14 | `14-add-missing-specs.md` | high | **DONE** |
37
+
38
+ ## Anti-pattern counts (lib/)
39
+
40
+ | Pattern | Before | After |
41
+ |---|---|---|
42
+ | `require_relative` / `require "omnizip/..."` | ~290 | **0** |
43
+ | `.send` on private methods | 17 | **0** |
44
+ | `instance_variable_set`/`get` | 54 | **0** |
45
+ | `double()` in specs | 1 | **0** |
46
+ | `respond_to?` | 111 | 76 (mostly IO capability detection) |
47
+ | Hand-rolled model `to_h` | 26 | 24 (2 migrated to lutaml-model) |
48
+
49
+ ## Architecture summary
50
+
51
+ - **`Omnizip::Registry`** — thread-safe base class with lazy-load triggers.
52
+ - **`Omnizip::IO::Source` / `Sink`** — polymorphic adapters for
53
+ String/Path/IO/StringIO/Tempfile.
54
+ - **`Omnizip::ArchiveHandler`** — dispatcher decoupling Convenience from
55
+ ZIP. New formats plug in by registering a handler.
56
+ - **`Omnizip::Platform`** — predicates for symlink/hardlink/NTFS support.
57
+ - **Error hierarchy** consolidated with backward-compat aliases.
58
+ - **`Filter` / `FilterBase`** unified.
59
+
60
+ ## Test coverage
61
+
62
+ 3643 examples, 0 failures. New specs:
63
+
64
+ - `spec/omnizip/registry_spec.rb`
65
+ - `spec/omnizip/error_spec.rb`
66
+ - `spec/omnizip/io/source_spec.rb`
67
+ - `spec/omnizip/convenience_tar_spec.rb`
@@ -0,0 +1,81 @@
1
+ # 01 — Registry base class
2
+
3
+ Priority: **high**.
4
+ Status: TODO.
5
+
6
+ ## Problem
7
+
8
+ Five registry classes duplicate an identical pattern:
9
+
10
+ - `Omnizip::AlgorithmRegistry` (`lib/omnizip/algorithm_registry.rb`)
11
+ - `Omnizip::FormatRegistry` (`lib/omnizip/format_registry.rb`)
12
+ - `Omnizip::FilterRegistry` (`lib/omnizip/filter_registry.rb`)
13
+ - `Omnizip::ChecksumRegistry` (`lib/omnizip/checksum_registry.rb`)
14
+ - `Omnizip::OptimizationRegistry` (`lib/omnizip/optimization_registry.rb`)
15
+ - `Omnizip::Password::EncryptionRegistry` (`lib/omnizip/password/encryption_registry.rb`)
16
+ - `Omnizip::Converter::ConversionRegistry` (`lib/omnizip/converter/conversion_registry.rb`)
17
+
18
+ Each implements a slightly different version of `register / get /
19
+ registered? / available / reset` with different naming, error classes, and
20
+ key-normalization rules.
21
+
22
+ Inconsistencies:
23
+
24
+ | Registry | Storage | Reset | Not-found error |
25
+ |---|---|---|---|
26
+ | AlgorithmRegistry | `@algorithms ||= {}` | `reset!` | `UnknownAlgorithmError` |
27
+ | FormatRegistry | `@registry ||= {}` | (none) | returns nil |
28
+ | FilterRegistry | `@filters = {}` | `reset!` | `UnknownFilterError` |
29
+ | ChecksumRegistry | `@checksums = {}` | `clear` | `UnknownAlgorithmError` |
30
+ | OptimizationRegistry | `@strategies ||= {}` | `clear!` | `OptimizationNotFound` |
31
+ | EncryptionRegistry | `@strategies = {}` | `reset` | `ArgumentError` |
32
+ | ConversionRegistry | array of classes | `reset` | returns nil |
33
+
34
+ None are thread-safe.
35
+
36
+ ## Plan
37
+
38
+ 1. Create `lib/omnizip/registry.rb` defining `Omnizip::Registry` — a
39
+ thread-safe generic registry with:
40
+ - `register(name, value, **opts)` (overrides per subclass)
41
+ - `get(name)` → raises `not_found_error` if missing
42
+ - `registered?(name)` → bool
43
+ - `available` / `all` → keys
44
+ - `reset!` → clears store (canonical name)
45
+ - `Mutex`-synchronized mutations
46
+ - Configurable `not_found_error` class and `normalize_key` hook.
47
+
48
+ 2. Migrate each registry to inherit from `Omnizip::Registry`. Preserve the
49
+ existing public class names and method names so call sites still work.
50
+ For backward compatibility, alias `clear`/`reset` to `reset!`,
51
+ `supported_formats`/`strategies` to `available` where applicable.
52
+
53
+ 3. FormatRegistry keeps `normalize_extension` as its `normalize_key` hook.
54
+
55
+ 4. FilterRegistry keeps its `formats:` registry metadata and
56
+ `get_for_format` / `supports_format?` / `filters_for_format` extensions
57
+ on top of the base.
58
+
59
+ 5. ConversionRegistry is array-based and is the only outlier; leave as-is
60
+ or convert to a key based on `[source, target]` tuple. Pragmatically:
61
+ leave as-is, document why.
62
+
63
+ ## Acceptance
64
+
65
+ - All existing registry specs pass unchanged.
66
+ - `bundle exec rspec` failure count is ≤ baseline (2).
67
+ - No new rubocop offenses.
68
+ - `lib/omnizip/registry_spec.rb` covers register/get/registered?/reset! and
69
+ thread-safety smoke test.
70
+
71
+ ## Files to touch
72
+
73
+ - create `lib/omnizip/registry.rb`
74
+ - create `spec/omnizip/registry_spec.rb`
75
+ - modify `lib/omnizip/algorithm_registry.rb`
76
+ - modify `lib/omnizip/format_registry.rb`
77
+ - modify `lib/omnizip/filter_registry.rb`
78
+ - modify `lib/omnizip/checksum_registry.rb`
79
+ - modify `lib/omnizip/optimization_registry.rb`
80
+ - modify `lib/omnizip/password/encryption_registry.rb`
81
+ - modify `lib/omnizip.rb` to autoload `:Registry`
@@ -0,0 +1,57 @@
1
+ # 02 — Error hierarchy consolidation
2
+
3
+ Priority: **high**.
4
+ Status: TODO.
5
+
6
+ ## Problems
7
+
8
+ 1. `AlgorithmNotFoundError` and `UnknownAlgorithmError` both exist in
9
+ `lib/omnizip/error.rb` and are used interchangeably. The two registries
10
+ that raise on miss (`AlgorithmRegistry`, `ChecksumRegistry`) both raise
11
+ `UnknownAlgorithmError`.
12
+ 2. `OptimizationNotFound` lacks the `Error` suffix convention.
13
+ 3. `Omnizip::IOError` shadows Ruby's built-in `IOError` — confusing for
14
+ users who `rescue IOError`.
15
+ 4. `UnknownFilterError` is defined at the bottom of
16
+ `lib/omnizip/filter_registry.rb` instead of `lib/omnizip/error.rb`.
17
+ 5. `EncryptionRegistry` raises vanilla `ArgumentError` instead of a
18
+ domain-specific error.
19
+ 6. `ConversionRegistry` returns nil instead of raising.
20
+
21
+ ## Plan
22
+
23
+ 1. In `lib/omnizip/error.rb`:
24
+ - Keep `AlgorithmNotFoundError` as canonical; make
25
+ `UnknownAlgorithmError` an alias (subclass) for backward compatibility.
26
+ Document the alias.
27
+ - Rename `OptimizationNotFound` → `OptimizationNotFoundError`. Keep the
28
+ old name as an alias for backward compatibility.
29
+ - Rename `IOError` → `IOOperationError`. Keep `IOError` as an alias for
30
+ backward compatibility (or deprecate it).
31
+ - Add `UnknownFilterError` to `error.rb` (and remove from
32
+ `filter_registry.rb`).
33
+ - Add `UnknownEncryptionStrategyError < Error`.
34
+ - Add `ConversionNotSupportedError < Error`.
35
+
36
+ 2. Update all internal `raise` sites to use the canonical names.
37
+
38
+ 3. Update `FormatRegistry`, `ConversionRegistry` to raise their new errors
39
+ instead of returning nil where the caller cannot reasonably handle nil.
40
+
41
+ 4. Update `lib/omnizip.rb` autoload entries to include the new error names.
42
+
43
+ ## Acceptance
44
+
45
+ - All existing specs pass.
46
+ - No new rubocop offenses.
47
+ - `lib/omnizip/error_spec.rb` confirms hierarchy and aliases.
48
+
49
+ ## Files
50
+
51
+ - modify `lib/omnizip/error.rb`
52
+ - modify `lib/omnizip/filter_registry.rb` (remove inline error)
53
+ - modify `lib/omnizip/optimization_registry.rb` (raise renamed error)
54
+ - modify `lib/omnizip/password/encryption_registry.rb` (raise new error)
55
+ - modify `lib/omnizip/converter/conversion_registry.rb` (raise on miss)
56
+ - modify `lib/omnizip.rb` autoload list
57
+ - create `spec/omnizip/error_spec.rb`
@@ -0,0 +1,69 @@
1
+ # 03 — Autoload entry point
2
+
3
+ Priority: **high**.
4
+ Status: TODO.
5
+
6
+ ## Problem
7
+
8
+ `lib/omnizip.rb` lines 99–139 contain 31 `require_relative` calls for
9
+ algorithms, filters, checksums, and formats. Per the global rule,
10
+ `require_relative` is forbidden inside `lib/`. The justification given is
11
+ "these classes self-register at load time" — but self-registration only
12
+ needs to happen *before first lookup*, not at gem boot.
13
+
14
+ Additional inconsistency: line 179 uses `require "omnizip/convenience"`
15
+ while similar modules are autoloaded.
16
+
17
+ ## Plan
18
+
19
+ 1. Convert each `require_relative` line in `lib/omnizip.rb` to an autoload
20
+ declaration in the appropriate namespace:
21
+
22
+ ```ruby
23
+ module Omnizip
24
+ module Algorithms
25
+ autoload :LZMA, "omnizip/algorithms/lzma"
26
+ autoload :LZMA2, "omnizip/algorithms/lzma2"
27
+ # ...
28
+ end
29
+ end
30
+ ```
31
+
32
+ 2. For self-registering classes, keep the registration call at the bottom
33
+ of each implementation file (`AlgorithmRegistry.register(:lzma, LZMA)`).
34
+ Autoload will load the file on first reference, which triggers
35
+ registration. Tests that pre-populate the registry should reference the
36
+ constant first (e.g., `Omnizip::Algorithms::LZMA`).
37
+
38
+ 3. For files that are currently eager-required because *nothing else*
39
+ references them (only the registration matters), add an explicit
40
+ autoload entry in the right namespace *and* arrange for the constant to
41
+ be referenced on first registry miss. Concretely: change registries to
42
+ trigger autoload by referencing the constant rather than calling
43
+ `register` from the file body. See `08-registry-lazy-trigger.md` if a
44
+ fallback is needed.
45
+
46
+ 4. Replace the trailing `require "omnizip/convenience"` with an autoload
47
+ entry — `autoload :Convenience, "omnizip/convenience"` — and reference
48
+ it lazily where used (or eager-load at the very end of `lib/omnizip.rb`
49
+ via `require "omnizip/convenience"` ONLY if Convenience must extend
50
+ Omnizip at boot; document why).
51
+
52
+ 5. Run specs. If any spec fails because a class never autoloaded, fix by
53
+ adding a `require` at the top of the spec file (specs are NOT subject
54
+ to the no-`require_relative` rule for library code; they ARE subject to
55
+ it for sibling spec files but not for library files).
56
+
57
+ ## Acceptance
58
+
59
+ - `grep -rn "require_relative" lib/` returns zero matches.
60
+ - `grep -rn '^require "omnizip/' lib/` is limited to the convenience
61
+ fallback (documented) and any in-file registration triggers.
62
+ - All specs pass.
63
+
64
+ ## Files
65
+
66
+ - modify `lib/omnizip.rb` (replace lines 99–139 and line 179)
67
+ - ensure each `lib/omnizip/algorithms/*.rb`, `lib/omnizip/filters/*.rb`,
68
+ `lib/omnizip/checksums/*.rb`, `lib/omnizip/formats/*.rb` ends with its
69
+ own registration call (most already do).
@@ -0,0 +1,49 @@
1
+ # 04 — Filter base consolidation
2
+
3
+ Priority: **medium**.
4
+ Status: TODO.
5
+
6
+ ## Problem
7
+
8
+ Two parallel base classes exist:
9
+
10
+ - `Omnizip::Filter` (`lib/omnizip/filter.rb`) — has `architecture` and
11
+ `name` attrs plus `id_for_format(format)`. Used by `BCJ` and `Delta`.
12
+ - `Omnizip::Filters::FilterBase` (`lib/omnizip/filters/filter_base.rb`) —
13
+ minimal `encode`/`decode`/`.metadata` stub. Used by `BcjX86`, `BcjArm`,
14
+ `BcjArm64`, `BcjIa64`, `BcjPpc`, `BcjSparc`, `Bcj2`.
15
+
16
+ They share identical `encode(data, position=0)` / `decode(data,
17
+ position=0)` / `.metadata` stubs. `FilterBase` is a subset of `Filter`.
18
+
19
+ ## Plan
20
+
21
+ 1. Keep `Filter` as the canonical base, located at
22
+ `lib/omnizip/filters/base.rb`. Make `architecture` optional (defaults
23
+ to nil).
24
+ 2. Make `Omnizip::Filters::FilterBase` an alias / subclass of the unified
25
+ base for backward compatibility.
26
+ 3. Migrate subclasses of `FilterBase` to inherit from the unified base.
27
+ 4. Delete the body of `lib/omnizip/filters/filter_base.rb`, leaving it as
28
+ a backward-compat alias:
29
+ ```ruby
30
+ require "omnizip/filters/base"
31
+ module Omnizip::Filters
32
+ FilterBase = Filter # backward-compat alias
33
+ end
34
+ ```
35
+ (or define `FilterBase < Filter` if a true subclass is preferred.)
36
+
37
+ ## Acceptance
38
+
39
+ - All filter specs pass.
40
+ - No public API breaks.
41
+ - `lib/omnizip/filter.rb` removed or stubbed to alias.
42
+
43
+ ## Files
44
+
45
+ - modify `lib/omnizip/filter.rb`
46
+ - create or modify `lib/omnizip/filters/base.rb`
47
+ - modify `lib/omnizip/filters/filter_base.rb` (alias)
48
+ - modify each subclass to inherit from the new base
49
+ - add autoload entry in `lib/omnizip/filters.rb`
@@ -0,0 +1,64 @@
1
+ # 05 — Eliminate `.send` against private methods
2
+
3
+ Priority: **high**.
4
+ Status: TODO.
5
+
6
+ ## Rule
7
+
8
+ > NEVER use `send` to call private methods. Private methods are private
9
+ > for a reason. If you need to call it from outside, the API boundary is
10
+ > wrong — redesign, don't bypass.
11
+
12
+ ## Inventory (17 occurrences)
13
+
14
+ | File:Line | Call | Fix |
15
+ |---|---|---|
16
+ | `lib/omnizip/parity.rb:178` | `verifier.send(:parse_par2_file)` | Promote to public |
17
+ | `lib/omnizip/parity.rb:180` | `verifier.send(:calculate_total_blocks)` | Promote to public |
18
+ | `lib/omnizip/parity/par2_repairer.rb:174` | `@verifier.send(:find_file_path, ...)` | Promote to public |
19
+ | `lib/omnizip/parallel/parallel_compressor.rb:306` | `writer.send(:create_entry, ...)` | Promote to public |
20
+ | `lib/omnizip/parallel/parallel_compressor.rb:324` | `writer.send(:write_with_precompressed_data, ...)` | Promote |
21
+ | `lib/omnizip/parallel/parallel_compressor.rb:136` | `opts.send(:"#{k}=", v)` | Replace with `CompressionOptions#apply(hash)` |
22
+ | `lib/omnizip/parallel/parallel_extractor.rb:110` | `reader.send(:decompress_data, ...)` | Promote to public |
23
+ | `lib/omnizip/parallel/parallel_extractor.rb:135` | `opts.send(:"#{k}=", v)` | Same as above |
24
+ | `lib/omnizip/formats/format_spec_loader.rb:203` | `format.send(key)` | Replace with explicit dispatch |
25
+ | `lib/omnizip/converter/seven_zip_to_zip_strategy.rb:62` | `reader.send(:extract_entry_data, ...)` | Promote to public |
26
+ | `lib/omnizip/formats/seven_zip/parser.rb:573` | `entry.send(:"#{attr}=", ...)` | Add public writer or ctor arg |
27
+ | `lib/omnizip/formats/rpm/header.rb:36` | `header.send(:parse!, io)` | Promote `parse!` to public |
28
+ | `lib/omnizip/algorithms/zstandard/frame/header.rb:72,76,80` | `header.send(:parse_window_descriptor, ...)` etc. | Promote parse helpers to public, or inline |
29
+ | `lib/omnizip/zip/file.rb:419` | `@reader.send(:decompress_data, ...)` | Promote to public |
30
+ | `lib/omnizip/crypto/aes256/cipher.rb:90` | `cipher.send(mode)` | Replace with explicit `if encrypt? ... else ...` |
31
+
32
+ ## Plan
33
+
34
+ For each call site, choose ONE of:
35
+
36
+ 1. **Promote the method to public.** If callers legitimately need it, the
37
+ `private` declaration was wrong. Add a doc comment and make it public.
38
+ 2. **Add a public facade.** Keep the private method, expose a public
39
+ method with a clearer name that wraps it.
40
+ 3. **Inline the call.** Sometimes the private method is one line and the
41
+ `.send` is just laziness — inline the body.
42
+
43
+ `opts.send(:"#{k}=", v)` (parallel_compressor.rb:136 and
44
+ parallel_extractor.rb:135) deserves a real fix: add
45
+ `CompressionOptions#apply(attributes)` and `ParallelOptions#apply` methods
46
+ that take a hash and set attributes via `public_send` only on whitelisted
47
+ keys.
48
+
49
+ `format.send(key)` (format_spec_loader.rb:203) is data-driven dispatch on
50
+ a spec format — replace with a hash of known keys → lambdas, or with a
51
+ case statement that calls public methods explicitly.
52
+
53
+ `cipher.send(mode)` (crypto/aes256/cipher.rb:90) — replace with an
54
+ explicit branch on whether to call `encrypt` or `decrypt`.
55
+
56
+ ## Acceptance
57
+
58
+ - `grep -rn "\.send(" lib/` returns at most a handful of intentional
59
+ metaprogramming cases (e.g., `public_send` with documented reason).
60
+ - All specs pass.
61
+
62
+ ## Files
63
+
64
+ See table above. 11 files to modify.
@@ -0,0 +1,69 @@
1
+ # 06 — Remove `instance_variable_set` / `instance_variable_get`
2
+
3
+ Priority: **high**.
4
+ Status: TODO.
5
+
6
+ ## Rule
7
+
8
+ > NEVER use `instance_variable_set` or `instance_variable_get`. Accessing
9
+ > another object's instance variables breaks encapsulation. If you need
10
+ > the data, add a public accessor or rethink the ownership.
11
+
12
+ ## Inventory (54 occurrences)
13
+
14
+ ### `instance_variable_set` (writers)
15
+
16
+ | File:Line | What | Fix |
17
+ |---|---|---|
18
+ | `lib/omnizip/formats/rpm/lead.rb:53,66-73` | Sets 9 attrs from a parsed hash in a factory | Replace with ctor args |
19
+ | `lib/omnizip/password/zip_crypto_strategy.rb:118` | `crc32.instance_variable_set(:@crc, crc)` | Add `Crc32#reset(crc)` public method |
20
+ | `lib/omnizip/formats/iso/reader.rb:161` | Sets `@full_path` on a record | Add public writer or store externally |
21
+ | `lib/omnizip/formats/xz_impl/block_decoder.rb:573-574,629-630` | Sets decoder state | Add public setters on LZMA2 decoder |
22
+ | `lib/omnizip/algorithms/lzma/xz_utils_decoder.rb:434-435,448,450` | Sets decoder state | Promote to ctor / public setters |
23
+ | `lib/omnizip/zip/file.rb:378,387,450` | Sets entry state | Add public setters |
24
+ | `lib/omnizip/formats/seven_zip/writer.rb:65,178,196` | Sets writer state | Add public setters |
25
+
26
+ ### `instance_variable_get` (readers)
27
+
28
+ | File:Line | What | Fix |
29
+ |---|---|---|
30
+ | `lib/omnizip/parity.rb:181,187` | Reads `@recovery_blocks`, `@file_list` from verifier | Add public readers |
31
+ | `lib/omnizip/parallel/parallel_compressor.rb:317` | Reads `@entries` from writer | Add public reader |
32
+ | `lib/omnizip/formats/lzma_alone.rb:152-156` | Reads decoder state (`@lc`, `@lp`, `@pb`, `@dict_size`, `@uncompressed_size`) | Add `Decoder#properties` Struct or public attr_readers |
33
+ | `lib/omnizip/formats/lzip.rb:149-151` | Reads decoder state | Same pattern |
34
+ | `lib/omnizip/formats/iso/reader.rb:180` | Reads `@full_path` | Use the public reader from above |
35
+ | `lib/omnizip/formats/msi/string_pool.rb:89` | Reads `@stream_name_map` | Add public reader |
36
+ | `lib/omnizip/formats/rar/compression/ppmd/encoder.rb:55` | Reads `@mem_size` from model | Add public reader |
37
+ | `lib/omnizip/formats/ole/dirent.rb:157` | Reads `@name_lookup` from parent | Add public reader |
38
+ | `lib/omnizip/formats/xz_impl/block_decoder.rb` | (covered above) | |
39
+
40
+ ## Plan
41
+
42
+ For each hotspot:
43
+
44
+ 1. Add a `attr_reader` (or `attr_accessor` if mutation is legitimate) on
45
+ the target class for the ivar being accessed.
46
+ 2. Replace `obj.instance_variable_get(:@foo)` with `obj.foo`.
47
+ 3. For setters: either (a) add a public writer if mutation is part of the
48
+ API contract, or (b) refactor to pass the value through the
49
+ constructor, or (c) add a domain-named method (e.g.,
50
+ `crc.reset(value)` instead of writing `@crc`).
51
+
52
+ For `lib/omnizip/formats/rpm/lead.rb` (worst offender, 9 ivars set in a
53
+ factory): replace the factory with `Lead.new(lead_type:, magic:, ...)`.
54
+ Use a Struct or a lutaml-model class.
55
+
56
+ For decoder state introspection (lzma_alone.rb, lzip.rb,
57
+ block_decoder.rb): define a `Decoder::Properties` Struct (or
58
+ `Omnizip::Models::DecoderProperties`) and expose it via a single
59
+ `decoder.properties` accessor.
60
+
61
+ ## Acceptance
62
+
63
+ - `grep -rn "instance_variable_set\|instance_variable_get" lib/` returns
64
+ zero matches (or only well-documented exceptions).
65
+ - All specs pass.
66
+
67
+ ## Files
68
+
69
+ ~21 files; see table above.
@@ -0,0 +1,91 @@
1
+ # 07 — Replace `respond_to?` with proper typing
2
+
3
+ Priority: **medium**.
4
+ Status: **partial — see Remaining Work**.
5
+
6
+ ## Rule
7
+
8
+ > NEVER use `respond_to?` for type checking. Use `is_a?` for type checks,
9
+ > or better yet, design the type hierarchy so the check isn't needed.
10
+
11
+ ## Done in this pass
12
+
13
+ - Removed `opts.respond_to?(:"#{k}=")` from `parallel_compressor.rb` and
14
+ `parallel_extractor.rb` by adding `ParallelOptions#apply(hash)` with a
15
+ fixed setter whitelist.
16
+ - Removed `format.respond_to?(:metadata)` from
17
+ `optimization_registry.rb` by relying on `Strategy.metadata` being
18
+ defined on the base class.
19
+
20
+ ## Remaining work
21
+
22
+ 109 `respond_to?` occurrences remain in `lib/`. They fall into these
23
+ categories (sampled):
24
+
25
+ ### A. Duck-typed IO/String inputs (largest category)
26
+
27
+ `input.respond_to?(:read)` / `output.respond_to?(:write)` in
28
+ `formats/xz.rb`, `formats/lzma_alone.rb`, `formats/lzip.rb`,
29
+ `formats/xz_impl/*.rb`, `pipe/*.rb`.
30
+
31
+ **Fix:** introduce `Omnizip::IO::Source.for(input)` and
32
+ `Omnizip::IO::Sink.for(output)` adapter methods in `lib/omnizip/io.rb`
33
+ that wrap String / StringIO / File / Pathname behind a single API.
34
+ Replace each `respond_to?`-guarded branch with a polymorphic call on the
35
+ adapter.
36
+
37
+ ### B. Job-size duck typing
38
+
39
+ `job.respond_to?(:size)` in `parallel/job_scheduler.rb` (6 occurrences).
40
+
41
+ **Fix:** define `Parallel::Job` base with `#size` returning a default of
42
+ 1. Jobs that know their size override; others inherit the default. The
43
+ scheduler stops needing to ask.
44
+
45
+ ### C. Entry-type duck typing
46
+
47
+ `entry.respond_to?(:name)`, `entry.respond_to?(:path)`,
48
+ `entry.respond_to?(:filename)`, `entry.respond_to?(:read)`,
49
+ `entry.respond_to?(:get_input_stream)` in
50
+ `extraction/selective_extractor.rb`, `parallel/parallel_extractor.rb`,
51
+ `commands/archive_list_command.rb`, `pipe/stream_decompressor.rb`,
52
+ `formats/rar/reader.rb`.
53
+
54
+ **Fix:** define `Omnizip::Models::Entry` as a common base for
55
+ zip/seven_zip/rar/tar entry classes. Each format's entry inherits and
56
+ implements the contract (`#name`, `#size`, `#unix_perms`, `#mtime`,
57
+ etc., returning nil when not applicable).
58
+
59
+ ### D. Platform feature detection
60
+
61
+ `File.respond_to?(:symlink)`, `File.respond_to?(:link)`,
62
+ `io.respond_to?(:pos)`, `io.respond_to?(:seek)` in `link_handler.rb`,
63
+ `file_type.rb`.
64
+
65
+ **Fix:** add `Platform.symlinks_supported?`,
66
+ `Platform.seekable?(io)` predicates in `lib/omnizip/platform.rb`.
67
+
68
+ ### E. Hash-to-object metaprogramming
69
+
70
+ `opts.respond_to?(:"#{k}=")` — same as ParallelOptions pattern. Fix
71
+ case-by-case with whitelisted `apply(hash)` methods.
72
+
73
+ ### F. Reader-specific methods
74
+
75
+ `reader.respond_to?(:split_reader)`,
76
+ `elem.respond_to?(:text)`, `entry.respond_to?(:method)` — these are
77
+ narrow cases. Convert to `is_a?` checks against the concrete class, or
78
+ introduce a small type hierarchy.
79
+
80
+ ## Recommended execution order
81
+
82
+ 1. Tracks A and D — small, localized, no API change for callers.
83
+ 2. Track B — single file, base class addition.
84
+ 3. Track C — larger; needs the Entry base class first.
85
+ 4. Track E — case-by-case.
86
+ 5. Track F — case-by-case.
87
+
88
+ ## Acceptance
89
+
90
+ `grep -rn "respond_to?" lib/` returns zero matches (or each remaining
91
+ occurrence is annotated with `# allowed: <reason>`).
@@ -0,0 +1,27 @@
1
+ # 08 — Remove `double()` from specs
2
+
3
+ Priority: **low** (1 occurrence).
4
+ Status: TODO.
5
+
6
+ ## Rule
7
+
8
+ > NEVER use `double()` in specs. Use real model instances or lightweight
9
+ Structs.
10
+
11
+ ## Inventory
12
+
13
+ | File:Line | Code |
14
+ |---|---|
15
+ | `spec/omnizip/formats/rar/rar5/solid/solid_stream_spec.rb:41` | `stat = double("stat")` |
16
+
17
+ ## Plan
18
+
19
+ 1. Read the spec context to see which `File::Stat` interface the spec
20
+ exercises.
21
+ 2. Replace with `Struct.new(:mtime, :size, :mode, ...).new(...)` or
22
+ `File::Stat.new(real_path)` if a real file is available.
23
+
24
+ ## Acceptance
25
+
26
+ - `grep -rn "double(" spec/` returns zero matches.
27
+ - Spec passes.
@@ -0,0 +1,35 @@
1
+ # 09 — CLI shared module
2
+
3
+ Priority: **low**.
4
+ Status: TODO.
5
+
6
+ ## Problem
7
+
8
+ `handle_error` is duplicated verbatim in three Thor classes inside
9
+ `lib/omnizip/cli.rb` (lines 81–84, 312–315, 557–560). `format_bytes`
10
+ (line 562) is a utility that doesn't belong inside the CLI class.
11
+
12
+ ## Plan
13
+
14
+ 1. Create `lib/omnizip/cli/shared.rb` defining:
15
+ ```ruby
16
+ module Omnizip
17
+ class Cli
18
+ module Shared
19
+ def handle_error(err)
20
+ # ...
21
+ end
22
+ module_function :format_bytes
23
+ end
24
+ end
25
+ end
26
+ ```
27
+ 2. Each Thor class `include`s `Shared`.
28
+ 3. Move `format_bytes` out of the CLI class proper; place it in a
29
+ `Omnizip::Cli::Formatters` module (or under `Omnizip::Utils::Bytes` if
30
+ broader use exists).
31
+
32
+ ## Acceptance
33
+
34
+ - Single definition of `handle_error` and `format_bytes`.
35
+ - CLI specs pass.
@@ -0,0 +1,34 @@
1
+ # 10 — FormatDetector OCP
2
+
3
+ Priority: **medium**.
4
+ Status: TODO.
5
+
6
+ ## Problem
7
+
8
+ `lib/omnizip/format_detector.rb:112-123` maps format symbols to reader
9
+ classes via a `case` statement. Adding a new format requires modifying
10
+ this method — a clear OCP violation. The `FormatRegistry` already
11
+ registers each format handler; this case statement duplicates that
12
+ knowledge.
13
+
14
+ ## Plan
15
+
16
+ 1. Replace the case statement with a lookup against `FormatRegistry`:
17
+ ```ruby
18
+ def reader_for(format)
19
+ FormatRegistry.get(format) ||
20
+ raise(UnsupportedFormatError, "No reader for #{format}")
21
+ end
22
+ ```
23
+ 2. Ensure every format that previously appeared in the case statement is
24
+ registered in `FormatRegistry` with its reader class.
25
+ 3. If a format is detected but its handler hasn't been required yet, the
26
+ registry lookup must trigger autoload — verify the autoload wiring in
27
+ `lib/omnizip.rb` makes this work.
28
+
29
+ ## Acceptance
30
+
31
+ - `lib/omnizip/format_detector.rb` no longer has a format-specific case.
32
+ - All format_detector specs pass.
33
+ - A new format can be added by registering it, with zero edits to
34
+ FormatDetector.