omnizip 0.3.8 → 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 (201) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +51 -50
  3. data/Gemfile +2 -0
  4. data/Rakefile +3 -0
  5. data/TODO.refactor/00-overview.md +67 -0
  6. data/TODO.refactor/01-registry-base-class.md +81 -0
  7. data/TODO.refactor/02-error-hierarchy.md +57 -0
  8. data/TODO.refactor/03-autoload-entry-point.md +69 -0
  9. data/TODO.refactor/04-filter-base-consolidation.md +49 -0
  10. data/TODO.refactor/05-send-private-methods.md +64 -0
  11. data/TODO.refactor/06-instance-variable-access.md +69 -0
  12. data/TODO.refactor/07-respond-to-replacement.md +91 -0
  13. data/TODO.refactor/08-spec-doubles.md +27 -0
  14. data/TODO.refactor/09-cli-shared-module.md +35 -0
  15. data/TODO.refactor/10-format-detector-ocp.md +34 -0
  16. data/TODO.refactor/11-convenience-decoupling.md +38 -0
  17. data/TODO.refactor/12-thread-safety.md +8 -0
  18. data/TODO.refactor/13-lutaml-model-migration.md +82 -0
  19. data/TODO.refactor/14-add-missing-specs.md +25 -0
  20. data/lib/omnizip/algorithm.rb +0 -2
  21. data/lib/omnizip/algorithm_registry.rb +29 -57
  22. data/lib/omnizip/algorithms/lzma/bit_model.rb +1 -1
  23. data/lib/omnizip/algorithms/lzma/dictionary.rb +4 -3
  24. data/lib/omnizip/algorithms/lzma/distance_coder.rb +5 -102
  25. data/lib/omnizip/algorithms/lzma/length_coder.rb +4 -92
  26. data/lib/omnizip/algorithms/lzma/literal_decoder.rb +4 -119
  27. data/lib/omnizip/algorithms/lzma/lzip_decoder.rb +2 -0
  28. data/lib/omnizip/algorithms/lzma/match_finder.rb +59 -29
  29. data/lib/omnizip/algorithms/lzma/optimal_encoder.rb +20 -12
  30. data/lib/omnizip/algorithms/lzma/range_decoder.rb +77 -269
  31. data/lib/omnizip/algorithms/lzma/range_encoder.rb +0 -9
  32. data/lib/omnizip/algorithms/lzma/xz_range_encoder_exact.rb +0 -15
  33. data/lib/omnizip/algorithms/lzma/xz_state.rb +0 -8
  34. data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +189 -968
  35. data/lib/omnizip/algorithms/lzma2/encoder.rb +0 -2
  36. data/lib/omnizip/algorithms/lzma2/lzma2_chunk.rb +0 -2
  37. data/lib/omnizip/algorithms/lzma2/properties.rb +0 -2
  38. data/lib/omnizip/algorithms/ppmd7/model.rb +1 -1
  39. data/lib/omnizip/algorithms/ppmd8/context.rb +0 -2
  40. data/lib/omnizip/algorithms/ppmd8/model.rb +1 -1
  41. data/lib/omnizip/algorithms/ppmd8.rb +0 -2
  42. data/lib/omnizip/algorithms/ppmd_base.rb +0 -2
  43. data/lib/omnizip/algorithms/sevenzip_lzma2.rb +0 -5
  44. data/lib/omnizip/algorithms/xz_lzma2.rb +0 -5
  45. data/lib/omnizip/algorithms/zstandard/frame/header.rb +3 -5
  46. data/lib/omnizip/algorithms.rb +1 -0
  47. data/lib/omnizip/archive_handler.rb +75 -0
  48. data/lib/omnizip/archive_handlers/tar_handler.rb +51 -0
  49. data/lib/omnizip/archive_handlers/zip_handler.rb +73 -0
  50. data/lib/omnizip/archive_handlers.rb +11 -0
  51. data/lib/omnizip/checksum_registry.rb +23 -96
  52. data/lib/omnizip/cli/shared.rb +31 -0
  53. data/lib/omnizip/cli.rb +6 -43
  54. data/lib/omnizip/commands/archive_create_command.rb +0 -6
  55. data/lib/omnizip/commands/archive_extract_command.rb +0 -4
  56. data/lib/omnizip/commands/archive_list_command.rb +0 -4
  57. data/lib/omnizip/commands/archive_repair_command.rb +0 -2
  58. data/lib/omnizip/commands/archive_verify_command.rb +0 -2
  59. data/lib/omnizip/commands/compress_command.rb +0 -2
  60. data/lib/omnizip/commands/decompress_command.rb +0 -2
  61. data/lib/omnizip/commands/list_command.rb +0 -2
  62. data/lib/omnizip/commands/metadata_command.rb +0 -5
  63. data/lib/omnizip/commands/parity_create_command.rb +0 -2
  64. data/lib/omnizip/commands/parity_repair_command.rb +0 -2
  65. data/lib/omnizip/commands/parity_verify_command.rb +0 -2
  66. data/lib/omnizip/commands/profile_list_command.rb +0 -2
  67. data/lib/omnizip/commands/profile_show_command.rb +0 -2
  68. data/lib/omnizip/convenience.rb +111 -234
  69. data/lib/omnizip/converter/seven_zip_to_zip_strategy.rb +1 -1
  70. data/lib/omnizip/crypto/aes256/cipher.rb +6 -1
  71. data/lib/omnizip/error.rb +22 -18
  72. data/lib/omnizip/filter.rb +5 -3
  73. data/lib/omnizip/filter_registry.rb +74 -98
  74. data/lib/omnizip/filters/bcj2/decoder.rb +196 -130
  75. data/lib/omnizip/filters/bcj2.rb +0 -2
  76. data/lib/omnizip/filters/bcj_x86.rb +0 -2
  77. data/lib/omnizip/filters/filter_base.rb +4 -70
  78. data/lib/omnizip/filters/registry.rb +0 -2
  79. data/lib/omnizip/format_detector.rb +21 -12
  80. data/lib/omnizip/format_registry.rb +79 -38
  81. data/lib/omnizip/formats/bzip2_file.rb +0 -2
  82. data/lib/omnizip/formats/cpio/entry.rb +0 -2
  83. data/lib/omnizip/formats/cpio/reader.rb +0 -1
  84. data/lib/omnizip/formats/cpio/writer.rb +0 -1
  85. data/lib/omnizip/formats/cpio.rb +0 -1
  86. data/lib/omnizip/formats/format_spec_loader.rb +1 -1
  87. data/lib/omnizip/formats/gzip.rb +0 -2
  88. data/lib/omnizip/formats/iso/directory_record.rb +6 -3
  89. data/lib/omnizip/formats/iso/reader.rb +2 -2
  90. data/lib/omnizip/formats/iso.rb +0 -1
  91. data/lib/omnizip/formats/lzip.rb +5 -25
  92. data/lib/omnizip/formats/lzma_alone.rb +7 -25
  93. data/lib/omnizip/formats/msi/cab_extractor.rb +181 -0
  94. data/lib/omnizip/formats/msi/constants.rb +157 -0
  95. data/lib/omnizip/formats/msi/directory_resolver.rb +188 -0
  96. data/lib/omnizip/formats/msi/entry.rb +128 -0
  97. data/lib/omnizip/formats/msi/reader.rb +301 -0
  98. data/lib/omnizip/formats/msi/string_pool.rb +246 -0
  99. data/lib/omnizip/formats/msi/table_parser.rb +244 -0
  100. data/lib/omnizip/formats/msi.rb +84 -0
  101. data/lib/omnizip/formats/ole/allocation_table.rb +10 -1
  102. data/lib/omnizip/formats/ole/dirent.rb +20 -3
  103. data/lib/omnizip/formats/ole/header.rb +0 -1
  104. data/lib/omnizip/formats/ole/ranges_io.rb +21 -3
  105. data/lib/omnizip/formats/ole/storage.rb +19 -4
  106. data/lib/omnizip/formats/ole.rb +0 -2
  107. data/lib/omnizip/formats/rar/block_parser.rb +0 -1
  108. data/lib/omnizip/formats/rar/compression/ppmd/encoder.rb +1 -1
  109. data/lib/omnizip/formats/rar/decompressor.rb +38 -1
  110. data/lib/omnizip/formats/rar/header.rb +0 -1
  111. data/lib/omnizip/formats/rar/rar5/writer.rb +0 -2
  112. data/lib/omnizip/formats/rar/reader.rb +2 -3
  113. data/lib/omnizip/formats/rar/writer.rb +0 -1
  114. data/lib/omnizip/formats/rar.rb +2 -2
  115. data/lib/omnizip/formats/rpm/entry.rb +0 -1
  116. data/lib/omnizip/formats/rpm/header.rb +3 -8
  117. data/lib/omnizip/formats/rpm/lead.rb +37 -26
  118. data/lib/omnizip/formats/rpm/tag.rb +0 -1
  119. data/lib/omnizip/formats/rpm/writer.rb +0 -1
  120. data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +98 -61
  121. data/lib/omnizip/formats/seven_zip/coder_chain.rb +1 -2
  122. data/lib/omnizip/formats/seven_zip/encoded_header.rb +0 -1
  123. data/lib/omnizip/formats/seven_zip/encrypted_header.rb +0 -1
  124. data/lib/omnizip/formats/seven_zip/header.rb +0 -1
  125. data/lib/omnizip/formats/seven_zip/header_encryptor.rb +0 -1
  126. data/lib/omnizip/formats/seven_zip/header_writer.rb +22 -61
  127. data/lib/omnizip/formats/seven_zip/models/file_entry.rb +1 -1
  128. data/lib/omnizip/formats/seven_zip/parser.rb +14 -13
  129. data/lib/omnizip/formats/seven_zip/reader.rb +82 -19
  130. data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +0 -15
  131. data/lib/omnizip/formats/seven_zip/split_archive_writer.rb +0 -1
  132. data/lib/omnizip/formats/seven_zip/writer.rb +22 -68
  133. data/lib/omnizip/formats/seven_zip.rb +63 -5
  134. data/lib/omnizip/formats/tar/entry.rb +0 -2
  135. data/lib/omnizip/formats/tar/header.rb +0 -2
  136. data/lib/omnizip/formats/tar/reader.rb +0 -2
  137. data/lib/omnizip/formats/tar/writer.rb +0 -2
  138. data/lib/omnizip/formats/tar.rb +0 -1
  139. data/lib/omnizip/formats/xar/entry.rb +0 -1
  140. data/lib/omnizip/formats/xar/header.rb +0 -1
  141. data/lib/omnizip/formats/xar/reader.rb +0 -1
  142. data/lib/omnizip/formats/xar/toc.rb +3 -3
  143. data/lib/omnizip/formats/xar/writer.rb +11 -3
  144. data/lib/omnizip/formats/xar.rb +0 -1
  145. data/lib/omnizip/formats/xz/reader.rb +7 -3
  146. data/lib/omnizip/formats/xz.rb +3 -40
  147. data/lib/omnizip/formats/xz_impl/block_decoder.rb +4 -10
  148. data/lib/omnizip/formats/xz_impl/block_encoder.rb +0 -2
  149. data/lib/omnizip/formats/xz_impl/block_header_parser.rb +0 -2
  150. data/lib/omnizip/formats/xz_impl/index_decoder.rb +0 -1
  151. data/lib/omnizip/formats/xz_impl/index_encoder.rb +0 -1
  152. data/lib/omnizip/formats/xz_impl/stream_encoder.rb +1 -1
  153. data/lib/omnizip/formats/xz_impl/stream_footer.rb +0 -1
  154. data/lib/omnizip/formats/xz_impl/stream_footer_parser.rb +0 -1
  155. data/lib/omnizip/formats/xz_impl/stream_header.rb +0 -1
  156. data/lib/omnizip/formats/xz_impl/stream_header_parser.rb +0 -2
  157. data/lib/omnizip/formats/zip/central_directory_header.rb +2 -3
  158. data/lib/omnizip/formats/zip/end_of_central_directory.rb +0 -2
  159. data/lib/omnizip/formats/zip/local_file_header.rb +0 -2
  160. data/lib/omnizip/formats/zip/reader.rb +12 -2
  161. data/lib/omnizip/formats/zip/writer.rb +73 -1
  162. data/lib/omnizip/formats/zip/zip64_end_of_central_directory.rb +0 -2
  163. data/lib/omnizip/formats/zip/zip64_end_of_central_directory_locator.rb +0 -2
  164. data/lib/omnizip/formats/zip/zip64_extra_field.rb +0 -2
  165. data/lib/omnizip/formats/zip.rb +0 -1
  166. data/lib/omnizip/formats.rb +9 -22
  167. data/lib/omnizip/implementations/seven_zip/lzma/range_decoder.rb +2 -2
  168. data/lib/omnizip/implementations/seven_zip/lzma/range_encoder.rb +1 -1
  169. data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +0 -6
  170. data/lib/omnizip/implementations/xz_utils/lzma2/decoder.rb +18 -185
  171. data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +3 -3
  172. data/lib/omnizip/io/source.rb +136 -0
  173. data/lib/omnizip/io.rb +2 -0
  174. data/lib/omnizip/link_handler.rb +7 -3
  175. data/lib/omnizip/metadata/metadata_editor.rb +1 -1
  176. data/lib/omnizip/models/algorithm_metadata.rb +33 -60
  177. data/lib/omnizip/models/compression_options.rb +40 -53
  178. data/lib/omnizip/models/conversion_options.rb +9 -18
  179. data/lib/omnizip/models/parallel_options.rb +25 -0
  180. data/lib/omnizip/optimization_registry.rb +8 -44
  181. data/lib/omnizip/parallel/job_scheduler.rb +26 -8
  182. data/lib/omnizip/parallel/parallel_compressor.rb +10 -20
  183. data/lib/omnizip/parallel/parallel_extractor.rb +20 -11
  184. data/lib/omnizip/parity/par2_repairer.rb +5 -5
  185. data/lib/omnizip/parity/par2_verifier.rb +27 -22
  186. data/lib/omnizip/parity.rb +4 -4
  187. data/lib/omnizip/password/encryption_registry.rb +11 -50
  188. data/lib/omnizip/password/zip_crypto_strategy.rb +1 -4
  189. data/lib/omnizip/pipe/stream_decompressor.rb +2 -3
  190. data/lib/omnizip/profile/archive_profile.rb +0 -2
  191. data/lib/omnizip/profile/binary_profile.rb +0 -2
  192. data/lib/omnizip/profile/compression_profile.rb +0 -1
  193. data/lib/omnizip/profile/profile_detector.rb +0 -2
  194. data/lib/omnizip/profile/text_profile.rb +0 -2
  195. data/lib/omnizip/profile.rb +0 -2
  196. data/lib/omnizip/registry.rb +112 -0
  197. data/lib/omnizip/version.rb +1 -1
  198. data/lib/omnizip/zip/file.rb +20 -9
  199. data/lib/omnizip.rb +26 -55
  200. data/lib/tasks/msi_test_files.rake +66 -0
  201. metadata +57 -6
data/lib/omnizip/error.rb CHANGED
@@ -3,59 +3,64 @@
3
3
  # Copyright (C) 2025 Ribose Inc.
4
4
 
5
5
  module Omnizip
6
- # Base error class for all Omnizip errors
7
6
  class Error < StandardError
8
7
  end
9
8
 
10
- # Error raised when compression fails
11
9
  class CompressionError < Error
12
10
  end
13
11
 
14
- # Error raised when decompression fails
15
12
  class DecompressionError < Error
16
13
  end
17
14
 
18
- # Error raised when an algorithm is not found
19
15
  class AlgorithmNotFoundError < Error
20
16
  end
21
17
 
22
- # Error raised when an unknown algorithm is requested
23
- class UnknownAlgorithmError < Error
18
+ # Canonical alias of +AlgorithmNotFoundError+. Older code referenced
19
+ # +UnknownAlgorithmError+; both names are kept for backward
20
+ # compatibility.
21
+ UnknownAlgorithmError = AlgorithmNotFoundError
22
+
23
+ class UnknownChecksumError < Error
24
+ end
25
+
26
+ class UnknownFilterError < Error
27
+ end
28
+
29
+ class UnknownEncryptionStrategyError < Error
30
+ end
31
+
32
+ class ConversionNotSupportedError < Error
24
33
  end
25
34
 
26
- # Error raised when a format is not supported
27
35
  class UnsupportedFormatError < Error
28
36
  end
29
37
 
30
- # Error raised when format parsing fails
31
38
  class FormatError < Error
32
39
  end
33
40
 
34
- # Error raised when archive is invalid
35
41
  class InvalidArchiveError < Error
36
42
  end
37
43
 
38
- # Error raised when I/O operations fail
39
- class IOError < Error
44
+ # Renamed from +IOError+ to avoid shadowing Ruby's built-in IOError.
45
+ # The legacy name is kept as an alias.
46
+ class IOOperationError < Error
40
47
  end
48
+ IOError = IOOperationError
41
49
 
42
- # Error raised when checksum validation fails
43
50
  class ChecksumError < Error
44
51
  end
45
52
 
46
- # Error raised when optimization strategy is not found
47
- class OptimizationNotFound < Error
53
+ # Canonical name. +OptimizationNotFound+ kept as alias.
54
+ class OptimizationNotFoundError < Error
48
55
  end
56
+ OptimizationNotFound = OptimizationNotFoundError
49
57
 
50
- # Error raised when progress tracking fails
51
58
  class ProgressError < Error
52
59
  end
53
60
 
54
- # Error raised when ETA calculation fails
55
61
  class ETAError < Error
56
62
  end
57
63
 
58
- # Error raised when RAR write is attempted without license
59
64
  class NotLicensedError < Error
60
65
  def initialize(message = default_message)
61
66
  super
@@ -82,7 +87,6 @@ module Omnizip
82
87
  end
83
88
  end
84
89
 
85
- # Error raised when RAR executable is not found
86
90
  class RarNotAvailableError < Error
87
91
  def initialize(message = default_message)
88
92
  super
@@ -57,7 +57,8 @@ module Omnizip
57
57
  # end
58
58
  # end
59
59
  class Filter
60
- # @return [Symbol] Architecture identifier (:x86, :arm, :arm64, :powerpc, :ia64, :sparc)
60
+ # @return [Symbol, nil] Architecture identifier (:x86, :arm, :arm64,
61
+ # :powerpc, :ia64, :sparc) or nil for architecture-agnostic filters
61
62
  attr_reader :architecture
62
63
 
63
64
  # @return [String] Human-readable filter name
@@ -65,9 +66,10 @@ module Omnizip
65
66
 
66
67
  # Initialize filter
67
68
  #
68
- # @param architecture [Symbol] Target architecture
69
+ # @param architecture [Symbol, nil] Target architecture (optional;
70
+ # filters that don't vary by architecture leave this as nil)
69
71
  # @param name [String] Human-readable name
70
- def initialize(architecture:, name: "Unknown")
72
+ def initialize(architecture: nil, name: "Unknown")
71
73
  @architecture = architecture
72
74
  @name = name
73
75
  end
@@ -1,133 +1,109 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
- # Copyright (C) 2024 Ribose Inc.
5
- #
6
- # This file is part of Omnizip.
7
- #
8
- # Omnizip is a pure Ruby port of 7-Zip compression algorithms.
9
- # Based on the 7-Zip LZMA SDK by Igor Pavlov.
10
- #
11
- # This library is free software; you can redistribute it and/or
12
- # modify it under the terms of the GNU Lesser General Public
13
- # License as published by the Free Software Foundation; either
14
- # version 2.1 of the License, or (at your option) any later version.
15
- #
16
- # See the COPYING file for the complete text of the license.
17
- #
18
-
19
3
  module Omnizip
20
- # Registry for managing filter classes.
21
- #
22
- # This class provides a centralized registry for preprocessing filters,
23
- # allowing filters to self-register and be retrieved by name.
24
- # It implements a plugin-style architecture for extensibility.
25
- class FilterRegistry
26
- @filters = {}
4
+ class FilterRegistry < Omnizip::Registry
5
+ DEFAULT_FORMATS = %i[xz seven_zip].freeze
6
+
7
+ BUILTIN_FILTERS = {
8
+ "bcj-x86": "Omnizip::Filters::BcjX86",
9
+ "bcj-arm": "Omnizip::Filters::BcjArm",
10
+ "bcj-arm64": "Omnizip::Filters::BcjArm64",
11
+ "bcj-ia64": "Omnizip::Filters::BcjIa64",
12
+ "bcj-ppc": "Omnizip::Filters::BcjPpc",
13
+ "bcj-sparc": "Omnizip::Filters::BcjSparc",
14
+ bcj: "Omnizip::Filters::BCJ",
15
+ bcj2: "Omnizip::Filters::Bcj2",
16
+ delta: "Omnizip::Filters::Delta",
17
+ }.freeze
27
18
 
28
19
  class << self
29
- # Register a filter class with format support.
30
- #
31
- # @param name [Symbol, String] The name identifier for the filter
32
- # @param filter_class [Class] The filter class to register
33
- # @param formats [Array<Symbol>] Supported formats (default: [:xz,
34
- # :seven_zip])
35
- # @return [void]
36
- def register(name, filter_class, formats: %i[xz seven_zip])
20
+ def not_found_error_class
21
+ Omnizip::UnknownFilterError
22
+ end
23
+
24
+ def label
25
+ "Filter"
26
+ end
27
+
28
+ def register(name, filter_class, formats: DEFAULT_FORMATS)
37
29
  raise ArgumentError, "Filter name cannot be nil" if name.nil?
38
30
  raise ArgumentError, "Filter class cannot be nil" if filter_class.nil?
39
31
 
40
- @filters[name.to_sym] = {
41
- class: filter_class,
42
- formats: formats,
43
- }
32
+ synchronize do
33
+ storage[normalize_key(name)] = {
34
+ class: filter_class,
35
+ formats: formats,
36
+ }
37
+ end
38
+ filter_class
44
39
  end
45
40
  alias register_with_formats register
46
41
 
47
- # Retrieve a filter class by name.
48
- #
49
- # @param name [Symbol, String] The name identifier for the filter
50
- # @raise [UnknownFilterError] If filter is not registered
51
- # @return [Class] The registered filter class
52
42
  def get(name)
53
- filter = @filters[name.to_sym]
54
- unless filter
55
- raise UnknownFilterError,
56
- "Unknown filter: #{name}. " \
43
+ entry = entry_for(name)
44
+ unless entry
45
+ raise not_found_error_class,
46
+ "#{label} not registered: #{name.inspect}. " \
57
47
  "Available: #{available.join(', ')}"
58
48
  end
59
49
 
60
- filter[:class]
50
+ entry[:class]
61
51
  end
62
52
 
63
- # Check if a filter is registered.
64
- #
65
- # @param name [Symbol, String] The name identifier for the filter
66
- # @return [Boolean] True if filter is registered, false otherwise
67
- def registered?(name)
68
- @filters.key?(name.to_sym)
69
- end
70
-
71
- # Get list of all registered filter names.
72
- #
73
- # @return [Array<Symbol>] Array of registered filter names
74
- def available
75
- @filters.keys
76
- end
77
-
78
- # Reset the registry (primarily for testing).
79
- #
80
- # @return [void]
81
- def reset!
82
- @filters.clear
83
- end
84
-
85
- # Get filter instance for specific format.
86
- #
87
- # Returns a new filter instance after verifying the filter supports
88
- # the specified format.
89
- #
90
- # @param name [Symbol, String] The name identifier for the filter
91
- # @param format [Symbol] Format identifier (:xz, :seven_zip)
92
- # @raise [KeyError] If filter is not registered
93
- # @raise [ArgumentError] If filter doesn't support the format
94
- # @return [Object] New filter instance
95
53
  def get_for_format(name, format)
96
- filter_info = @filters[name.to_sym]
97
- raise KeyError, "Filter not found: #{name}" unless filter_info
54
+ entry = entry_for(name)
55
+ raise KeyError, "Filter not found: #{name}" unless entry
98
56
 
99
- unless filter_info[:formats].include?(format)
57
+ unless entry[:formats].include?(format)
100
58
  raise ArgumentError,
101
59
  "Filter #{name} not supported for format #{format}"
102
60
  end
103
61
 
104
- filter_info[:class].new
62
+ entry[:class].new
105
63
  end
106
64
 
107
- # Check if filter supports specific format.
108
- #
109
- # @param name [Symbol, String] The name identifier for the filter
110
- # @param format [Symbol] Format identifier
111
- # @return [Boolean] True if filter supports the format
112
65
  def supports_format?(name, format)
113
- return false unless @filters[name.to_sym]
66
+ entry = entry_for(name)
67
+ return false unless entry
114
68
 
115
- filter_info = @filters[name.to_sym]
116
- filter_info[:formats]&.include?(format)
69
+ entry[:formats]&.include?(format)
117
70
  end
118
71
 
119
- # Get all filters supporting a specific format.
120
- #
121
- # @param format [Symbol] Format identifier
122
- # @return [Array<Symbol>] Filter names supporting the format
123
72
  def filters_for_format(format)
124
- @filters.select do |_, info|
125
- info[:formats]&.include?(format)
126
- end.keys
73
+ # Ensure all lazy triggers have fired so the format filter list
74
+ # reflects every registered builtin.
75
+ lazy_triggers.keys.dup.each { |name| get(name) }
76
+
77
+ storage.select { |_, info| info[:formats]&.include?(format) }.keys
78
+ end
79
+
80
+ private
81
+
82
+ def entry_for(name)
83
+ normalized = normalize_key(name)
84
+ entry = storage[normalized]
85
+ return entry if entry
86
+
87
+ trigger = lazy_triggers[normalized]
88
+ if trigger
89
+ synchronize { lazy_triggers.delete(normalized) }
90
+ trigger.call
91
+ entry = storage[normalized]
92
+ return entry if entry
93
+ end
94
+
95
+ nil
127
96
  end
128
97
  end
129
98
  end
99
+ end
130
100
 
131
- # Error raised when an unknown filter is requested
132
- class UnknownFilterError < StandardError; end
101
+ # Lazy triggers calling the Filters::Registry.register_all re-runs
102
+ # the registration of every builtin filter. Idempotent: register just
103
+ # overwrites the storage entry. Survives reset! because the trigger
104
+ # proc is held by FilterRegistry, not the storage.
105
+ Omnizip::FilterRegistry::BUILTIN_FILTERS.each_key do |name|
106
+ Omnizip::FilterRegistry.register_lazy(name) do
107
+ Omnizip::Filters::Registry.register_all
108
+ end
133
109
  end
@@ -20,11 +20,11 @@ module Omnizip
20
20
  module Filters
21
21
  # BCJ2 decoder - reconstructs original data from 4 streams.
22
22
  #
23
- # Combines:
24
- # - Main stream (non-convertible bytes)
25
- # - Call stream (CALL/E8 addresses)
26
- # - Jump stream (JUMP/E9 addresses)
27
- # - RC stream (range coder probability data)
23
+ # Matches the modern 7-Zip SDK Bcj2.c decode loop (v18.06+):
24
+ # - Pre-allocated output buffer (no dynamic growth)
25
+ # - Handles E8 (CALL), E9 (JMP), and JCC (0x0F 0x80-0x8F) opcodes
26
+ # - Probability model: index 0=JCC, 1=E9, 2+prev_byte=E8
27
+ # - Inline range decoder with cached locals
28
28
  class Bcj2Decoder
29
29
  include Bcj2Constants
30
30
 
@@ -50,147 +50,213 @@ module Omnizip
50
50
 
51
51
  # Decode 4 streams back to original data.
52
52
  #
53
+ # Matches modern 7-Zip SDK Bcj2Dec_Decode pattern:
54
+ # - Pre-allocated output buffer sized to expected output
55
+ # - Single C-optimized regex scan for E8/E9/JCC opcodes
56
+ # - Direct setbyte writes (no intermediate String allocations)
57
+ # - Opcode written to output before range-coded decision (matches SDK)
58
+ #
59
+ # @param expected_size [Integer, nil] Expected output size for pre-allocation
53
60
  # @return [String] Decoded binary data
54
- def decode
55
- result = String.new(encoding: Encoding::BINARY)
56
- init_range_decoder
57
-
58
- loop do
59
- break if @main_pos >= @streams.main.bytesize
60
-
61
- byte = @streams.main.getbyte(@main_pos)
62
- @main_pos += 1
63
-
64
- # Check for CALL (E8) or JUMP (E9) opcodes
65
- if [OPCODE_CALL, OPCODE_JUMP].include?(byte)
66
- # Use range decoder to determine if convertible
67
- if read_bit(get_prob_index(byte))
68
- # Convertible - read address from call/jump stream
69
- addr = read_address(byte)
70
- result << byte.chr(Encoding::BINARY)
71
- result << encode_int32_le(addr)
72
- @ip += 5
73
- else
74
- # Not convertible - just copy byte
75
- result << byte.chr(Encoding::BINARY)
76
- @ip += 1
77
- end
78
- else
79
- # Regular byte - just copy
80
- result << byte.chr(Encoding::BINARY)
81
- @ip += 1
82
- end
83
- end
61
+ # @raise [Omnizip::DecompressionError] If output exceeds buffer capacity when expected_size is nil
62
+ def decode(expected_size = nil)
63
+ main_data = @streams.main
64
+ main_size = main_data.bytesize
84
65
 
85
- result
86
- end
66
+ # Pre-allocate output buffer to expected size (C++ SDK: dic is pre-allocated)
67
+ # When expected_size is unknown, use a conservative upper bound
68
+ # Each byte can produce at most 5 bytes (opcode + 4-byte address), but
69
+ # in practice BCJ2 never expands more than 2x due to rarity of convertible opcodes
70
+ out_capacity = expected_size || (main_size * 2)
71
+ result = ("\0" * out_capacity).b
72
+ result_pos = 0
87
73
 
88
- private
74
+ # Helper to grow buffer if needed (only when expected_size is nil)
75
+ grow_buffer = lambda do |required|
76
+ return if result_pos + required <= result.bytesize
89
77
 
90
- # Initialize range decoder by reading first 5 bytes from RC stream.
91
- #
92
- # @return [void]
93
- def init_range_decoder
94
- @range = 0xFFFFFFFF
95
- @code = 0
78
+ if expected_size
79
+ raise Omnizip::DecompressionError,
80
+ "BCJ2 output size (#{result_pos + required}) exceeds expected size (#{expected_size})"
81
+ end
82
+
83
+ # Dynamic growth when expected_size is unknown
84
+ new_capacity = [result.bytesize * 2, result_pos + required].max
85
+ new_buf = ("\0" * new_capacity).b
86
+ new_buf[0, result_pos] = result.byteslice(0, result_pos)
87
+ result = new_buf
88
+ end
89
+
90
+ # Cache stream references as locals (avoids repeated ivar lookups in hot loop)
91
+ call_data = @streams.call
92
+ jump_data = @streams.jump
93
+ rc_data = @streams.rc
94
+ call_size = call_data.bytesize
95
+ jump_size = jump_data.bytesize
96
+ rc_size = rc_data.bytesize
97
+ call_pos = @call_pos
98
+ jump_pos = @jump_pos
99
+ rc_pos = @rc_pos
100
+ probs = @probs
101
+ ip = @ip
96
102
 
103
+ # Initialize range decoder (read first 5 bytes from RC stream)
104
+ range = 0xFFFFFFFF
105
+ code = 0
97
106
  5.times do
98
- break if @rc_pos >= @streams.rc.bytesize
107
+ break if rc_pos >= rc_size
99
108
 
100
- @code = (@code << 8) | @streams.rc.getbyte(@rc_pos)
101
- @rc_pos += 1
109
+ code = ((code << 8) | rc_data.getbyte(rc_pos)) & 0xFFFFFFFF
110
+ rc_pos += 1
102
111
  end
103
- end
104
112
 
105
- # Read a single bit from range coder.
106
- #
107
- # @param prob_index [Integer] Probability model index
108
- # @return [Boolean] Decoded bit (true = 1, false = 0)
109
- def read_bit(prob_index) # rubocop:disable Naming/PredicateMethod
110
- normalize_range
111
-
112
- prob = @probs[prob_index]
113
- bound = (@range >> BIT_MODEL_TOTAL_BITS) * prob
114
-
115
- if @code < bound
116
- # Bit is 0
117
- @range = bound
118
- @probs[prob_index] += (BIT_MODEL_TOTAL - prob) >> MOVE_BITS
119
- false
120
- else
121
- # Bit is 1
122
- @range -= bound
123
- @code -= bound
124
- @probs[prob_index] -= prob >> MOVE_BITS
125
- true
126
- end
127
- end
113
+ # Regex to find E8, E9, or JCC (0x0F followed by 0x80-0x8F)
114
+ opcode_re = /[\xe8\xe9]|\x0f[\x80-\x8f]/n
115
+ main_pos = @main_pos
116
+ prev_byte = 0
128
117
 
129
- # Normalize range decoder if needed.
130
- #
131
- # @return [void]
132
- def normalize_range
133
- while @range < TOP_VALUE
134
- @range <<= 8
135
- next_byte = if @rc_pos < @streams.rc.bytesize
136
- @streams.rc.getbyte(@rc_pos)
137
- else
138
- 0
139
- end
140
- @code = (@code << 8) | next_byte
141
- @rc_pos += 1 if @rc_pos < @streams.rc.bytesize
142
- end
143
- end
118
+ while main_pos < main_size
119
+ # C-optimized scan for E8, E9, and JCC opcodes
120
+ next_pos = main_data.index(opcode_re, main_pos)
144
121
 
145
- # Get probability model index for a byte.
146
- #
147
- # @param byte [Integer] Byte value
148
- # @return [Integer] Probability model index
149
- def get_prob_index(byte)
150
- # Use byte-specific model for E8, general model for E9
151
- byte == OPCODE_CALL ? (2 + (@ip & 0xFF)) : 0
152
- end
122
+ unless next_pos
123
+ # No more opcodes — bulk copy remaining bytes
124
+ chunk_len = main_size - main_pos
125
+ grow_buffer.call(chunk_len)
126
+ result[result_pos, chunk_len] =
127
+ main_data.byteslice(main_pos, chunk_len)
128
+ result_pos += chunk_len
129
+ ip += chunk_len
130
+ main_pos = main_size
131
+ break
132
+ end
153
133
 
154
- # Read 32-bit address from call or jump stream.
155
- #
156
- # @param opcode [Integer] Opcode (E8 or E9)
157
- # @return [Integer] Converted address
158
- def read_address(opcode)
159
- stream_pos = opcode == OPCODE_CALL ? @call_pos : @jump_pos
160
- stream = opcode == OPCODE_CALL ? @streams.call : @streams.jump
161
-
162
- # Read 4 bytes (big-endian in stream, stored as absolute)
163
- addr = 0
164
- 4.times do |i|
165
- break if stream_pos >= stream.bytesize
166
-
167
- addr |= stream.getbyte(stream_pos) << (24 - (i * 8))
168
- stream_pos += 1
169
- end
134
+ # Bulk copy bytes before opcode
135
+ if next_pos > main_pos
136
+ chunk_len = next_pos - main_pos
137
+ grow_buffer.call(chunk_len)
138
+ result[result_pos, chunk_len] =
139
+ main_data.byteslice(main_pos, chunk_len)
140
+ result_pos += chunk_len
141
+ ip += chunk_len
142
+ main_pos = next_pos
143
+ # Track prev_byte from bulk copy (last byte before opcode in output)
144
+ prev_byte = main_data.getbyte(next_pos - 1)
145
+ end
146
+
147
+ byte = main_data.getbyte(main_pos)
148
+
149
+ if byte == 0x0F
150
+ # JCC: 0x0F followed by 0x80-0x8F conditional jump
151
+ # Ensure buffer has room for 2 opcode bytes + potential 4 address bytes
152
+ grow_buffer.call(6)
153
+ # Write 0x0F to output (SDK writes opcode bytes before range decision)
154
+ result.setbyte(result_pos, 0x0F)
155
+ result_pos += 1
156
+ ip += 1
157
+ main_pos += 1
158
+
159
+ # Read and write the 0x8x second opcode byte
160
+ byte = main_data.getbyte(main_pos)
161
+ result.setbyte(result_pos, byte)
162
+ result_pos += 1
163
+ ip += 1
164
+ main_pos += 1
165
+
166
+ prob_idx = 0 # JCC uses probability index 0
167
+ is_call = false
168
+ else
169
+ # E8 (CALL) or E9 (JMP) — write opcode before range decision
170
+ # Ensure buffer has room for 1 opcode byte + potential 4 address bytes
171
+ grow_buffer.call(5)
172
+ result.setbyte(result_pos, byte)
173
+ result_pos += 1
174
+ ip += 1
175
+ main_pos += 1
176
+
177
+ if byte == 0xE8
178
+ prob_idx = 2 + prev_byte # E8 uses prev_byte context (256 models)
179
+ is_call = true
180
+ else
181
+ prob_idx = 1 # E9 uses fixed index 1
182
+ is_call = false
183
+ end
184
+ end
185
+
186
+ # Inline range decoder normalization
187
+ if range < TOP_VALUE
188
+ range = (range << 8) & 0xFFFFFFFF
189
+ rc_byte = rc_pos < rc_size ? rc_data.getbyte(rc_pos) : 0
190
+ code = ((code << 8) | rc_byte) & 0xFFFFFFFF
191
+ rc_pos += 1 if rc_pos < rc_size
192
+ end
170
193
 
171
- # Update stream position
172
- if opcode == OPCODE_CALL
173
- @call_pos = stream_pos
174
- else
175
- @jump_pos = stream_pos
194
+ prob = probs[prob_idx]
195
+ bound = (range >> BIT_MODEL_TOTAL_BITS) * prob
196
+
197
+ if code < bound
198
+ # Bit = 0: not convertible (opcode already written to output)
199
+ range = bound
200
+ probs[prob_idx] = prob + ((BIT_MODEL_TOTAL - prob) >> MOVE_BITS)
201
+ prev_byte = byte
202
+ else
203
+ # Bit = 1: convertible — read 4-byte address from call/jump stream
204
+ range -= bound
205
+ code -= bound
206
+ probs[prob_idx] = prob - (prob >> MOVE_BITS)
207
+
208
+ # Read 4 bytes big-endian from call stream (E8) or jump stream (E9/JCC)
209
+ if is_call
210
+ if call_pos + 4 <= call_size
211
+ addr = call_data.byteslice(call_pos, 4).unpack1("N")
212
+ call_pos += 4
213
+ else
214
+ addr = 0
215
+ 4.times do |i|
216
+ break if call_pos >= call_size
217
+
218
+ addr |= call_data.getbyte(call_pos) << (24 - (i * 8))
219
+ call_pos += 1
220
+ end
221
+ end
222
+ elsif jump_pos + 4 <= jump_size
223
+ addr = jump_data.byteslice(jump_pos, 4).unpack1("N")
224
+ jump_pos += 4
225
+ else
226
+ addr = 0
227
+ 4.times do |i|
228
+ break if jump_pos >= jump_size
229
+
230
+ addr |= jump_data.getbyte(jump_pos) << (24 - (i * 8))
231
+ jump_pos += 1
232
+ end
233
+ end
234
+
235
+ # Convert absolute address to relative (ip already past opcode bytes)
236
+ addr = (addr - (ip + 4)) & 0xFFFFFFFF
237
+
238
+ # Write 4-byte little-endian address directly (no pack/Array alloc)
239
+ result.setbyte(result_pos, addr & 0xFF)
240
+ result.setbyte(result_pos + 1, (addr >> 8) & 0xFF)
241
+ result.setbyte(result_pos + 2, (addr >> 16) & 0xFF)
242
+ result.setbyte(result_pos + 3, (addr >> 24) & 0xFF)
243
+ result_pos += 4
244
+ ip += 4
245
+ prev_byte = (addr >> 24) & 0xFF
246
+ end
176
247
  end
177
248
 
178
- # Convert back to relative
179
- addr - (@ip + 5)
180
- end
249
+ # Store final positions back
250
+ @main_pos = main_pos
251
+ @call_pos = call_pos
252
+ @jump_pos = jump_pos
253
+ @rc_pos = rc_pos
254
+ @range = range
255
+ @code = code
256
+ @ip = ip
181
257
 
182
- # Encode 32-bit integer as little-endian bytes.
183
- #
184
- # @param value [Integer] Value to encode
185
- # @return [String] 4-byte string
186
- def encode_int32_le(value)
187
- unsigned = value & 0xFFFFFFFF
188
- [
189
- unsigned & 0xFF,
190
- (unsigned >> 8) & 0xFF,
191
- (unsigned >> 16) & 0xFF,
192
- (unsigned >> 24) & 0xFF,
193
- ].pack("C*")
258
+ # Return exact output (trim if pre-allocated too large)
259
+ result_pos < result.bytesize ? result.byteslice(0, result_pos) : result
194
260
  end
195
261
  end
196
262
  end