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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +51 -50
- data/Gemfile +2 -0
- data/Rakefile +3 -0
- data/TODO.refactor/00-overview.md +67 -0
- data/TODO.refactor/01-registry-base-class.md +81 -0
- data/TODO.refactor/02-error-hierarchy.md +57 -0
- data/TODO.refactor/03-autoload-entry-point.md +69 -0
- data/TODO.refactor/04-filter-base-consolidation.md +49 -0
- data/TODO.refactor/05-send-private-methods.md +64 -0
- data/TODO.refactor/06-instance-variable-access.md +69 -0
- data/TODO.refactor/07-respond-to-replacement.md +91 -0
- data/TODO.refactor/08-spec-doubles.md +27 -0
- data/TODO.refactor/09-cli-shared-module.md +35 -0
- data/TODO.refactor/10-format-detector-ocp.md +34 -0
- data/TODO.refactor/11-convenience-decoupling.md +38 -0
- data/TODO.refactor/12-thread-safety.md +8 -0
- data/TODO.refactor/13-lutaml-model-migration.md +82 -0
- data/TODO.refactor/14-add-missing-specs.md +25 -0
- data/lib/omnizip/algorithm.rb +0 -2
- data/lib/omnizip/algorithm_registry.rb +29 -57
- data/lib/omnizip/algorithms/lzma/bit_model.rb +1 -1
- data/lib/omnizip/algorithms/lzma/dictionary.rb +4 -3
- data/lib/omnizip/algorithms/lzma/distance_coder.rb +5 -102
- data/lib/omnizip/algorithms/lzma/length_coder.rb +4 -92
- data/lib/omnizip/algorithms/lzma/literal_decoder.rb +4 -119
- data/lib/omnizip/algorithms/lzma/lzip_decoder.rb +2 -0
- data/lib/omnizip/algorithms/lzma/match_finder.rb +59 -29
- data/lib/omnizip/algorithms/lzma/optimal_encoder.rb +20 -12
- data/lib/omnizip/algorithms/lzma/range_decoder.rb +77 -269
- data/lib/omnizip/algorithms/lzma/range_encoder.rb +0 -9
- data/lib/omnizip/algorithms/lzma/xz_range_encoder_exact.rb +0 -15
- data/lib/omnizip/algorithms/lzma/xz_state.rb +0 -8
- data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +189 -968
- data/lib/omnizip/algorithms/lzma2/encoder.rb +0 -2
- data/lib/omnizip/algorithms/lzma2/lzma2_chunk.rb +0 -2
- data/lib/omnizip/algorithms/lzma2/properties.rb +0 -2
- data/lib/omnizip/algorithms/ppmd7/model.rb +1 -1
- data/lib/omnizip/algorithms/ppmd8/context.rb +0 -2
- data/lib/omnizip/algorithms/ppmd8/model.rb +1 -1
- data/lib/omnizip/algorithms/ppmd8.rb +0 -2
- data/lib/omnizip/algorithms/ppmd_base.rb +0 -2
- data/lib/omnizip/algorithms/sevenzip_lzma2.rb +0 -5
- data/lib/omnizip/algorithms/xz_lzma2.rb +0 -5
- data/lib/omnizip/algorithms/zstandard/frame/header.rb +3 -5
- data/lib/omnizip/algorithms.rb +1 -0
- data/lib/omnizip/archive_handler.rb +75 -0
- data/lib/omnizip/archive_handlers/tar_handler.rb +51 -0
- data/lib/omnizip/archive_handlers/zip_handler.rb +73 -0
- data/lib/omnizip/archive_handlers.rb +11 -0
- data/lib/omnizip/checksum_registry.rb +23 -96
- data/lib/omnizip/cli/shared.rb +31 -0
- data/lib/omnizip/cli.rb +6 -43
- data/lib/omnizip/commands/archive_create_command.rb +0 -6
- data/lib/omnizip/commands/archive_extract_command.rb +0 -4
- data/lib/omnizip/commands/archive_list_command.rb +0 -4
- data/lib/omnizip/commands/archive_repair_command.rb +0 -2
- data/lib/omnizip/commands/archive_verify_command.rb +0 -2
- data/lib/omnizip/commands/compress_command.rb +0 -2
- data/lib/omnizip/commands/decompress_command.rb +0 -2
- data/lib/omnizip/commands/list_command.rb +0 -2
- data/lib/omnizip/commands/metadata_command.rb +0 -5
- data/lib/omnizip/commands/parity_create_command.rb +0 -2
- data/lib/omnizip/commands/parity_repair_command.rb +0 -2
- data/lib/omnizip/commands/parity_verify_command.rb +0 -2
- data/lib/omnizip/commands/profile_list_command.rb +0 -2
- data/lib/omnizip/commands/profile_show_command.rb +0 -2
- data/lib/omnizip/convenience.rb +111 -234
- data/lib/omnizip/converter/seven_zip_to_zip_strategy.rb +1 -1
- data/lib/omnizip/crypto/aes256/cipher.rb +6 -1
- data/lib/omnizip/error.rb +22 -18
- data/lib/omnizip/filter.rb +5 -3
- data/lib/omnizip/filter_registry.rb +74 -98
- data/lib/omnizip/filters/bcj2/decoder.rb +196 -130
- data/lib/omnizip/filters/bcj2.rb +0 -2
- data/lib/omnizip/filters/bcj_x86.rb +0 -2
- data/lib/omnizip/filters/filter_base.rb +4 -70
- data/lib/omnizip/filters/registry.rb +0 -2
- data/lib/omnizip/format_detector.rb +21 -12
- data/lib/omnizip/format_registry.rb +79 -38
- data/lib/omnizip/formats/bzip2_file.rb +0 -2
- data/lib/omnizip/formats/cpio/entry.rb +0 -2
- data/lib/omnizip/formats/cpio/reader.rb +0 -1
- data/lib/omnizip/formats/cpio/writer.rb +0 -1
- data/lib/omnizip/formats/cpio.rb +0 -1
- data/lib/omnizip/formats/format_spec_loader.rb +1 -1
- data/lib/omnizip/formats/gzip.rb +0 -2
- data/lib/omnizip/formats/iso/directory_record.rb +6 -3
- data/lib/omnizip/formats/iso/reader.rb +2 -2
- data/lib/omnizip/formats/iso.rb +0 -1
- data/lib/omnizip/formats/lzip.rb +5 -25
- data/lib/omnizip/formats/lzma_alone.rb +7 -25
- data/lib/omnizip/formats/msi/cab_extractor.rb +181 -0
- data/lib/omnizip/formats/msi/constants.rb +157 -0
- data/lib/omnizip/formats/msi/directory_resolver.rb +188 -0
- data/lib/omnizip/formats/msi/entry.rb +128 -0
- data/lib/omnizip/formats/msi/reader.rb +301 -0
- data/lib/omnizip/formats/msi/string_pool.rb +246 -0
- data/lib/omnizip/formats/msi/table_parser.rb +244 -0
- data/lib/omnizip/formats/msi.rb +84 -0
- data/lib/omnizip/formats/ole/allocation_table.rb +10 -1
- data/lib/omnizip/formats/ole/dirent.rb +20 -3
- data/lib/omnizip/formats/ole/header.rb +0 -1
- data/lib/omnizip/formats/ole/ranges_io.rb +21 -3
- data/lib/omnizip/formats/ole/storage.rb +19 -4
- data/lib/omnizip/formats/ole.rb +0 -2
- data/lib/omnizip/formats/rar/block_parser.rb +0 -1
- data/lib/omnizip/formats/rar/compression/ppmd/encoder.rb +1 -1
- data/lib/omnizip/formats/rar/decompressor.rb +38 -1
- data/lib/omnizip/formats/rar/header.rb +0 -1
- data/lib/omnizip/formats/rar/rar5/writer.rb +0 -2
- data/lib/omnizip/formats/rar/reader.rb +2 -3
- data/lib/omnizip/formats/rar/writer.rb +0 -1
- data/lib/omnizip/formats/rar.rb +2 -2
- data/lib/omnizip/formats/rpm/entry.rb +0 -1
- data/lib/omnizip/formats/rpm/header.rb +3 -8
- data/lib/omnizip/formats/rpm/lead.rb +37 -26
- data/lib/omnizip/formats/rpm/tag.rb +0 -1
- data/lib/omnizip/formats/rpm/writer.rb +0 -1
- data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +98 -61
- data/lib/omnizip/formats/seven_zip/coder_chain.rb +1 -2
- data/lib/omnizip/formats/seven_zip/encoded_header.rb +0 -1
- data/lib/omnizip/formats/seven_zip/encrypted_header.rb +0 -1
- data/lib/omnizip/formats/seven_zip/header.rb +0 -1
- data/lib/omnizip/formats/seven_zip/header_encryptor.rb +0 -1
- data/lib/omnizip/formats/seven_zip/header_writer.rb +22 -61
- data/lib/omnizip/formats/seven_zip/models/file_entry.rb +1 -1
- data/lib/omnizip/formats/seven_zip/parser.rb +14 -13
- data/lib/omnizip/formats/seven_zip/reader.rb +82 -19
- data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +0 -15
- data/lib/omnizip/formats/seven_zip/split_archive_writer.rb +0 -1
- data/lib/omnizip/formats/seven_zip/writer.rb +22 -68
- data/lib/omnizip/formats/seven_zip.rb +63 -5
- data/lib/omnizip/formats/tar/entry.rb +0 -2
- data/lib/omnizip/formats/tar/header.rb +0 -2
- data/lib/omnizip/formats/tar/reader.rb +0 -2
- data/lib/omnizip/formats/tar/writer.rb +0 -2
- data/lib/omnizip/formats/tar.rb +0 -1
- data/lib/omnizip/formats/xar/entry.rb +0 -1
- data/lib/omnizip/formats/xar/header.rb +0 -1
- data/lib/omnizip/formats/xar/reader.rb +0 -1
- data/lib/omnizip/formats/xar/toc.rb +3 -3
- data/lib/omnizip/formats/xar/writer.rb +11 -3
- data/lib/omnizip/formats/xar.rb +0 -1
- data/lib/omnizip/formats/xz/reader.rb +7 -3
- data/lib/omnizip/formats/xz.rb +3 -40
- data/lib/omnizip/formats/xz_impl/block_decoder.rb +4 -10
- data/lib/omnizip/formats/xz_impl/block_encoder.rb +0 -2
- data/lib/omnizip/formats/xz_impl/block_header_parser.rb +0 -2
- data/lib/omnizip/formats/xz_impl/index_decoder.rb +0 -1
- data/lib/omnizip/formats/xz_impl/index_encoder.rb +0 -1
- data/lib/omnizip/formats/xz_impl/stream_encoder.rb +1 -1
- data/lib/omnizip/formats/xz_impl/stream_footer.rb +0 -1
- data/lib/omnizip/formats/xz_impl/stream_footer_parser.rb +0 -1
- data/lib/omnizip/formats/xz_impl/stream_header.rb +0 -1
- data/lib/omnizip/formats/xz_impl/stream_header_parser.rb +0 -2
- data/lib/omnizip/formats/zip/central_directory_header.rb +2 -3
- data/lib/omnizip/formats/zip/end_of_central_directory.rb +0 -2
- data/lib/omnizip/formats/zip/local_file_header.rb +0 -2
- data/lib/omnizip/formats/zip/reader.rb +12 -2
- data/lib/omnizip/formats/zip/writer.rb +73 -1
- data/lib/omnizip/formats/zip/zip64_end_of_central_directory.rb +0 -2
- data/lib/omnizip/formats/zip/zip64_end_of_central_directory_locator.rb +0 -2
- data/lib/omnizip/formats/zip/zip64_extra_field.rb +0 -2
- data/lib/omnizip/formats/zip.rb +0 -1
- data/lib/omnizip/formats.rb +9 -22
- data/lib/omnizip/implementations/seven_zip/lzma/range_decoder.rb +2 -2
- data/lib/omnizip/implementations/seven_zip/lzma/range_encoder.rb +1 -1
- data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +0 -6
- data/lib/omnizip/implementations/xz_utils/lzma2/decoder.rb +18 -185
- data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +3 -3
- data/lib/omnizip/io/source.rb +136 -0
- data/lib/omnizip/io.rb +2 -0
- data/lib/omnizip/link_handler.rb +7 -3
- data/lib/omnizip/metadata/metadata_editor.rb +1 -1
- data/lib/omnizip/models/algorithm_metadata.rb +33 -60
- data/lib/omnizip/models/compression_options.rb +40 -53
- data/lib/omnizip/models/conversion_options.rb +9 -18
- data/lib/omnizip/models/parallel_options.rb +25 -0
- data/lib/omnizip/optimization_registry.rb +8 -44
- data/lib/omnizip/parallel/job_scheduler.rb +26 -8
- data/lib/omnizip/parallel/parallel_compressor.rb +10 -20
- data/lib/omnizip/parallel/parallel_extractor.rb +20 -11
- data/lib/omnizip/parity/par2_repairer.rb +5 -5
- data/lib/omnizip/parity/par2_verifier.rb +27 -22
- data/lib/omnizip/parity.rb +4 -4
- data/lib/omnizip/password/encryption_registry.rb +11 -50
- data/lib/omnizip/password/zip_crypto_strategy.rb +1 -4
- data/lib/omnizip/pipe/stream_decompressor.rb +2 -3
- data/lib/omnizip/profile/archive_profile.rb +0 -2
- data/lib/omnizip/profile/binary_profile.rb +0 -2
- data/lib/omnizip/profile/compression_profile.rb +0 -1
- data/lib/omnizip/profile/profile_detector.rb +0 -2
- data/lib/omnizip/profile/text_profile.rb +0 -2
- data/lib/omnizip/profile.rb +0 -2
- data/lib/omnizip/registry.rb +112 -0
- data/lib/omnizip/version.rb +1 -1
- data/lib/omnizip/zip/file.rb +20 -9
- data/lib/omnizip.rb +26 -55
- data/lib/tasks/msi_test_files.rake +66 -0
- 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
|
-
#
|
|
23
|
-
|
|
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
|
-
#
|
|
39
|
-
|
|
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
|
-
#
|
|
47
|
-
class
|
|
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
|
data/lib/omnizip/filter.rb
CHANGED
|
@@ -57,7 +57,8 @@ module Omnizip
|
|
|
57
57
|
# end
|
|
58
58
|
# end
|
|
59
59
|
class Filter
|
|
60
|
-
# @return [Symbol] Architecture identifier (:x86, :arm, :arm64,
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
54
|
-
unless
|
|
55
|
-
raise
|
|
56
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
raise KeyError, "Filter not found: #{name}" unless
|
|
54
|
+
entry = entry_for(name)
|
|
55
|
+
raise KeyError, "Filter not found: #{name}" unless entry
|
|
98
56
|
|
|
99
|
-
unless
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
entry = entry_for(name)
|
|
67
|
+
return false unless entry
|
|
114
68
|
|
|
115
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
#
|
|
24
|
-
# -
|
|
25
|
-
# -
|
|
26
|
-
# -
|
|
27
|
-
# -
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
107
|
+
break if rc_pos >= rc_size
|
|
99
108
|
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
code = ((code << 8) | rc_data.getbyte(rc_pos)) & 0xFFFFFFFF
|
|
110
|
+
rc_pos += 1
|
|
102
111
|
end
|
|
103
|
-
end
|
|
104
112
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
#
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|