omnizip 0.4.7-x86_64-linux
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +32 -0
- data/.rubocop_todo.yml +293 -0
- data/CHANGELOG.md +1647 -0
- data/CONTEXT.md +68 -0
- data/COPYING +502 -0
- data/Gemfile +19 -0
- data/LICENSE +12 -0
- data/README.adoc +1327 -0
- data/Rakefile +15 -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 +237 -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 +158 -0
- data/TODO.refactor/14-add-missing-specs.md +25 -0
- data/benchmark/README.md +260 -0
- data/benchmark/benchmark_suite.rb +125 -0
- data/benchmark/compression_bench.rb +181 -0
- data/benchmark/filter_bench.rb +180 -0
- data/benchmark/models/benchmark_result.rb +59 -0
- data/benchmark/models/comparison_result.rb +69 -0
- data/benchmark/profile_suite.rb +167 -0
- data/benchmark/reporter.rb +150 -0
- data/benchmark/run_benchmarks.rb +66 -0
- data/benchmark/test_data.rb +137 -0
- data/config/formats/rar3_spec.yml +92 -0
- data/config/formats/rar5_spec.yml +102 -0
- data/docs/.github/workflows/docs.yml +142 -0
- data/docs/.gitignore +21 -0
- data/docs/.lychee.toml +67 -0
- data/docs/Gemfile +13 -0
- data/docs/RAR_WRITE_SUPPORT.md +26 -0
- data/docs/README.md +101 -0
- data/docs/_config.yml +112 -0
- data/docs/assets/logo.svg +1 -0
- data/docs/assets/omnizip-logo.pdf +1540 -11
- data/docs/comparison/feature-matrix.adoc +694 -0
- data/docs/comparison/index.adoc +113 -0
- data/docs/comparison/vs-7zip.adoc +309 -0
- data/docs/comparison/vs-peazip.adoc +77 -0
- data/docs/comparison/vs-rubyzip.adoc +342 -0
- data/docs/comparison/vs-winrar.adoc +100 -0
- data/docs/compatibility.adoc +579 -0
- data/docs/concepts/index.adoc +129 -0
- data/docs/developer/architecture.adoc +256 -0
- data/docs/developer/contributing.adoc +158 -0
- data/docs/developer/index.adoc +25 -0
- data/docs/developer/testing.adoc +212 -0
- data/docs/getting-started/basic-usage.adoc +263 -0
- data/docs/getting-started/index.adoc +42 -0
- data/docs/getting-started/installation.adoc +138 -0
- data/docs/getting-started/quick-start.adoc +183 -0
- data/docs/getting-started/your-first-archive.adoc +219 -0
- data/docs/guides/advanced-features/encryption.adoc +277 -0
- data/docs/guides/advanced-features/index.adoc +59 -0
- data/docs/guides/advanced-features/parallel-processing.adoc +144 -0
- data/docs/guides/advanced-features/progress-tracking.adoc +124 -0
- data/docs/guides/advanced-features/streaming.adoc +142 -0
- data/docs/guides/archive-formats/gzip-format.adoc +105 -0
- data/docs/guides/archive-formats/index.adoc +160 -0
- data/docs/guides/archive-formats/ole-format.adoc +151 -0
- data/docs/guides/archive-formats/rar-format.adoc +104 -0
- data/docs/guides/archive-formats/rar5.adoc +521 -0
- data/docs/guides/archive-formats/rpm-format.adoc +108 -0
- data/docs/guides/archive-formats/seven-zip-format.adoc +35 -0
- data/docs/guides/archive-formats/tar-format.adoc +106 -0
- data/docs/guides/archive-formats/xz-format.adoc +118 -0
- data/docs/guides/archive-formats/zip-format.adoc +35 -0
- data/docs/guides/compression-algorithms/bzip2.adoc +114 -0
- data/docs/guides/compression-algorithms/deflate.adoc +313 -0
- data/docs/guides/compression-algorithms/index.adoc +190 -0
- data/docs/guides/compression-algorithms/lzma.adoc +394 -0
- data/docs/guides/compression-algorithms/lzma2.adoc +244 -0
- data/docs/guides/compression-algorithms/ppmd.adoc +313 -0
- data/docs/guides/compression-algorithms/zstandard.adoc +296 -0
- data/docs/guides/creating-archives.adoc +348 -0
- data/docs/guides/extracting-archives.adoc +53 -0
- data/docs/guides/format-conversion.adoc +64 -0
- data/docs/guides/index.adoc +49 -0
- data/docs/guides/migration-rubyzip.adoc +217 -0
- data/docs/guides/parity-archives.adoc +605 -0
- data/docs/guides/performance-tuning.adoc +88 -0
- data/docs/index.adoc +228 -0
- data/docs/lychee.toml +67 -0
- data/docs/reference/api/overview.adoc +187 -0
- data/docs/reference/cli/compress-command.adoc +114 -0
- data/docs/reference/cli/overview.adoc +140 -0
- data/docs/reference/index.adoc +26 -0
- data/docs/resources/faq.adoc +185 -0
- data/docs/resources/quick-reference.adoc +222 -0
- data/docs/rust-backend.md +0 -0
- data/docs/troubleshooting/index.adoc +207 -0
- data/docs/xar_format.md +216 -0
- data/examples/api_comparison.rb +205 -0
- data/examples/deflate64_example.rb +96 -0
- data/examples/par2_demo.rb +121 -0
- data/examples/quick_start_native.rb +150 -0
- data/examples/quick_start_rubyzip.rb +115 -0
- data/examples/rubyzip_compatibility_demo.rb +194 -0
- data/exe/omnizip +27 -0
- data/lib/omnizip/algorithm.rb +176 -0
- data/lib/omnizip/algorithm_registry.rb +65 -0
- data/lib/omnizip/algorithms/.keep +0 -0
- data/lib/omnizip/algorithms/bzip2/bwt.rb +236 -0
- data/lib/omnizip/algorithms/bzip2/bz2.rb +687 -0
- data/lib/omnizip/algorithms/bzip2/rle.rb +146 -0
- data/lib/omnizip/algorithms/bzip2.rb +108 -0
- data/lib/omnizip/algorithms/deflate/constants.rb +28 -0
- data/lib/omnizip/algorithms/deflate/decoder.rb +37 -0
- data/lib/omnizip/algorithms/deflate/encoder.rb +45 -0
- data/lib/omnizip/algorithms/deflate.rb +135 -0
- data/lib/omnizip/algorithms/deflate64/constants.rb +45 -0
- data/lib/omnizip/algorithms/deflate64/decoder.rb +151 -0
- data/lib/omnizip/algorithms/deflate64/encoder.rb +95 -0
- data/lib/omnizip/algorithms/deflate64/huffman_coder.rb +352 -0
- data/lib/omnizip/algorithms/deflate64/lz77_encoder.rb +140 -0
- data/lib/omnizip/algorithms/deflate64.rb +118 -0
- data/lib/omnizip/algorithms/lzma/bit_model.rb +120 -0
- data/lib/omnizip/algorithms/lzma/constants.rb +112 -0
- data/lib/omnizip/algorithms/lzma/decoder.rb +146 -0
- data/lib/omnizip/algorithms/lzma/dictionary.rb +70 -0
- data/lib/omnizip/algorithms/lzma/distance_coder.rb +326 -0
- data/lib/omnizip/algorithms/lzma/encoder.rb +139 -0
- data/lib/omnizip/algorithms/lzma/length_coder.rb +172 -0
- data/lib/omnizip/algorithms/lzma/literal_decoder.rb +204 -0
- data/lib/omnizip/algorithms/lzma/literal_encoder.rb +208 -0
- data/lib/omnizip/algorithms/lzma/lzip_decoder.rb +368 -0
- data/lib/omnizip/algorithms/lzma/lzma1_encoder.rb +105 -0
- data/lib/omnizip/algorithms/lzma/lzma_alone_decoder.rb +194 -0
- data/lib/omnizip/algorithms/lzma/lzma_state.rb +128 -0
- data/lib/omnizip/algorithms/lzma/match.rb +32 -0
- data/lib/omnizip/algorithms/lzma/match_finder.rb +233 -0
- data/lib/omnizip/algorithms/lzma/match_finder_config.rb +142 -0
- data/lib/omnizip/algorithms/lzma/match_finder_factory.rb +84 -0
- data/lib/omnizip/algorithms/lzma/optimal_encoder.rb +159 -0
- data/lib/omnizip/algorithms/lzma/probability_models.rb +72 -0
- data/lib/omnizip/algorithms/lzma/range_coder.rb +82 -0
- data/lib/omnizip/algorithms/lzma/range_decoder.rb +274 -0
- data/lib/omnizip/algorithms/lzma/range_encoder.rb +202 -0
- data/lib/omnizip/algorithms/lzma/state.rb +127 -0
- data/lib/omnizip/algorithms/lzma/xz_buffered_range_encoder.rb +323 -0
- data/lib/omnizip/algorithms/lzma/xz_encoder.rb +420 -0
- data/lib/omnizip/algorithms/lzma/xz_encoder_fast.rb +640 -0
- data/lib/omnizip/algorithms/lzma/xz_match_finder_adapter.rb +224 -0
- data/lib/omnizip/algorithms/lzma/xz_price_calculator.rb +167 -0
- data/lib/omnizip/algorithms/lzma/xz_probability_models.rb +258 -0
- data/lib/omnizip/algorithms/lzma/xz_range_encoder.rb +223 -0
- data/lib/omnizip/algorithms/lzma/xz_range_encoder_exact.rb +314 -0
- data/lib/omnizip/algorithms/lzma/xz_state.rb +108 -0
- data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +1311 -0
- data/lib/omnizip/algorithms/lzma.rb +266 -0
- data/lib/omnizip/algorithms/lzma2/chunk_manager.rb +180 -0
- data/lib/omnizip/algorithms/lzma2/constants.rb +38 -0
- data/lib/omnizip/algorithms/lzma2/encoder.rb +145 -0
- data/lib/omnizip/algorithms/lzma2/lzma2_chunk.rb +165 -0
- data/lib/omnizip/algorithms/lzma2/properties.rb +179 -0
- data/lib/omnizip/algorithms/lzma2/simple_lzma2_encoder.rb +122 -0
- data/lib/omnizip/algorithms/lzma2/xz_encoder_adapter.rb +85 -0
- data/lib/omnizip/algorithms/lzma2.rb +133 -0
- data/lib/omnizip/algorithms/ppmd7/constants.rb +74 -0
- data/lib/omnizip/algorithms/ppmd7/context.rb +151 -0
- data/lib/omnizip/algorithms/ppmd7/decoder.rb +127 -0
- data/lib/omnizip/algorithms/ppmd7/encoder.rb +153 -0
- data/lib/omnizip/algorithms/ppmd7/model.rb +245 -0
- data/lib/omnizip/algorithms/ppmd7/symbol_state.rb +57 -0
- data/lib/omnizip/algorithms/ppmd7.rb +146 -0
- data/lib/omnizip/algorithms/ppmd8/constants.rb +61 -0
- data/lib/omnizip/algorithms/ppmd8/context.rb +32 -0
- data/lib/omnizip/algorithms/ppmd8/decoder.rb +104 -0
- data/lib/omnizip/algorithms/ppmd8/encoder.rb +135 -0
- data/lib/omnizip/algorithms/ppmd8/model.rb +246 -0
- data/lib/omnizip/algorithms/ppmd8/restoration_method.rb +78 -0
- data/lib/omnizip/algorithms/ppmd8.rb +122 -0
- data/lib/omnizip/algorithms/ppmd_base.rb +142 -0
- data/lib/omnizip/algorithms/registration.rb +28 -0
- data/lib/omnizip/algorithms/zstandard/constants.rb +158 -0
- data/lib/omnizip/algorithms/zstandard/decoder.rb +249 -0
- data/lib/omnizip/algorithms/zstandard/dictionary.rb +73 -0
- data/lib/omnizip/algorithms/zstandard/encoder.rb +294 -0
- data/lib/omnizip/algorithms/zstandard/frame/block.rb +143 -0
- data/lib/omnizip/algorithms/zstandard/frame/header.rb +233 -0
- data/lib/omnizip/algorithms/zstandard/frame.rb +33 -0
- data/lib/omnizip/algorithms/zstandard/fse/bitstream.rb +183 -0
- data/lib/omnizip/algorithms/zstandard/fse/encoder.rb +534 -0
- data/lib/omnizip/algorithms/zstandard/fse/interleaved.rb +92 -0
- data/lib/omnizip/algorithms/zstandard/fse/table.rb +188 -0
- data/lib/omnizip/algorithms/zstandard/fse/table_description.rb +203 -0
- data/lib/omnizip/algorithms/zstandard/fse.rb +53 -0
- data/lib/omnizip/algorithms/zstandard/huffman.rb +276 -0
- data/lib/omnizip/algorithms/zstandard/huffman_encoder.rb +327 -0
- data/lib/omnizip/algorithms/zstandard/ldm.rb +121 -0
- data/lib/omnizip/algorithms/zstandard/literals.rb +241 -0
- data/lib/omnizip/algorithms/zstandard/literals_encoder.rb +96 -0
- data/lib/omnizip/algorithms/zstandard/match_finder.rb +483 -0
- data/lib/omnizip/algorithms/zstandard/sequences.rb +350 -0
- data/lib/omnizip/algorithms/zstandard/sequences_encoder.rb +333 -0
- data/lib/omnizip/algorithms/zstandard/xxhash.rb +159 -0
- data/lib/omnizip/algorithms/zstandard.rb +210 -0
- data/lib/omnizip/algorithms.rb +35 -0
- data/lib/omnizip/archive/builder.rb +48 -0
- data/lib/omnizip/archive/reader_session.rb +67 -0
- data/lib/omnizip/archive.rb +86 -0
- data/lib/omnizip/archive_handler.rb +82 -0
- data/lib/omnizip/archive_handlers/cpio_handler.rb +47 -0
- data/lib/omnizip/archive_handlers/iso_handler.rb +54 -0
- data/lib/omnizip/archive_handlers/ole_handler.rb +37 -0
- data/lib/omnizip/archive_handlers/rar_handler.rb +96 -0
- data/lib/omnizip/archive_handlers/rpm_handler.rb +45 -0
- data/lib/omnizip/archive_handlers/seven_zip_handler.rb +89 -0
- data/lib/omnizip/archive_handlers/tar_handler.rb +75 -0
- data/lib/omnizip/archive_handlers/xar_handler.rb +49 -0
- data/lib/omnizip/archive_handlers/zip_handler.rb +115 -0
- data/lib/omnizip/archive_handlers.rb +18 -0
- data/lib/omnizip/backends.rb +160 -0
- data/lib/omnizip/buffer/memory_archive.rb +246 -0
- data/lib/omnizip/buffer/memory_extractor.rb +234 -0
- data/lib/omnizip/buffer/seven_zip_bridge.rb +171 -0
- data/lib/omnizip/buffer.rb +187 -0
- data/lib/omnizip/checksum_registry.rb +41 -0
- data/lib/omnizip/checksums/crc32.rb +101 -0
- data/lib/omnizip/checksums/crc64.rb +102 -0
- data/lib/omnizip/checksums/crc_base.rb +158 -0
- data/lib/omnizip/checksums/registration.rb +16 -0
- data/lib/omnizip/checksums/verifier.rb +130 -0
- data/lib/omnizip/checksums.rb +11 -0
- data/lib/omnizip/chunked/memory_manager.rb +194 -0
- data/lib/omnizip/chunked/reader.rb +78 -0
- data/lib/omnizip/chunked/writer.rb +123 -0
- data/lib/omnizip/chunked.rb +123 -0
- data/lib/omnizip/cli/output_formatter.rb +112 -0
- data/lib/omnizip/cli.rb +604 -0
- data/lib/omnizip/commands/.keep +0 -0
- data/lib/omnizip/commands/archive_create_command.rb +245 -0
- data/lib/omnizip/commands/archive_extract_command.rb +231 -0
- data/lib/omnizip/commands/archive_list_command.rb +204 -0
- data/lib/omnizip/commands/archive_repair_command.rb +129 -0
- data/lib/omnizip/commands/archive_verify_command.rb +115 -0
- data/lib/omnizip/commands/compress_command.rb +115 -0
- data/lib/omnizip/commands/decompress_command.rb +149 -0
- data/lib/omnizip/commands/list_command.rb +51 -0
- data/lib/omnizip/commands/metadata_command.rb +215 -0
- data/lib/omnizip/commands/parity_create_command.rb +114 -0
- data/lib/omnizip/commands/parity_repair_command.rb +120 -0
- data/lib/omnizip/commands/parity_verify_command.rb +122 -0
- data/lib/omnizip/commands/profile_list_command.rb +54 -0
- data/lib/omnizip/commands/profile_show_command.rb +44 -0
- data/lib/omnizip/commands.rb +21 -0
- data/lib/omnizip/convenience.rb +389 -0
- data/lib/omnizip/converter/conversion_registry.rb +50 -0
- data/lib/omnizip/converter/conversion_strategy.rb +108 -0
- data/lib/omnizip/converter/extract_repack_strategy.rb +51 -0
- data/lib/omnizip/converter/seven_zip_to_zip_strategy.rb +43 -0
- data/lib/omnizip/converter/zip_to_seven_zip_strategy.rb +54 -0
- data/lib/omnizip/converter.rb +124 -0
- data/lib/omnizip/crypto/aes256/cipher.rb +104 -0
- data/lib/omnizip/crypto/aes256/constants.rb +28 -0
- data/lib/omnizip/crypto/aes256/key_derivation.rb +100 -0
- data/lib/omnizip/crypto/aes256.rb +102 -0
- data/lib/omnizip/crypto.rb +11 -0
- data/lib/omnizip/entry.rb +44 -0
- data/lib/omnizip/error.rb +110 -0
- data/lib/omnizip/eta/exponential_smoothing_estimator.rb +95 -0
- data/lib/omnizip/eta/moving_average_estimator.rb +96 -0
- data/lib/omnizip/eta/rate_calculator.rb +104 -0
- data/lib/omnizip/eta/sample_history.rb +143 -0
- data/lib/omnizip/eta/time_estimator.rb +106 -0
- data/lib/omnizip/eta.rb +68 -0
- data/lib/omnizip/extraction/filter_chain.rb +179 -0
- data/lib/omnizip/extraction/glob_pattern.rb +140 -0
- data/lib/omnizip/extraction/pattern_matcher.rb +66 -0
- data/lib/omnizip/extraction/predicate_pattern.rb +52 -0
- data/lib/omnizip/extraction/regex_pattern.rb +50 -0
- data/lib/omnizip/extraction/selective_extractor.rb +247 -0
- data/lib/omnizip/extraction.rb +115 -0
- data/lib/omnizip/file_type/mime_classifier.rb +144 -0
- data/lib/omnizip/file_type.rb +117 -0
- data/lib/omnizip/filter.rb +141 -0
- data/lib/omnizip/filter_pipeline.rb +108 -0
- data/lib/omnizip/filter_registry.rb +113 -0
- data/lib/omnizip/filters/bcj.rb +277 -0
- data/lib/omnizip/filters/bcj2/constants.rb +53 -0
- data/lib/omnizip/filters/bcj2/decoder.rb +263 -0
- data/lib/omnizip/filters/bcj2/encoder.rb +58 -0
- data/lib/omnizip/filters/bcj2/stream_data.rb +91 -0
- data/lib/omnizip/filters/bcj2.rb +93 -0
- data/lib/omnizip/filters/bcj_arm.rb +174 -0
- data/lib/omnizip/filters/bcj_arm64.rb +242 -0
- data/lib/omnizip/filters/bcj_ia64.rb +194 -0
- data/lib/omnizip/filters/bcj_ppc.rb +188 -0
- data/lib/omnizip/filters/bcj_sparc.rb +174 -0
- data/lib/omnizip/filters/bcj_x86.rb +191 -0
- data/lib/omnizip/filters/delta.rb +195 -0
- data/lib/omnizip/filters/xz_delta.rb +258 -0
- data/lib/omnizip/filters.rb +20 -0
- data/lib/omnizip/format_detector.rb +166 -0
- data/lib/omnizip/formats/.keep +0 -0
- data/lib/omnizip/formats/bzip2_file.rb +159 -0
- data/lib/omnizip/formats/cpio/bounded_io.rb +66 -0
- data/lib/omnizip/formats/cpio/constants.rb +55 -0
- data/lib/omnizip/formats/cpio/entry.rb +389 -0
- data/lib/omnizip/formats/cpio/reader.rb +194 -0
- data/lib/omnizip/formats/cpio/writer.rb +232 -0
- data/lib/omnizip/formats/cpio.rb +132 -0
- data/lib/omnizip/formats/format_spec_loader.rb +228 -0
- data/lib/omnizip/formats/gzip.rb +236 -0
- data/lib/omnizip/formats/iso/directory_builder.rb +321 -0
- data/lib/omnizip/formats/iso/directory_record.rb +162 -0
- data/lib/omnizip/formats/iso/joliet.rb +204 -0
- data/lib/omnizip/formats/iso/path_table.rb +125 -0
- data/lib/omnizip/formats/iso/reader.rb +194 -0
- data/lib/omnizip/formats/iso/rock_ridge.rb +349 -0
- data/lib/omnizip/formats/iso/volume_builder.rb +319 -0
- data/lib/omnizip/formats/iso/volume_descriptor.rb +168 -0
- data/lib/omnizip/formats/iso/writer.rb +543 -0
- data/lib/omnizip/formats/iso.rb +133 -0
- data/lib/omnizip/formats/lzip.rb +161 -0
- data/lib/omnizip/formats/lzma_alone.rb +149 -0
- 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 +134 -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 +73 -0
- data/lib/omnizip/formats/ole/allocation_table.rb +263 -0
- data/lib/omnizip/formats/ole/constants.rb +61 -0
- data/lib/omnizip/formats/ole/dirent.rb +439 -0
- data/lib/omnizip/formats/ole/header.rb +199 -0
- data/lib/omnizip/formats/ole/ranges_io.rb +347 -0
- data/lib/omnizip/formats/ole/storage.rb +482 -0
- data/lib/omnizip/formats/ole/types/variant.rb +338 -0
- data/lib/omnizip/formats/ole.rb +143 -0
- data/lib/omnizip/formats/rar/archive_repairer.rb +240 -0
- data/lib/omnizip/formats/rar/archive_verifier.rb +192 -0
- data/lib/omnizip/formats/rar/block_parser.rb +282 -0
- data/lib/omnizip/formats/rar/compression/bit_stream.rb +180 -0
- data/lib/omnizip/formats/rar/compression/dispatcher.rb +210 -0
- data/lib/omnizip/formats/rar/compression/lz77_huffman/decoder.rb +212 -0
- data/lib/omnizip/formats/rar/compression/lz77_huffman/encoder.rb +154 -0
- data/lib/omnizip/formats/rar/compression/lz77_huffman/huffman_builder.rb +217 -0
- data/lib/omnizip/formats/rar/compression/lz77_huffman/huffman_coder.rb +189 -0
- data/lib/omnizip/formats/rar/compression/lz77_huffman/match_finder.rb +135 -0
- data/lib/omnizip/formats/rar/compression/lz77_huffman/sliding_window.rb +165 -0
- data/lib/omnizip/formats/rar/compression/lz77_huffman.rb +25 -0
- data/lib/omnizip/formats/rar/compression/ppmd/context.rb +103 -0
- data/lib/omnizip/formats/rar/compression/ppmd/decoder.rb +238 -0
- data/lib/omnizip/formats/rar/compression/ppmd/encoder.rb +250 -0
- data/lib/omnizip/formats/rar/compression/ppmd.rb +16 -0
- data/lib/omnizip/formats/rar/compression.rb +17 -0
- data/lib/omnizip/formats/rar/compression_method_registry.rb +106 -0
- data/lib/omnizip/formats/rar/constants.rb +88 -0
- data/lib/omnizip/formats/rar/decompressor.rb +316 -0
- data/lib/omnizip/formats/rar/external_writer.rb +311 -0
- data/lib/omnizip/formats/rar/header.rb +209 -0
- data/lib/omnizip/formats/rar/license_validator.rb +111 -0
- data/lib/omnizip/formats/rar/models/rar_archive.rb +77 -0
- data/lib/omnizip/formats/rar/models/rar_entry.rb +65 -0
- data/lib/omnizip/formats/rar/models/rar_volume.rb +56 -0
- data/lib/omnizip/formats/rar/models.rb +14 -0
- data/lib/omnizip/formats/rar/parity_handler.rb +299 -0
- data/lib/omnizip/formats/rar/rar5/compression/lzma.rb +200 -0
- data/lib/omnizip/formats/rar/rar5/compression/lzss.rb +584 -0
- data/lib/omnizip/formats/rar/rar5/compression/store.rb +60 -0
- data/lib/omnizip/formats/rar/rar5/crc32.rb +39 -0
- data/lib/omnizip/formats/rar/rar5/encryption/aes256_cbc.rb +96 -0
- data/lib/omnizip/formats/rar/rar5/encryption/encryption_header.rb +114 -0
- data/lib/omnizip/formats/rar/rar5/encryption/encryption_manager.rb +162 -0
- data/lib/omnizip/formats/rar/rar5/encryption/key_derivation.rb +97 -0
- data/lib/omnizip/formats/rar/rar5/header.rb +208 -0
- data/lib/omnizip/formats/rar/rar5/models/encryption_options.rb +74 -0
- data/lib/omnizip/formats/rar/rar5/models/recovery_options.rb +63 -0
- data/lib/omnizip/formats/rar/rar5/models/solid_options.rb +63 -0
- data/lib/omnizip/formats/rar/rar5/models/volume_options.rb +74 -0
- data/lib/omnizip/formats/rar/rar5/multi_volume/ARCHITECTURE.md +290 -0
- data/lib/omnizip/formats/rar/rar5/multi_volume/volume_manager.rb +260 -0
- data/lib/omnizip/formats/rar/rar5/multi_volume/volume_splitter.rb +155 -0
- data/lib/omnizip/formats/rar/rar5/multi_volume/volume_writer.rb +176 -0
- data/lib/omnizip/formats/rar/rar5/solid/solid_encoder.rb +108 -0
- data/lib/omnizip/formats/rar/rar5/solid/solid_manager.rb +139 -0
- data/lib/omnizip/formats/rar/rar5/solid/solid_stream.rb +121 -0
- data/lib/omnizip/formats/rar/rar5/vint.rb +53 -0
- data/lib/omnizip/formats/rar/rar5/writer.rb +540 -0
- data/lib/omnizip/formats/rar/rar_format_base.rb +239 -0
- data/lib/omnizip/formats/rar/reader.rb +377 -0
- data/lib/omnizip/formats/rar/recovery_record.rb +245 -0
- data/lib/omnizip/formats/rar/volume_manager.rb +166 -0
- data/lib/omnizip/formats/rar/writer.rb +437 -0
- data/lib/omnizip/formats/rar.rb +193 -0
- data/lib/omnizip/formats/rar3/compressor.rb +71 -0
- data/lib/omnizip/formats/rar3/decompressor.rb +64 -0
- data/lib/omnizip/formats/rar3/reader.rb +107 -0
- data/lib/omnizip/formats/rar3/writer.rb +220 -0
- data/lib/omnizip/formats/rar3.rb +15 -0
- data/lib/omnizip/formats/rar5/compressor.rb +71 -0
- data/lib/omnizip/formats/rar5/decompressor.rb +64 -0
- data/lib/omnizip/formats/rar5/reader.rb +130 -0
- data/lib/omnizip/formats/rar5/writer.rb +65 -0
- data/lib/omnizip/formats/rar5.rb +15 -0
- data/lib/omnizip/formats/rpm/constants.rb +58 -0
- data/lib/omnizip/formats/rpm/entry.rb +106 -0
- data/lib/omnizip/formats/rpm/header.rb +106 -0
- data/lib/omnizip/formats/rpm/lead.rb +132 -0
- data/lib/omnizip/formats/rpm/tag.rb +228 -0
- data/lib/omnizip/formats/rpm/writer.rb +652 -0
- data/lib/omnizip/formats/rpm.rb +458 -0
- data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +274 -0
- data/lib/omnizip/formats/seven_zip/coder_chain.rb +169 -0
- data/lib/omnizip/formats/seven_zip/constants.rb +126 -0
- data/lib/omnizip/formats/seven_zip/encoded_header.rb +110 -0
- data/lib/omnizip/formats/seven_zip/encrypted_header.rb +139 -0
- data/lib/omnizip/formats/seven_zip/file_collector.rb +143 -0
- data/lib/omnizip/formats/seven_zip/header.rb +102 -0
- data/lib/omnizip/formats/seven_zip/header_encryptor.rb +132 -0
- data/lib/omnizip/formats/seven_zip/header_writer.rb +425 -0
- data/lib/omnizip/formats/seven_zip/models/coder_info.rb +30 -0
- data/lib/omnizip/formats/seven_zip/models/file_entry.rb +65 -0
- data/lib/omnizip/formats/seven_zip/models/folder.rb +67 -0
- data/lib/omnizip/formats/seven_zip/models/stream_info.rb +42 -0
- data/lib/omnizip/formats/seven_zip/models.rb +20 -0
- data/lib/omnizip/formats/seven_zip/parser.rb +671 -0
- data/lib/omnizip/formats/seven_zip/reader.rb +626 -0
- data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +611 -0
- data/lib/omnizip/formats/seven_zip/split_archive_writer.rb +308 -0
- data/lib/omnizip/formats/seven_zip/stream_compressor.rb +151 -0
- data/lib/omnizip/formats/seven_zip/stream_decompressor.rb +166 -0
- data/lib/omnizip/formats/seven_zip/writer.rb +801 -0
- data/lib/omnizip/formats/seven_zip.rb +179 -0
- data/lib/omnizip/formats/tar/constants.rb +73 -0
- data/lib/omnizip/formats/tar/entry.rb +98 -0
- data/lib/omnizip/formats/tar/header.rb +206 -0
- data/lib/omnizip/formats/tar/reader.rb +119 -0
- data/lib/omnizip/formats/tar/writer.rb +212 -0
- data/lib/omnizip/formats/tar.rb +74 -0
- data/lib/omnizip/formats/xar/constants.rb +119 -0
- data/lib/omnizip/formats/xar/entry.rb +268 -0
- data/lib/omnizip/formats/xar/header.rb +223 -0
- data/lib/omnizip/formats/xar/reader.rb +400 -0
- data/lib/omnizip/formats/xar/toc.rb +570 -0
- data/lib/omnizip/formats/xar/writer.rb +461 -0
- data/lib/omnizip/formats/xar.rb +145 -0
- data/lib/omnizip/formats/xz/reader.rb +121 -0
- data/lib/omnizip/formats/xz/writer.rb +416 -0
- data/lib/omnizip/formats/xz.rb +210 -0
- data/lib/omnizip/formats/xz_const.rb +49 -0
- data/lib/omnizip/formats/xz_impl/block_decoder.rb +761 -0
- data/lib/omnizip/formats/xz_impl/block_encoder.rb +304 -0
- data/lib/omnizip/formats/xz_impl/block_header.rb +210 -0
- data/lib/omnizip/formats/xz_impl/block_header_parser.rb +182 -0
- data/lib/omnizip/formats/xz_impl/index_decoder.rb +174 -0
- data/lib/omnizip/formats/xz_impl/index_encoder.rb +120 -0
- data/lib/omnizip/formats/xz_impl/stream_decoder.rb +470 -0
- data/lib/omnizip/formats/xz_impl/stream_encoder.rb +95 -0
- data/lib/omnizip/formats/xz_impl/stream_footer.rb +79 -0
- data/lib/omnizip/formats/xz_impl/stream_footer_parser.rb +115 -0
- data/lib/omnizip/formats/xz_impl/stream_header.rb +53 -0
- data/lib/omnizip/formats/xz_impl/stream_header_parser.rb +106 -0
- data/lib/omnizip/formats/xz_impl/vli.rb +128 -0
- data/lib/omnizip/formats/xz_impl.rb +52 -0
- data/lib/omnizip/formats/zip/central_directory_header.rb +223 -0
- data/lib/omnizip/formats/zip/constants.rb +69 -0
- data/lib/omnizip/formats/zip/end_of_central_directory.rb +132 -0
- data/lib/omnizip/formats/zip/local_file_header.rb +136 -0
- data/lib/omnizip/formats/zip/reader.rb +376 -0
- data/lib/omnizip/formats/zip/unix_extra_field.rb +153 -0
- data/lib/omnizip/formats/zip/writer.rb +489 -0
- data/lib/omnizip/formats/zip/zip64_end_of_central_directory.rb +102 -0
- data/lib/omnizip/formats/zip/zip64_end_of_central_directory_locator.rb +64 -0
- data/lib/omnizip/formats/zip/zip64_extra_field.rb +112 -0
- data/lib/omnizip/formats/zip.rb +50 -0
- data/lib/omnizip/formats.rb +29 -0
- data/lib/omnizip/implementations/base/lzma2_decoder_base.rb +75 -0
- data/lib/omnizip/implementations/base/lzma2_encoder_base.rb +128 -0
- data/lib/omnizip/implementations/base/lzma_decoder_base.rb +83 -0
- data/lib/omnizip/implementations/base/lzma_encoder_base.rb +108 -0
- data/lib/omnizip/implementations/base/state_machine_base.rb +182 -0
- data/lib/omnizip/implementations/base.rb +19 -0
- data/lib/omnizip/implementations/rust/archive.rb +93 -0
- data/lib/omnizip/implementations/rust/library.rb +221 -0
- data/lib/omnizip/implementations/rust.rb +26 -0
- data/lib/omnizip/implementations/seven_zip/lzma/decoder.rb +428 -0
- data/lib/omnizip/implementations/seven_zip/lzma/encoder.rb +456 -0
- data/lib/omnizip/implementations/seven_zip/lzma/match_finder.rb +285 -0
- data/lib/omnizip/implementations/seven_zip/lzma/range_decoder.rb +197 -0
- data/lib/omnizip/implementations/seven_zip/lzma/range_encoder.rb +194 -0
- data/lib/omnizip/implementations/seven_zip/lzma/state_machine.rb +139 -0
- data/lib/omnizip/implementations/seven_zip/lzma.rb +21 -0
- data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +56 -0
- data/lib/omnizip/implementations/seven_zip/lzma2.rb +12 -0
- data/lib/omnizip/implementations/seven_zip.rb +11 -0
- data/lib/omnizip/implementations/xz_utils/lzma2/decoder.rb +547 -0
- data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +748 -0
- data/lib/omnizip/implementations/xz_utils/lzma2.rb +13 -0
- data/lib/omnizip/implementations/xz_utils.rb +10 -0
- data/lib/omnizip/implementations.rb +25 -0
- data/lib/omnizip/io/buffered_input.rb +147 -0
- data/lib/omnizip/io/buffered_output.rb +106 -0
- data/lib/omnizip/io/source.rb +143 -0
- data/lib/omnizip/io/stream_manager.rb +117 -0
- data/lib/omnizip/io.rb +15 -0
- data/lib/omnizip/link_handler/hard_link.rb +79 -0
- data/lib/omnizip/link_handler/symbolic_link.rb +74 -0
- data/lib/omnizip/link_handler.rb +129 -0
- data/lib/omnizip/metadata/archive_metadata.rb +114 -0
- data/lib/omnizip/metadata/entry_metadata.rb +153 -0
- data/lib/omnizip/metadata/metadata_editor.rb +169 -0
- data/lib/omnizip/metadata/metadata_registry.rb +71 -0
- data/lib/omnizip/metadata/metadata_validator.rb +101 -0
- data/lib/omnizip/metadata.rb +58 -0
- data/lib/omnizip/models/.keep +0 -0
- data/lib/omnizip/models/algorithm_metadata.rb +41 -0
- data/lib/omnizip/models/attribute_apply.rb +40 -0
- data/lib/omnizip/models/compression_options.rb +53 -0
- data/lib/omnizip/models/conversion_options.rb +79 -0
- data/lib/omnizip/models/conversion_result.rb +141 -0
- data/lib/omnizip/models/eta_result.rb +45 -0
- data/lib/omnizip/models/extraction_rule.rb +115 -0
- data/lib/omnizip/models/filter_chain.rb +142 -0
- data/lib/omnizip/models/filter_config.rb +148 -0
- data/lib/omnizip/models/match_result.rb +141 -0
- data/lib/omnizip/models/optimization_suggestion.rb +95 -0
- data/lib/omnizip/models/parallel_options.rb +116 -0
- data/lib/omnizip/models/performance_result.rb +85 -0
- data/lib/omnizip/models/profile_report.rb +116 -0
- data/lib/omnizip/models/progress_options.rb +39 -0
- data/lib/omnizip/models/split_options.rb +116 -0
- data/lib/omnizip/optimization_registry.rb +45 -0
- data/lib/omnizip/parallel/engine.rb +52 -0
- data/lib/omnizip/parallel/job_queue.rb +209 -0
- data/lib/omnizip/parallel/job_scheduler.rb +225 -0
- data/lib/omnizip/parallel/parallel_compressor.rb +326 -0
- data/lib/omnizip/parallel/parallel_extractor.rb +238 -0
- data/lib/omnizip/parallel/thread_pool.rb +60 -0
- data/lib/omnizip/parallel/worker_pool.rb +227 -0
- data/lib/omnizip/parallel.rb +156 -0
- data/lib/omnizip/parity/chunked_block_processor.rb +194 -0
- data/lib/omnizip/parity/galois16.rb +145 -0
- data/lib/omnizip/parity/models/creator_packet.rb +71 -0
- data/lib/omnizip/parity/models/file_description_packet.rb +131 -0
- data/lib/omnizip/parity/models/ifsc_packet.rb +111 -0
- data/lib/omnizip/parity/models/main_packet.rb +126 -0
- data/lib/omnizip/parity/models/packet.rb +156 -0
- data/lib/omnizip/parity/models/packet_registry.rb +102 -0
- data/lib/omnizip/parity/models/recovery_slice_packet.rb +76 -0
- data/lib/omnizip/parity/models.rb +27 -0
- data/lib/omnizip/parity/par2_creator.rb +496 -0
- data/lib/omnizip/parity/par2_repairer.rb +409 -0
- data/lib/omnizip/parity/par2_verifier.rb +373 -0
- data/lib/omnizip/parity/par2cmdline_algorithm.rb +108 -0
- data/lib/omnizip/parity/par2cmdline_coefficients.rb +78 -0
- data/lib/omnizip/parity/reed_solomon_decoder.rb +263 -0
- data/lib/omnizip/parity/reed_solomon_encoder.rb +108 -0
- data/lib/omnizip/parity/reed_solomon_matrix.rb +339 -0
- data/lib/omnizip/parity.rb +207 -0
- data/lib/omnizip/password/encryption_registry.rb +26 -0
- data/lib/omnizip/password/encryption_strategy.rb +96 -0
- data/lib/omnizip/password/password_validator.rb +129 -0
- data/lib/omnizip/password/winzip_aes_strategy.rb +190 -0
- data/lib/omnizip/password/zip_crypto_strategy.rb +135 -0
- data/lib/omnizip/password.rb +88 -0
- data/lib/omnizip/pipe/stream_compressor.rb +124 -0
- data/lib/omnizip/pipe/stream_decompressor.rb +173 -0
- data/lib/omnizip/pipe.rb +120 -0
- data/lib/omnizip/platform/ntfs_streams.rb +200 -0
- data/lib/omnizip/platform.rb +189 -0
- data/lib/omnizip/profile/archive_profile.rb +36 -0
- data/lib/omnizip/profile/balanced_profile.rb +31 -0
- data/lib/omnizip/profile/binary_profile.rb +33 -0
- data/lib/omnizip/profile/compression_profile.rb +157 -0
- data/lib/omnizip/profile/custom_profile.rb +155 -0
- data/lib/omnizip/profile/fast_profile.rb +31 -0
- data/lib/omnizip/profile/maximum_profile.rb +31 -0
- data/lib/omnizip/profile/profile_detector.rb +108 -0
- data/lib/omnizip/profile/profile_registry.rb +160 -0
- data/lib/omnizip/profile/text_profile.rb +33 -0
- data/lib/omnizip/profile.rb +261 -0
- data/lib/omnizip/profiler/memory_profiler.rb +66 -0
- data/lib/omnizip/profiler/method_profiler.rb +49 -0
- data/lib/omnizip/profiler/report_generator.rb +158 -0
- data/lib/omnizip/profiler.rb +192 -0
- data/lib/omnizip/progress/callback_reporter.rb +34 -0
- data/lib/omnizip/progress/console_reporter.rb +59 -0
- data/lib/omnizip/progress/log_reporter.rb +89 -0
- data/lib/omnizip/progress/operation_progress.rb +118 -0
- data/lib/omnizip/progress/progress_bar.rb +156 -0
- data/lib/omnizip/progress/progress_reporter.rb +40 -0
- data/lib/omnizip/progress/progress_tracker.rb +215 -0
- data/lib/omnizip/progress/silent_reporter.rb +22 -0
- data/lib/omnizip/progress.rb +131 -0
- data/lib/omnizip/registry.rb +111 -0
- data/lib/omnizip/rubyzip_compat.rb +61 -0
- data/lib/omnizip/temp/safe_extract.rb +124 -0
- data/lib/omnizip/temp/temp_file.rb +123 -0
- data/lib/omnizip/temp/temp_file_pool.rb +109 -0
- data/lib/omnizip/temp.rb +184 -0
- data/lib/omnizip/version.rb +5 -0
- data/lib/omnizip/zip/entry.rb +160 -0
- data/lib/omnizip/zip/file.rb +485 -0
- data/lib/omnizip/zip/input_stream.rb +270 -0
- data/lib/omnizip/zip/output_stream.rb +320 -0
- data/lib/omnizip/zip.rb +23 -0
- data/lib/omnizip.rb +157 -0
- data/lib/tasks/msi_test_files.rake +66 -0
- data/lib/tasks/platform_gems.rake +147 -0
- data/lib/tasks/rust_backend.rake +48 -0
- data/readme-docs/advanced-features.adoc +514 -0
- data/readme-docs/api-usage.adoc +445 -0
- data/readme-docs/architecture.adoc +450 -0
- data/readme-docs/archive-formats.adoc +479 -0
- data/readme-docs/cli-usage.adoc +222 -0
- data/readme-docs/compression-algorithms.adoc +446 -0
- data/readme-docs/compression-profiles.adoc +247 -0
- data/readme-docs/encryption-checksums.adoc +328 -0
- data/readme-docs/format-converter.adoc +316 -0
- data/readme-docs/installation.adoc +228 -0
- data/readme-docs/par2-archives.adoc +608 -0
- data/readme-docs/performance-profiler.adoc +389 -0
- data/readme-docs/preprocessing-filters.adoc +280 -0
- data/vendor/libomnizip_ffi.so +0 -0
- data/xz-file-format-1.2.1.txt +1174 -0
- metadata +768 -0
|
@@ -0,0 +1,1311 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
# Copyright (C) 2025 Ribose Inc.
|
|
6
|
+
#
|
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
8
|
+
# copy of this software and associated documentation files (the "Software"),
|
|
9
|
+
# to deal in the Software without restriction, including without limitation
|
|
10
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
11
|
+
# and/or sell copies of the Software, and to permit persons to whom the
|
|
12
|
+
# Software is furnished to do so, subject to the following conditions:
|
|
13
|
+
#
|
|
14
|
+
# The above copyright notice and this permission notice shall be included in
|
|
15
|
+
# all copies or substantial portions of the Software.
|
|
16
|
+
#
|
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
22
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
23
|
+
# DEALINGS IN THE SOFTWARE.
|
|
24
|
+
|
|
25
|
+
module Omnizip
|
|
26
|
+
module Algorithms
|
|
27
|
+
# LZMA XZ Utils implementation
|
|
28
|
+
#
|
|
29
|
+
# This namespace contains the XZ Utils implementation of LZMA decoder.
|
|
30
|
+
# XZ Utils is based on LZMA SDK but has been MODIFIED SIGNIFICANTLY.
|
|
31
|
+
# This implementation is for XZ format (.xz files) ONLY.
|
|
32
|
+
#
|
|
33
|
+
# Reference: /Users/mulgogi/src/external/xz/src/liblzma/lzma/lzma_decoder.c
|
|
34
|
+
#
|
|
35
|
+
# XZ Utils LZMA decoder
|
|
36
|
+
#
|
|
37
|
+
# This class implements XZ Utils' LZMA decoder (NOT LZMA SDK/7-Zip!)
|
|
38
|
+
# XZ Utils is based on LZMA SDK but has been MODIFIED SIGNIFICANTLY.
|
|
39
|
+
# Reference: /Users/mulgogi/src/external/xz/src/liblzma/lzma/lzma_decoder.c
|
|
40
|
+
#
|
|
41
|
+
# This decoder is used for:
|
|
42
|
+
# - XZ format (.xz files)
|
|
43
|
+
# - LZMA2 compression in XZ format
|
|
44
|
+
#
|
|
45
|
+
|
|
46
|
+
# XZ Utils implementation of LZMA decoder
|
|
47
|
+
#
|
|
48
|
+
# IMPORTANT: This is NOT the LZMA SDK/7-Zip decoder!
|
|
49
|
+
# XZ Utils modified LZMA significantly - this is XZ format only.
|
|
50
|
+
# Components integrated:
|
|
51
|
+
# - LiteralDecoder: Matched/unmatched literal decoding
|
|
52
|
+
# - StateMachine: 12-state FSM for probability model selection
|
|
53
|
+
# - LengthCoder: 3-level length decoding (low/mid/high)
|
|
54
|
+
# - DistanceCoder: 64-slot distance decoding with aligned bits
|
|
55
|
+
#
|
|
56
|
+
# The decoder follows XZ Utils' exact decoding sequence:
|
|
57
|
+
# 1. Read LZMA header (property byte, dict size, uncompressed size)
|
|
58
|
+
# 2. Initialize range decoder and probability models
|
|
59
|
+
# 3. Decode loop:
|
|
60
|
+
# - Decode is_match bit
|
|
61
|
+
# - If literal: decode byte (matched/unmatched)
|
|
62
|
+
# - If match: decode length and distance
|
|
63
|
+
# - Update state machine
|
|
64
|
+
# - Write to output
|
|
65
|
+
# 4. Handle EOS marker
|
|
66
|
+
#
|
|
67
|
+
# @example Basic usage
|
|
68
|
+
# decoder = Omnizip::Algorithms::XzUtilsDecoder.new(input)
|
|
69
|
+
# data = decoder.decode_stream
|
|
70
|
+
#
|
|
71
|
+
# @example With output stream
|
|
72
|
+
# decoder = Omnizip::Algorithms::XzUtilsDecoder.new(input)
|
|
73
|
+
# File.open('output.txt', 'wb') { |f| decoder.decode_stream(f) }
|
|
74
|
+
class XzUtilsDecoder
|
|
75
|
+
include LZMA::Constants
|
|
76
|
+
|
|
77
|
+
# Maximum dictionary size to prevent memory exhaustion
|
|
78
|
+
# 64MB is a reasonable practical limit
|
|
79
|
+
MAX_DICT_SIZE = 64 * 1024 * 1024
|
|
80
|
+
|
|
81
|
+
# Alias for nested classes for easier access
|
|
82
|
+
BitModel = LZMA::BitModel
|
|
83
|
+
LengthCoder = LZMA::LengthCoder
|
|
84
|
+
DistanceCoder = LZMA::DistanceCoder
|
|
85
|
+
LiteralDecoder = LZMA::LiteralDecoder
|
|
86
|
+
RangeDecoder = LZMA::RangeDecoder
|
|
87
|
+
SdkStateMachine = Implementations::SevenZip::LZMA::StateMachine
|
|
88
|
+
|
|
89
|
+
attr_reader :lc, :lp, :pb, :dict_size, :uncompressed_size,
|
|
90
|
+
:range_decoder
|
|
91
|
+
|
|
92
|
+
# XZ Utils dictionary constants (from lz_decoder.h)
|
|
93
|
+
# See: /Users/mulgogi/src/external/xz/src/liblzma/lz/lz_decoder.h
|
|
94
|
+
LZ_DICT_REPEAT_MAX = 288
|
|
95
|
+
LZ_DICT_INIT_POS = 2 * LZ_DICT_REPEAT_MAX # = 576
|
|
96
|
+
|
|
97
|
+
# Map a monotonic position to a circular buffer index.
|
|
98
|
+
# Buffer layout: [LZ_DICT_INIT_POS zero bytes] + [dict_size circular region]
|
|
99
|
+
# @pos grows forever; this maps it to the fixed-size buffer.
|
|
100
|
+
# Matches C++ 7-Zip SDK: dic[dicPos] with dicPos wrapping at dicBufSize.
|
|
101
|
+
def dict_index(pos)
|
|
102
|
+
LZ_DICT_INIT_POS + ((pos - LZ_DICT_INIT_POS) % @dict_size)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Flush decoded output from the circular dictionary buffer.
|
|
106
|
+
# Copies bytes from @flush_pos to @pos into the output stream or accumulator.
|
|
107
|
+
# Handles wrap-around when the data spans the circular buffer boundary.
|
|
108
|
+
def flush_circular_output(output)
|
|
109
|
+
return if @pos == @flush_pos
|
|
110
|
+
|
|
111
|
+
length = @pos - @flush_pos
|
|
112
|
+
|
|
113
|
+
if length > @dict_size
|
|
114
|
+
raise Omnizip::DecompressionError,
|
|
115
|
+
"Internal error: flush lag (#{length}) exceeds dict_size (#{@dict_size})"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
start_idx = dict_index(@flush_pos)
|
|
119
|
+
buf_end = @buf_end
|
|
120
|
+
|
|
121
|
+
if start_idx + length <= buf_end
|
|
122
|
+
# No wrap
|
|
123
|
+
data = @dict_buf.byteslice(start_idx, length)
|
|
124
|
+
else
|
|
125
|
+
# Wraps around the circular boundary
|
|
126
|
+
first_len = buf_end - start_idx
|
|
127
|
+
data = @dict_buf.byteslice(start_idx, first_len)
|
|
128
|
+
data << @dict_buf.byteslice(LZ_DICT_INIT_POS, length - first_len)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if output
|
|
132
|
+
output.write(data.force_encoding(Encoding::BINARY))
|
|
133
|
+
else
|
|
134
|
+
@output_accumulator ||= StringIO.new("".b)
|
|
135
|
+
@output_accumulator.write(data)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
@flush_pos = @pos
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Initialize the SDK-compatible decoder
|
|
142
|
+
#
|
|
143
|
+
# @param input [IO] Input stream of compressed data
|
|
144
|
+
# @param options [Hash] Decoding options
|
|
145
|
+
# @option options [Boolean] :lzma2_mode If true, initialize without reading header
|
|
146
|
+
# (for LZMA2 use, requires lc, lp, pb, dict_size, uncompressed_size)
|
|
147
|
+
# @option options [Integer] :lc Literal context bits (required for lzma2_mode)
|
|
148
|
+
# @option options [Integer] :lp Literal position bits (required for lzma2_mode)
|
|
149
|
+
# @option options [Integer] :pb Position bits (required for lzma2_mode)
|
|
150
|
+
# @option options [Integer] :dict_size Dictionary size (required for lzma2_mode)
|
|
151
|
+
# @option options [Integer] :uncompressed_size Uncompressed size (required for lzma2_mode)
|
|
152
|
+
# @option options [String] :preloaded_data Data to preload into dictionary (for LZMA2
|
|
153
|
+
# uncompressed chunks followed by compressed chunks)
|
|
154
|
+
# @option options [Boolean] :validate_size If true, validate decoded size matches uncompressed_size
|
|
155
|
+
# (default: false, only for .lzma format)
|
|
156
|
+
def initialize(input, options = {})
|
|
157
|
+
@input = input
|
|
158
|
+
@decoder_id = object_id # Track decoder instance ID
|
|
159
|
+
|
|
160
|
+
# Check for preloaded data (from LZMA2 uncompressed chunks)
|
|
161
|
+
@preloaded_data = options[:preloaded_data]
|
|
162
|
+
@validate_size = options.fetch(:validate_size, false)
|
|
163
|
+
@allow_eopm = options.fetch(:allow_eopm, nil)
|
|
164
|
+
|
|
165
|
+
if options[:lzma2_mode]
|
|
166
|
+
# Direct initialization for LZMA2 use (XZ Utils pattern)
|
|
167
|
+
# The LZMA2 decoder provides parameters directly, no header to read
|
|
168
|
+
# See: /Users/mulgogi/src/external/xz/src/liblzma/lzma/lzma2_decoder.c:140-141
|
|
169
|
+
@lc = options.fetch(:lc)
|
|
170
|
+
@lp = options.fetch(:lp)
|
|
171
|
+
@pb = options.fetch(:pb)
|
|
172
|
+
@dict_size = [[options.fetch(:dict_size), 1].max, MAX_DICT_SIZE].min
|
|
173
|
+
@uncompressed_size = options.fetch(:uncompressed_size)
|
|
174
|
+
else
|
|
175
|
+
# Standalone LZMA file - read header from input
|
|
176
|
+
read_header
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
validate_parameters
|
|
180
|
+
init_models
|
|
181
|
+
init_coders
|
|
182
|
+
|
|
183
|
+
# Cache computed values for hot loop
|
|
184
|
+
@pb_mask = (1 << @pb) - 1
|
|
185
|
+
@pb_shift = 1 << @pb
|
|
186
|
+
@literal_mask = (0x100 << @lp) - (0x100 >> @lc)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Decode a compressed stream
|
|
190
|
+
#
|
|
191
|
+
# Main decoding loop following SDK's LzmaDec_DecodeToDic logic:
|
|
192
|
+
# 1. Initialize range decoder
|
|
193
|
+
# 2. Process each position: decode literals/matches
|
|
194
|
+
# 3. Detect EOS marker
|
|
195
|
+
# 4. Return decompressed data
|
|
196
|
+
#
|
|
197
|
+
# XZ Utils dictionary system (from lz_decoder.h):
|
|
198
|
+
# - pos starts at LZ_DICT_INIT_POS (576)
|
|
199
|
+
# - full = pos - LZ_DICT_INIT_POS (count of valid bytes)
|
|
200
|
+
# - has_wrapped = false until dictionary buffer wraps
|
|
201
|
+
# - Distance validation: full > distance
|
|
202
|
+
#
|
|
203
|
+
# @param output [IO, nil] Optional output stream (if nil, returns String)
|
|
204
|
+
# @param preserve_dict [Boolean] Whether to preserve dictionary from previous decode
|
|
205
|
+
# @param check_rc_finished [Boolean] Whether to check if range decoder is finished
|
|
206
|
+
# @return [String, Integer] Decompressed data or bytes written
|
|
207
|
+
def decode_stream(output = nil, preserve_dict: false,
|
|
208
|
+
check_rc_finished: true)
|
|
209
|
+
@decode_stream_call_count ||= 0
|
|
210
|
+
@decode_stream_call_count += 1
|
|
211
|
+
|
|
212
|
+
# Create range decoder if it doesn't exist (first chunk)
|
|
213
|
+
# This happens when the decoder is created directly for LZMA (not LZMA2)
|
|
214
|
+
@range_decoder ||= RangeDecoder.new(@input)
|
|
215
|
+
|
|
216
|
+
# Special case: empty input (uncompressed_size == 0)
|
|
217
|
+
# Return immediately without trying to decode anything
|
|
218
|
+
if @uncompressed_size != 0xFFFFFFFFFFFFFFFF && @uncompressed_size.zero?
|
|
219
|
+
return "" # Empty output
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
@debug_iter = 0
|
|
223
|
+
|
|
224
|
+
# Track bytes decoded in this chunk (for multi-chunk streams)
|
|
225
|
+
# This is needed to limit match lengths correctly when @uncompressed_size
|
|
226
|
+
# represents only the current chunk's size, not the total size
|
|
227
|
+
@chunk_bytes_decoded = 0
|
|
228
|
+
|
|
229
|
+
# Initialize state and dictionary (XZ Utils system from lz_decoder.c)
|
|
230
|
+
# See: /Users/mulgogi/src/external/xz/src/liblzma/lz/lz_decoder.c:56
|
|
231
|
+
# For LZMA2 multi-chunk streams, state machine persists across chunks
|
|
232
|
+
# Only reset when not preserving dictionary (first chunk)
|
|
233
|
+
#
|
|
234
|
+
# IMPORTANT: Initialize @state if it's nil (first call) OR if not preserving dict
|
|
235
|
+
if @state.nil? || !preserve_dict
|
|
236
|
+
@state = SdkStateMachine.new
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# For LZMA2 multi-chunk streams, preserve dictionary across chunks
|
|
240
|
+
# when preserve_dict is true (control >= 0x80 but < 0xA0)
|
|
241
|
+
# For subsequent chunks, the reset() method handles dictionary reset
|
|
242
|
+
# For the first chunk (when @dict_buf is nil), we need to init it here
|
|
243
|
+
if @dict_buf.nil?
|
|
244
|
+
@buf_end = LZ_DICT_INIT_POS + @dict_size
|
|
245
|
+
buf_size = @buf_end
|
|
246
|
+
@dict_buf = ("\0" * buf_size).b
|
|
247
|
+
@pos = LZ_DICT_INIT_POS
|
|
248
|
+
@dict_pos = LZ_DICT_INIT_POS # Circular buffer write position (tracks @pos)
|
|
249
|
+
@dict_full = 0
|
|
250
|
+
@has_wrapped = false
|
|
251
|
+
|
|
252
|
+
# Add preloaded data to dictionary (from LZMA2 uncompressed chunks)
|
|
253
|
+
# This must be done before decoding so matches can reference this data
|
|
254
|
+
if @preloaded_data && !@preloaded_data.empty?
|
|
255
|
+
idx = @dict_pos
|
|
256
|
+
buf_end = @buf_end
|
|
257
|
+
@preloaded_data.each_byte do |byte|
|
|
258
|
+
@dict_buf.setbyte(idx, byte)
|
|
259
|
+
@pos += 1
|
|
260
|
+
idx += 1
|
|
261
|
+
idx = LZ_DICT_INIT_POS if idx >= buf_end
|
|
262
|
+
end
|
|
263
|
+
@dict_pos = idx
|
|
264
|
+
# Update dict_full to reflect preloaded data
|
|
265
|
+
@dict_full = @pos - LZ_DICT_INIT_POS
|
|
266
|
+
@preloaded_data = nil # Clear after loading
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Track starting position for multi-chunk streams
|
|
271
|
+
# IMPORTANT: Calculate start_pos AFTER dictionary initialization!
|
|
272
|
+
# This ensures that preloaded data (from LZMA2 uncompressed chunks) is
|
|
273
|
+
# properly reflected in start_pos, so we only return NEW bytes.
|
|
274
|
+
# For LZMA2, we need to return only the NEW bytes, not all bytes from LZ_DICT_INIT_POS
|
|
275
|
+
start_pos = @pos || LZ_DICT_INIT_POS
|
|
276
|
+
|
|
277
|
+
# Initialize rep distances (XZ Utils initializes to 0)
|
|
278
|
+
# See: /Users/mulgogi/src/external/xz/src/liblzma/lzma/lzma_decoder.c:1054-1055
|
|
279
|
+
# For LZMA2 multi-chunk streams, rep distances persist across chunks
|
|
280
|
+
# Only reset when not preserving dictionary (first chunk)
|
|
281
|
+
#
|
|
282
|
+
# IMPORTANT: Initialize rep distances if they're nil OR not preserving dict
|
|
283
|
+
if @rep0.nil? || @rep1.nil? || @rep2.nil? || @rep3.nil? || !preserve_dict
|
|
284
|
+
@rep0 = 0
|
|
285
|
+
@rep1 = 0
|
|
286
|
+
@rep2 = 0
|
|
287
|
+
@rep3 = 0
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Read range decoder init bytes (must happen after set_input sets correct stream)
|
|
291
|
+
@range_decoder.read_init_bytes if @range_decoder.init_bytes_remaining.positive?
|
|
292
|
+
|
|
293
|
+
# Main decoding loop
|
|
294
|
+
# XZ Utils pattern (lzma_decoder.c:305-306):
|
|
295
|
+
# Set dict.limit = dict.pos + (size_t)(coder->uncompressed_size)
|
|
296
|
+
# Then check dict.pos < dict.limit
|
|
297
|
+
# Since our @pos starts at LZ_DICT_INIT_POS, we set limit accordingly
|
|
298
|
+
# IMPORTANT: For multi-chunk streams, calculate limit from start_pos, not LZ_DICT_INIT_POS!
|
|
299
|
+
# XZ Utils uses dict->pos (current position) + uncompressed_size
|
|
300
|
+
# We use start_pos (current position) + @uncompressed_size
|
|
301
|
+
limit = if @uncompressed_size == 0xFFFFFFFFFFFFFFFF
|
|
302
|
+
nil # No limit for unknown size
|
|
303
|
+
else
|
|
304
|
+
start_pos + @uncompressed_size
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Circular buffer: no growth needed. Buffer is fixed at dict_size + LZ_DICT_INIT_POS.
|
|
308
|
+
# Output is flushed incrementally when the circular position nears wrap point.
|
|
309
|
+
# Matches C++ 7-Zip SDK: dic is pre-allocated to dicBufSize and never resized.
|
|
310
|
+
@flush_pos = @pos
|
|
311
|
+
@output_accumulator = output.nil? ? StringIO.new("".b) : nil
|
|
312
|
+
|
|
313
|
+
iteration = 0
|
|
314
|
+
loop do
|
|
315
|
+
iteration += 1
|
|
316
|
+
# Check if we've reached the expected size (if known)
|
|
317
|
+
# XZ Utils: checks dict.pos < dict.limit
|
|
318
|
+
|
|
319
|
+
# Handle nil @pos or limit gracefully
|
|
320
|
+
if limit && (@pos.nil? || limit.nil?)
|
|
321
|
+
raise Omnizip::DecompressionError, "Invalid state: @pos=#{@pos.inspect}, limit=#{limit.inspect}"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Circular buffer: flush output before it can be overwritten.
|
|
325
|
+
# LZMA matches are at most 273 bytes, so flush when within 273 of wrapping.
|
|
326
|
+
if @pos - @flush_pos > @dict_size - 273
|
|
327
|
+
flush_circular_output(output)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Decode is_match bit
|
|
331
|
+
pos_state = @pos & @pb_mask
|
|
332
|
+
# XZ Utils: is_match[state][pos_state] where the array is NUM_STATES * (1 << pb)
|
|
333
|
+
# The array stride changes with pb value
|
|
334
|
+
model_index = (@state.value * @pb_shift) + pos_state
|
|
335
|
+
|
|
336
|
+
@debug_iter += 1
|
|
337
|
+
|
|
338
|
+
is_match = @range_decoder.decode_bit(@is_match_models[model_index])
|
|
339
|
+
|
|
340
|
+
if is_match.zero?
|
|
341
|
+
# Decode literal
|
|
342
|
+
decode_literal
|
|
343
|
+
elsif decode_match
|
|
344
|
+
# Decode match - returns true if EOS detected
|
|
345
|
+
break
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# XZ Utils: Check if we've reached the limit (known uncompressed size)
|
|
349
|
+
# Reference: lzma_decoder.c:347, 680-692
|
|
350
|
+
# When dict.pos == dict.limit, the decoder should stop
|
|
351
|
+
# IMPORTANT: Must verify range decoder is finished (code == 0)
|
|
352
|
+
# If code != 0, there's leftover data in the compressed stream (corruption)
|
|
353
|
+
if limit && @pos >= limit
|
|
354
|
+
|
|
355
|
+
# XZ Utils pattern (lzma_decoder.c:689-700):
|
|
356
|
+
# Check if range decoder is finished (code == 0)
|
|
357
|
+
# - If finished → STREAM_END (success)
|
|
358
|
+
# - If NOT finished AND allow_eopm is false → DATA_ERROR (corruption)
|
|
359
|
+
# - If NOT finished AND allow_eopm is true → continue (expect EOPM)
|
|
360
|
+
# Reference: /Users/mulgogi/src/external/xz/src/liblzma/lzma/lzma_decoder.c:689-700
|
|
361
|
+
#
|
|
362
|
+
# For LZMA2: @allow_eopm is false, so range decoder MUST be finished
|
|
363
|
+
# For .lzma format: @allow_eopm may be true, so we continue decoding to find EOPM
|
|
364
|
+
# Reference: /Users/mulgogi/src/external/xz/src/liblzma/rangecoder/range_decoder.h:138-139
|
|
365
|
+
# rc_is_finished(range_decoder) = ((range_decoder).code == 0)
|
|
366
|
+
#
|
|
367
|
+
# NOTE: The check_rc_finished parameter is a legacy override for .lzma format
|
|
368
|
+
# If explicitly set to false, it allows EOPM even when uncompressed size is known
|
|
369
|
+
# Reference: alone_decoder.c:127 (LZMA_LZMA1EXT_ALLOW_EOPM)
|
|
370
|
+
should_check = if @allow_eopm == true
|
|
371
|
+
# EOPM is explicitly allowed, skip the check
|
|
372
|
+
false
|
|
373
|
+
elsif @allow_eopm == false
|
|
374
|
+
# LZMA2 mode: always check (EOPM is not allowed)
|
|
375
|
+
true
|
|
376
|
+
else
|
|
377
|
+
# @allow_eopm is nil (not set, first chunk or legacy mode)
|
|
378
|
+
# Use check_rc_finished parameter as default
|
|
379
|
+
check_rc_finished
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
if should_check
|
|
383
|
+
# If EOPM is not allowed, range decoder MUST be finished
|
|
384
|
+
unless @range_decoder.code.zero?
|
|
385
|
+
raise Omnizip::DecompressionError,
|
|
386
|
+
"LZMA stream finished with leftover compressed data (range_decoder.code=#{@range_decoder.code}, expected 0). This indicates corruption in the compressed stream or an invalid EOPM for LZMA2."
|
|
387
|
+
end
|
|
388
|
+
# XZ Utils pattern (lzma_decoder.c): when STREAM_END is reached,
|
|
389
|
+
# reset the range coder for the next chunk. This happens even for
|
|
390
|
+
# no-reset chunks (control 0x80-0x9F) - the range coder is ALWAYS
|
|
391
|
+
# re-initialized between chunks, only state/dict/models are preserved.
|
|
392
|
+
@range_decoder.reset
|
|
393
|
+
break
|
|
394
|
+
elsif @range_decoder.code.zero?
|
|
395
|
+
# EOPM is allowed (e.g., LZMA_Alone format)
|
|
396
|
+
# If range decoder is finished, we're done
|
|
397
|
+
# XZ Utils: rc_reset at STREAM_END
|
|
398
|
+
@range_decoder.reset
|
|
399
|
+
break
|
|
400
|
+
# Otherwise, continue decoding to find EOPM marker
|
|
401
|
+
# XZ Utils sets eopm_is_valid = true and continues
|
|
402
|
+
# Reference: lzma_decoder.c:704
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Validate decoded size against expected uncompressed_size
|
|
408
|
+
# Only for .lzma (LZMA_Alone) format where validate_size=true
|
|
409
|
+
# For .lzma format with known uncompressed_size, verify we decoded the right amount
|
|
410
|
+
# This catches "too_small_size-without-eopm" files where the header says 1 byte
|
|
411
|
+
# but the compressed data produces more output
|
|
412
|
+
# XZ format does NOT validate size at the LZMA decoder level - it's handled at block level
|
|
413
|
+
if @validate_size && @uncompressed_size && @uncompressed_size != 0xFFFFFFFFFFFFFFFF
|
|
414
|
+
# Calculate actual decoded size (from start of data, not LZ_DICT_INIT_POS)
|
|
415
|
+
actual_decoded_size = @pos - LZ_DICT_INIT_POS
|
|
416
|
+
|
|
417
|
+
if actual_decoded_size != @uncompressed_size
|
|
418
|
+
raise Omnizip::DecompressionError,
|
|
419
|
+
"LZMA stream size mismatch: expected #{@uncompressed_size} bytes, decoded #{actual_decoded_size} bytes. The file may be corrupted or have an invalid uncompressed size field."
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# IMPORTANT: Check for leftover compressed data after EOPM
|
|
423
|
+
# If EOPM was encountered (range_decoder.code == 0) but there's still data
|
|
424
|
+
# in the input stream, the file is corrupted.
|
|
425
|
+
# Reference: /Users/mulgogi/src/external/xz/src/liblzma/common/alone_decoder.c
|
|
426
|
+
#
|
|
427
|
+
# We only check for leftover data when:
|
|
428
|
+
# 1. EOPM was encountered (code == 0) AND
|
|
429
|
+
# 2. There's more data in the input stream
|
|
430
|
+
#
|
|
431
|
+
# If EOPM was NOT encountered (code != 0), leftover data is expected
|
|
432
|
+
# (it's part of the compressed stream that we haven't read yet).
|
|
433
|
+
if @allow_eopm && @range_decoder&.code&.zero? && @range_decoder&.stream
|
|
434
|
+
stream = @range_decoder.stream
|
|
435
|
+
# Try to peek at the next byte - if available, there's data AFTER EOPM
|
|
436
|
+
begin
|
|
437
|
+
next_byte = stream.getbyte
|
|
438
|
+
if next_byte
|
|
439
|
+
# Put the byte back
|
|
440
|
+
# allowed: caller-supplied stream; peek-back is optional
|
|
441
|
+
stream.ungetbyte(next_byte) if stream.respond_to?(:ungetbyte)
|
|
442
|
+
raise Omnizip::DecompressionError,
|
|
443
|
+
"LZMA_Alone file has data after the end-of-payload marker. The file may be corrupted or contain concatenated streams."
|
|
444
|
+
end
|
|
445
|
+
rescue IOError, EOFError
|
|
446
|
+
# Stream doesn't support peeking or is exhausted, that's fine
|
|
447
|
+
end
|
|
448
|
+
elsif !@allow_eopm && @range_decoder&.stream
|
|
449
|
+
# For LZMA2 mode (EOPM not allowed): check for leftover data
|
|
450
|
+
stream = @range_decoder.stream
|
|
451
|
+
begin
|
|
452
|
+
next_byte = stream.getbyte
|
|
453
|
+
if next_byte
|
|
454
|
+
# allowed: caller-supplied stream; peek-back is optional
|
|
455
|
+
stream.ungetbyte(next_byte) if stream.respond_to?(:ungetbyte)
|
|
456
|
+
raise Omnizip::DecompressionError,
|
|
457
|
+
"LZMA_Alone file has more compressed data than expected. The uncompressed size field (#{@uncompressed_size} bytes) appears to be too small."
|
|
458
|
+
end
|
|
459
|
+
rescue IOError, EOFError
|
|
460
|
+
# Stream doesn't support peeking or is exhausted, that's fine
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Flush remaining output from the circular buffer
|
|
466
|
+
flush_circular_output(output)
|
|
467
|
+
|
|
468
|
+
# Return output
|
|
469
|
+
if output
|
|
470
|
+
@pos - start_pos
|
|
471
|
+
else
|
|
472
|
+
@output_accumulator.string.force_encoding(Encoding::BINARY)
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Reset the decoder state for reuse with new properties
|
|
477
|
+
#
|
|
478
|
+
# XZ Utils pattern (lzma_decoder.c:1034-1083):
|
|
479
|
+
# - Resets state machine and rep distances
|
|
480
|
+
# - Resets range decoder
|
|
481
|
+
# - Reinitializes all probability models
|
|
482
|
+
# - Preserves dictionary (managed externally by LZMA2 decoder)
|
|
483
|
+
#
|
|
484
|
+
# @param new_lc [Integer, nil] New lc value (if nil, keeps current)
|
|
485
|
+
# @param new_lp [Integer, nil] New lp value (if nil, keeps current)
|
|
486
|
+
# @param new_pb [Integer, nil] New pb value (if nil, keeps current)
|
|
487
|
+
# @param preserve_dict [Boolean] If true, preserve dictionary state (pos, dict_full)
|
|
488
|
+
# @return [void]
|
|
489
|
+
def reset(new_lc: nil, new_lp: nil, new_pb: nil, preserve_dict: false)
|
|
490
|
+
# Update properties if provided
|
|
491
|
+
properties_changed = !!(new_lc || new_lp || new_pb)
|
|
492
|
+
@lc = new_lc if new_lc
|
|
493
|
+
@lp = new_lp if new_lp
|
|
494
|
+
@pb = new_pb if new_pb
|
|
495
|
+
|
|
496
|
+
# Recompute cached values when properties change
|
|
497
|
+
if properties_changed
|
|
498
|
+
@pb_mask = (1 << @pb) - 1
|
|
499
|
+
@pb_shift = 1 << @pb
|
|
500
|
+
@literal_mask = (0x100 << @lp) - (0x100 >> @lc)
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Reset state machine (XZ Utils line 1053)
|
|
504
|
+
# Always create a new state machine when resetting
|
|
505
|
+
@state = SdkStateMachine.new
|
|
506
|
+
|
|
507
|
+
# Reset rep distances (XZ Utils lines 1071-1074)
|
|
508
|
+
# IMPORTANT: ALWAYS reset rep distances to 0 when state is reset
|
|
509
|
+
# This happens for both control=0xE0 (dict reset) and control=0xC0 (state reset)
|
|
510
|
+
# Reference: /Users/mulgogi/src/external/xz/src/liblzma/lzma/lzma_decoder.c:1071-1074
|
|
511
|
+
@rep0 = 0
|
|
512
|
+
@rep1 = 0
|
|
513
|
+
@rep2 = 0
|
|
514
|
+
@rep3 = 0
|
|
515
|
+
|
|
516
|
+
# Reset range decoder for next chunk
|
|
517
|
+
# XZ Utils pattern (lzma_decoder.c:1061):
|
|
518
|
+
# - rc_reset sets range=UINT32_MAX, code=0, init_bytes_left=5
|
|
519
|
+
# - This MUST happen during reset, not deferred to decode_stream
|
|
520
|
+
# Reference: /Users/mulgogi/src/external/xz/src/liblzma/lzma/lzma_decoder.c:1061
|
|
521
|
+
@range_decoder&.reset
|
|
522
|
+
|
|
523
|
+
# Reinitialize probability models (XZ Utils lines 1064-1082)
|
|
524
|
+
# IMPORTANT: Use reset_models (reset in place) instead of init_models (create new)
|
|
525
|
+
# for state reset only. Only create new models when properties change.
|
|
526
|
+
if properties_changed
|
|
527
|
+
init_models
|
|
528
|
+
else
|
|
529
|
+
reset_models
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Reinitialize coders (needed for pb changes)
|
|
533
|
+
# Only recreate coders when properties have changed
|
|
534
|
+
init_coders if properties_changed
|
|
535
|
+
|
|
536
|
+
# Reset dictionary position and full count (XZ Utils pattern)
|
|
537
|
+
# Only reset if preserve_dict is false
|
|
538
|
+
unless preserve_dict
|
|
539
|
+
# Reinitialize dictionary buffer
|
|
540
|
+
# XZ Utils allocates a new buffer for each dictionary reset
|
|
541
|
+
@dict_buf = ("\0" * (@dict_size + LZ_DICT_INIT_POS)).b
|
|
542
|
+
@pos = LZ_DICT_INIT_POS
|
|
543
|
+
@dict_pos = LZ_DICT_INIT_POS
|
|
544
|
+
@dict_full = 0
|
|
545
|
+
@has_wrapped = false
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
nil
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Reset all probability models in place (without creating new arrays)
|
|
552
|
+
#
|
|
553
|
+
# This matches XZ Utils init_temporals behavior for control >= 0xA0.
|
|
554
|
+
# Unlike init_models which creates new arrays, this resets existing
|
|
555
|
+
# BitModels in place to preserve object identity for any references.
|
|
556
|
+
#
|
|
557
|
+
# @return [void]
|
|
558
|
+
def reset_models
|
|
559
|
+
# Reset literal models
|
|
560
|
+
@literal_models.each(&:reset)
|
|
561
|
+
|
|
562
|
+
# Reset match/rep models
|
|
563
|
+
@is_match_models.each(&:reset)
|
|
564
|
+
@is_rep_models.each(&:reset)
|
|
565
|
+
@is_rep0_models.each(&:reset)
|
|
566
|
+
@is_rep1_models.each(&:reset)
|
|
567
|
+
@is_rep2_models.each(&:reset)
|
|
568
|
+
@is_rep0_long_models.each(&:reset)
|
|
569
|
+
|
|
570
|
+
# Reset length coders
|
|
571
|
+
@length_coder.reset_models
|
|
572
|
+
@rep_length_coder.reset_models
|
|
573
|
+
|
|
574
|
+
# Reset distance coder
|
|
575
|
+
@distance_coder.reset_models
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# Reset only state machine and rep distances, preserve probability models
|
|
579
|
+
#
|
|
580
|
+
# XZ Utils pattern for state reset only (control >= 0xA0):
|
|
581
|
+
# - Reset state machine
|
|
582
|
+
# - Reset rep distances
|
|
583
|
+
# - Reset probability models (via reset_models)
|
|
584
|
+
# - Reset range decoder (rc_reset + rc_read_init)
|
|
585
|
+
# - PRESERVE dictionary content (no dict_reset)
|
|
586
|
+
#
|
|
587
|
+
# XZ Utils source (lzma2_decoder.c):
|
|
588
|
+
# - For control >= 0xA0: calls lzma_lzma_decoder_reset(decoder, NULL)
|
|
589
|
+
# - lzma_lzma_decoder_reset always calls init_temporals which resets probability models
|
|
590
|
+
#
|
|
591
|
+
# @return [void]
|
|
592
|
+
# Prepare state reset - called BEFORE setting new input
|
|
593
|
+
#
|
|
594
|
+
# Resets state machine, rep distances, and probability models.
|
|
595
|
+
# The range decoder will be reset in finish_state_reset AFTER
|
|
596
|
+
# the new input is set (to match XZ Utils lzma_decoder_reset behavior).
|
|
597
|
+
#
|
|
598
|
+
# For LZMA2 control >= 0xC0, this is called before set_input to reset
|
|
599
|
+
# everything except the range decoder for the new chunk.
|
|
600
|
+
#
|
|
601
|
+
# @return [void]
|
|
602
|
+
def prepare_state_reset
|
|
603
|
+
# Reset state machine (XZ Utils line 1053)
|
|
604
|
+
@state = SdkStateMachine.new
|
|
605
|
+
|
|
606
|
+
# Reset rep distances (XZ Utils lines 1054-1057)
|
|
607
|
+
@rep0 = 0
|
|
608
|
+
@rep1 = 0
|
|
609
|
+
@rep2 = 0
|
|
610
|
+
@rep3 = 0
|
|
611
|
+
|
|
612
|
+
# Reset probability models (XZ Utils init_temporals for control >= 0xA0)
|
|
613
|
+
reset_models
|
|
614
|
+
|
|
615
|
+
nil
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
# Reset state machine only - preserves rep distances
|
|
619
|
+
#
|
|
620
|
+
# This is used for control >= 0xA0 but < 0xC0 where we want
|
|
621
|
+
# to reset the state machine but preserve rep distances from
|
|
622
|
+
# the previous chunk.
|
|
623
|
+
#
|
|
624
|
+
# @return [void]
|
|
625
|
+
def reset_state_machine_only
|
|
626
|
+
# Reset state machine only (XZ Utils line 1053)
|
|
627
|
+
@state = SdkStateMachine.new
|
|
628
|
+
|
|
629
|
+
# Reset probability models (XZ Utils init_temporals for control >= 0xA0)
|
|
630
|
+
reset_models
|
|
631
|
+
|
|
632
|
+
nil
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# Finish state reset - called AFTER setting new input
|
|
636
|
+
#
|
|
637
|
+
# Resets the range decoder to read from the new input stream.
|
|
638
|
+
# This completes the state reset process started by prepare_state_reset.
|
|
639
|
+
#
|
|
640
|
+
# XZ Utils pattern (lzma_decoder.c:1034-1083):
|
|
641
|
+
# - rc_reset is called as part of lzma_decoder_reset
|
|
642
|
+
# - rc_reset sets range = UINT32_MAX, code = 0, init_bytes_left = 5
|
|
643
|
+
# - The 5 initialization bytes are read during the first normalize calls
|
|
644
|
+
#
|
|
645
|
+
# @return [void]
|
|
646
|
+
def finish_state_reset
|
|
647
|
+
# Reset range decoder (XZ Utils rc_reset)
|
|
648
|
+
# This reinitializes the range decoder for the new chunk
|
|
649
|
+
# The reset will read 5 bytes from the input when decode_stream starts
|
|
650
|
+
@range_decoder&.reset
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def reset_state_only
|
|
654
|
+
# Complete state reset requires both prepare and finish phases
|
|
655
|
+
prepare_state_reset
|
|
656
|
+
finish_state_reset
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
# Reset only the range decoder for next chunk
|
|
660
|
+
#
|
|
661
|
+
# XZ Utils pattern (lzma_decoder.c:1014-1017):
|
|
662
|
+
# When LZMA chunk ends (LZMA_STREAM_END), reset range decoder
|
|
663
|
+
# for next LZMA2 chunk, but preserve state and probability models.
|
|
664
|
+
#
|
|
665
|
+
# Note: This method is a no-op in our implementation because
|
|
666
|
+
# decode_stream creates a fresh RangeDecoder for each chunk.
|
|
667
|
+
# The range decoder initialization happens automatically when
|
|
668
|
+
# the new RangeDecoder is created with the new input.
|
|
669
|
+
#
|
|
670
|
+
# @return [void]
|
|
671
|
+
def reset_range_decoder
|
|
672
|
+
# No-op: RangeDecoder is created fresh in decode_stream
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# Set new input stream for chunked decoding
|
|
676
|
+
#
|
|
677
|
+
# For LZMA2, the range decoder is persistent across chunks and is
|
|
678
|
+
# reset separately via prepare_state_reset + finish_state_reset.
|
|
679
|
+
# This method just updates the input stream reference.
|
|
680
|
+
#
|
|
681
|
+
# @param new_input [IO] New input stream
|
|
682
|
+
# @return [void]
|
|
683
|
+
def set_input(new_input)
|
|
684
|
+
@input = new_input
|
|
685
|
+
|
|
686
|
+
# Create range decoder if it doesn't exist (first chunk)
|
|
687
|
+
if @range_decoder.nil?
|
|
688
|
+
@range_decoder = RangeDecoder.new(@input)
|
|
689
|
+
else
|
|
690
|
+
# Update the range decoder's input stream to the new input
|
|
691
|
+
# This is needed because RangeDecoder holds a reference to the stream
|
|
692
|
+
@range_decoder.update_stream(@input)
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Add uncompressed data to the dictionary
|
|
697
|
+
#
|
|
698
|
+
# XZ Utils pattern (lzma2_decoder.c:195, dict_write):
|
|
699
|
+
# - Copy uncompressed data to the dictionary as-is
|
|
700
|
+
# - Update dict_full to reflect new data
|
|
701
|
+
# - This allows subsequent compressed chunks to reference the data
|
|
702
|
+
#
|
|
703
|
+
# This is used by LZMA2 decoder for uncompressed chunks (control=0x1 or 0x2)
|
|
704
|
+
#
|
|
705
|
+
# @param data [String] Uncompressed data to add to dictionary
|
|
706
|
+
# @return [void]
|
|
707
|
+
def add_to_dictionary(data)
|
|
708
|
+
idx = @dict_pos
|
|
709
|
+
buf_end = @buf_end
|
|
710
|
+
data.each_byte do |byte|
|
|
711
|
+
@dict_buf.setbyte(idx, byte)
|
|
712
|
+
@pos += 1
|
|
713
|
+
idx += 1
|
|
714
|
+
idx = LZ_DICT_INIT_POS if idx >= buf_end
|
|
715
|
+
end
|
|
716
|
+
@dict_pos = idx
|
|
717
|
+
|
|
718
|
+
# Update dict_full to reflect new data
|
|
719
|
+
unless @has_wrapped
|
|
720
|
+
@dict_full = @pos - LZ_DICT_INIT_POS
|
|
721
|
+
if @dict_full >= @dict_size
|
|
722
|
+
@has_wrapped = true
|
|
723
|
+
@dict_full = @dict_size
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# No-op: circular buffer has fixed size, no compaction needed.
|
|
729
|
+
# Kept for API compatibility — LZMA2 decoder calls this between chunks.
|
|
730
|
+
def compact_buffer; end
|
|
731
|
+
|
|
732
|
+
# Set uncompressed size for chunked decoding
|
|
733
|
+
#
|
|
734
|
+
# XZ Utils pattern (lzma2_decoder.c:140-141):
|
|
735
|
+
# Pass the chunk's uncompressed_size to the LZMA decoder
|
|
736
|
+
# for each LZMA2 chunk.
|
|
737
|
+
#
|
|
738
|
+
# @param size [Integer] Uncompressed size for current chunk
|
|
739
|
+
# @param allow_eopm [Boolean] Whether to allow end-of-payload marker
|
|
740
|
+
# @return [void]
|
|
741
|
+
def set_uncompressed_size(size, allow_eopm: true)
|
|
742
|
+
@uncompressed_size = size
|
|
743
|
+
@allow_eopm = allow_eopm
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
private
|
|
747
|
+
|
|
748
|
+
# Read and parse LZMA header
|
|
749
|
+
#
|
|
750
|
+
# SDK header format:
|
|
751
|
+
# - Property byte: (lc + lp*9 + pb*45)
|
|
752
|
+
# - Dictionary size: 4 bytes little-endian
|
|
753
|
+
# - Uncompressed size: 8 bytes (0xFF for unknown size)
|
|
754
|
+
#
|
|
755
|
+
# @return [void]
|
|
756
|
+
# @raise [RuntimeError] If header is invalid
|
|
757
|
+
def read_header
|
|
758
|
+
# Property byte
|
|
759
|
+
props = @input.getbyte
|
|
760
|
+
raise Omnizip::DecompressionError, "Invalid LZMA header" if props.nil?
|
|
761
|
+
|
|
762
|
+
@lc = props % 9
|
|
763
|
+
remainder = props / 9
|
|
764
|
+
@lp = remainder % 5
|
|
765
|
+
@pb = remainder / 5
|
|
766
|
+
|
|
767
|
+
# Dictionary size (4 bytes, little-endian)
|
|
768
|
+
@dict_size = 0
|
|
769
|
+
4.times do |i|
|
|
770
|
+
byte = @input.getbyte
|
|
771
|
+
raise Omnizip::DecompressionError, "Incomplete header" if byte.nil?
|
|
772
|
+
|
|
773
|
+
@dict_size |= (byte << (i * 8))
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
# Clamp dict_size to prevent memory exhaustion
|
|
777
|
+
@dict_size = [[@dict_size, 1].max, MAX_DICT_SIZE].min
|
|
778
|
+
|
|
779
|
+
# Uncompressed size (8 bytes, little-endian)
|
|
780
|
+
@uncompressed_size = 0
|
|
781
|
+
8.times do |i|
|
|
782
|
+
byte = @input.getbyte
|
|
783
|
+
raise Omnizip::DecompressionError, "Incomplete header" if byte.nil?
|
|
784
|
+
|
|
785
|
+
@uncompressed_size |= (byte << (i * 8))
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
# Validate parameters
|
|
790
|
+
#
|
|
791
|
+
# @return [void]
|
|
792
|
+
# @raise [RuntimeError] If parameters are invalid
|
|
793
|
+
def validate_parameters
|
|
794
|
+
raise Omnizip::DecompressionError, "Invalid lc (#{@lc})" unless @lc.between?(0, 8)
|
|
795
|
+
raise Omnizip::DecompressionError, "Invalid lp (#{@lp})" unless @lp.between?(0, 4)
|
|
796
|
+
raise Omnizip::DecompressionError, "Invalid pb (#{@pb})" unless @pb.between?(0, 4)
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
# Initialize probability models
|
|
800
|
+
#
|
|
801
|
+
# SDK allocates models following exact structure from LzmaDec.c:
|
|
802
|
+
# - Literal models: (1 << (lc+lp)) contexts * 0x300 models each
|
|
803
|
+
# - Match models: NUM_STATES * pos_states models (where pos_states = 1 << pb)
|
|
804
|
+
# - Rep models: NUM_STATES models each
|
|
805
|
+
#
|
|
806
|
+
# Must match SdkEncoder's model structure exactly.
|
|
807
|
+
# CRITICAL: When pb changes, models must be recreated with new pos_states!
|
|
808
|
+
#
|
|
809
|
+
# @return [void]
|
|
810
|
+
def init_models
|
|
811
|
+
# Calculate pos_states based on current @pb value
|
|
812
|
+
pos_states = 1 << @pb
|
|
813
|
+
@pos_states = pos_states # Store for use in indexing
|
|
814
|
+
|
|
815
|
+
# Literal models: XZ Utils compact layout
|
|
816
|
+
# context_value ranges from 0 to literal_mask (using XZ Utils formula)
|
|
817
|
+
# base_offset = (context_value * 3) << lc
|
|
818
|
+
# For unmatched mode: max index = (max_context_value * 3) << lc + 256
|
|
819
|
+
# For matched mode: max index = (max_context_value * 3) << lc + offset + match_bit + symbol
|
|
820
|
+
# where offset, match_bit, and symbol can each be up to 0x100
|
|
821
|
+
# So max matched index = base_offset + 0x100 + 0x100 + 0x100 = base_offset + 0x300
|
|
822
|
+
# XZ Utils formula for literal_mask: (0x100 << lp) - (0x100 >> lc)
|
|
823
|
+
literal_mask = (0x100 << @lp) - (0x100 >> @lc)
|
|
824
|
+
max_context_value = literal_mask
|
|
825
|
+
max_base_offset = (max_context_value * 3) << @lc
|
|
826
|
+
max_model_index = max_base_offset + 0x300 # accommodate matched mode (offset + match_bit + symbol)
|
|
827
|
+
@literal_models = Array.new(max_model_index + 1) do
|
|
828
|
+
BitModel.new
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
# Match/rep decision models
|
|
832
|
+
# IMPORTANT: Use current pos_states, not POS_STATES_MAX
|
|
833
|
+
# This ensures models are correctly sized when pb changes
|
|
834
|
+
@is_match_models = Array.new(NUM_STATES * pos_states) do
|
|
835
|
+
BitModel.new
|
|
836
|
+
end
|
|
837
|
+
@is_rep_models = Array.new(NUM_STATES) { BitModel.new }
|
|
838
|
+
@is_rep0_models = Array.new(NUM_STATES) { BitModel.new }
|
|
839
|
+
@is_rep1_models = Array.new(NUM_STATES) { BitModel.new }
|
|
840
|
+
@is_rep2_models = Array.new(NUM_STATES) { BitModel.new }
|
|
841
|
+
@is_rep0_long_models = Array.new(NUM_STATES * pos_states) do
|
|
842
|
+
BitModel.new
|
|
843
|
+
end
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
# Initialize SDK coders
|
|
847
|
+
#
|
|
848
|
+
# @return [void]
|
|
849
|
+
def init_coders
|
|
850
|
+
@literal_decoder = LiteralDecoder.new
|
|
851
|
+
pos_states = 1 << @pb
|
|
852
|
+
@length_coder = LengthCoder.new(pos_states)
|
|
853
|
+
@rep_length_coder = LengthCoder.new(pos_states)
|
|
854
|
+
@distance_coder = DistanceCoder.new(NUM_LEN_TO_POS_STATES)
|
|
855
|
+
|
|
856
|
+
# Update probability model indices to match new pos_states
|
|
857
|
+
# This is critical when pb changes between chunks
|
|
858
|
+
@pos_states = pos_states
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
# Reset distance coder probability models
|
|
862
|
+
#
|
|
863
|
+
# Called during state reset (control >= 0xA0) to reset the distance
|
|
864
|
+
# coder's probability models to initial values. This matches XZ Utils
|
|
865
|
+
# behavior where init_temporals resets all probability models.
|
|
866
|
+
#
|
|
867
|
+
# @return [void]
|
|
868
|
+
def reset_distance_coder
|
|
869
|
+
@distance_coder.reset_models
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
# Decode a literal byte
|
|
873
|
+
#
|
|
874
|
+
# SDK decoding sequence (from LzmaDec.c):
|
|
875
|
+
# 1. Calculate literal state
|
|
876
|
+
# 2. Decode literal (matched or unmatched based on state)
|
|
877
|
+
# 3. Update state machine
|
|
878
|
+
# 4. Update dictionary and position
|
|
879
|
+
#
|
|
880
|
+
# XZ Utils dict_put pattern (from lz_decoder.h:270-276):
|
|
881
|
+
# dict->buf[dict->pos++] = byte;
|
|
882
|
+
# if (!dict->has_wrapped)
|
|
883
|
+
# dict->full = dict->pos - LZ_DICT_INIT_POS;
|
|
884
|
+
#
|
|
885
|
+
# @return [void]
|
|
886
|
+
def decode_literal
|
|
887
|
+
# Calculate literal state using SDK formula
|
|
888
|
+
lit_state = calculate_literal_state
|
|
889
|
+
|
|
890
|
+
# Decode literal (matched or unmatched)
|
|
891
|
+
# Check if dictionary has any valid bytes (XZ Utils: dict->full > 0)
|
|
892
|
+
if @state.use_matched_literal? && @dict_full.positive?
|
|
893
|
+
# Matched literal: use match byte from dictionary at distance rep0
|
|
894
|
+
# XZ Utils dict_get pattern: dict->buf[dict->pos - distance - 1]
|
|
895
|
+
match_byte = @dict_buf.getbyte(dict_index(@pos - @rep0 - 1))
|
|
896
|
+
byte = @literal_decoder.decode_matched(match_byte, lit_state, @lc,
|
|
897
|
+
@range_decoder, @literal_models)
|
|
898
|
+
else
|
|
899
|
+
# Unmatched literal: simple 8-bit decoding
|
|
900
|
+
byte = @literal_decoder.decode_unmatched(lit_state, @lc,
|
|
901
|
+
@range_decoder, @literal_models)
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
# Update state and dictionary
|
|
905
|
+
# XZ Utils dict_put pattern:
|
|
906
|
+
# dict->buf[dict->pos++] = byte;
|
|
907
|
+
# if (!dict->has_wrapped)
|
|
908
|
+
# dict->full = dict->pos - LZ_DICT_INIT_POS;
|
|
909
|
+
@state.update_literal
|
|
910
|
+
|
|
911
|
+
# Write to dictionary buffer at current position
|
|
912
|
+
# XZ Utils dict_put pattern: dict->buf[dict->pos++] = byte;
|
|
913
|
+
@dict_buf.setbyte(@dict_pos, byte)
|
|
914
|
+
@pos += 1
|
|
915
|
+
@dict_pos += 1
|
|
916
|
+
@dict_pos = LZ_DICT_INIT_POS if @dict_pos >= @buf_end
|
|
917
|
+
|
|
918
|
+
# Update dict_full (XZ Utils pattern)
|
|
919
|
+
# When dict_full reaches dict_size, the dictionary is full
|
|
920
|
+
# After that, dict_full stays at dict_size and has_wrapped = true
|
|
921
|
+
unless @has_wrapped
|
|
922
|
+
@dict_full = @pos - LZ_DICT_INIT_POS
|
|
923
|
+
# Check if we've reached the maximum dictionary size
|
|
924
|
+
if @dict_full >= @dict_size
|
|
925
|
+
@has_wrapped = true
|
|
926
|
+
@dict_full = @dict_size
|
|
927
|
+
end
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
# Track bytes decoded in this chunk (for match length limiting)
|
|
931
|
+
# IMPORTANT: Always increment this, even after dictionary wraps!
|
|
932
|
+
# This is needed for correct match length limiting when @uncompressed_size is set.
|
|
933
|
+
# XZ Utils uses dict.limit for this, but we use @chunk_bytes_decoded.
|
|
934
|
+
if @uncompressed_size != 0xFFFFFFFFFFFFFFFF
|
|
935
|
+
@chunk_bytes_decoded += 1
|
|
936
|
+
end
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
# Decode a match
|
|
940
|
+
#
|
|
941
|
+
# SDK decoding sequence:
|
|
942
|
+
# 1. Decode is_rep bit
|
|
943
|
+
# 2. If regular match:
|
|
944
|
+
# - Decode match length using length coder
|
|
945
|
+
# - Decode match distance using distance coder
|
|
946
|
+
# 3. If rep match:
|
|
947
|
+
# - Decode which rep distance to use (rep0/1/2/3)
|
|
948
|
+
# - Decode rep match length
|
|
949
|
+
# 4. Check for EOS marker
|
|
950
|
+
# 5. Copy matched data from dictionary
|
|
951
|
+
# 6. Update state machine and rep distances
|
|
952
|
+
# 7. Update dictionary and position
|
|
953
|
+
#
|
|
954
|
+
# @return [Boolean] True if EOS marker detected, false otherwise
|
|
955
|
+
def decode_match
|
|
956
|
+
pos_state = @pos & @pb_mask
|
|
957
|
+
|
|
958
|
+
# Decode is_rep bit
|
|
959
|
+
is_rep_model = @is_rep_models[@state.value]
|
|
960
|
+
is_rep = @range_decoder.decode_bit(is_rep_model)
|
|
961
|
+
|
|
962
|
+
if is_rep.zero?
|
|
963
|
+
# Regular match (not rep)
|
|
964
|
+
# Return result from decode_regular_match (true if EOS marker detected)
|
|
965
|
+
return true if decode_regular_match(pos_state)
|
|
966
|
+
else
|
|
967
|
+
# Rep match - decode which rep distance to use
|
|
968
|
+
decode_rep_match(pos_state)
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
false # No EOS marker detected
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
# Decode a regular (non-rep) match
|
|
975
|
+
#
|
|
976
|
+
# XZ Utils dict_repeat pattern (from lz_decoder.h:203-263):
|
|
977
|
+
# - Validate distance: dict->full > distance
|
|
978
|
+
# - Calculate back = dict->pos - distance - 1
|
|
979
|
+
# - If distance >= dict->pos: back += dict->size - LZ_DICT_REPEAT_MAX
|
|
980
|
+
# - Copy bytes from back position
|
|
981
|
+
# - Update dict->full if !has_wrapped
|
|
982
|
+
#
|
|
983
|
+
# @param pos_state [Integer] Position state
|
|
984
|
+
# @return [Boolean] True if EOS marker detected, false otherwise
|
|
985
|
+
def decode_regular_match(pos_state)
|
|
986
|
+
# Decode match length
|
|
987
|
+
length_encoded = @length_coder.decode(@range_decoder, pos_state)
|
|
988
|
+
length = length_encoded + MATCH_LEN_MIN
|
|
989
|
+
|
|
990
|
+
# Calculate length state for distance decoding
|
|
991
|
+
# XZ Utils formula (from lzma_common.h get_dist_state macro):
|
|
992
|
+
# ((len) < DIST_STATES + MATCH_LEN_MIN ? (len) - MATCH_LEN_MIN : DIST_STATES - 1)
|
|
993
|
+
# This gives: len=2→0, len=3→1, len=4→2, len=5→3, len=6+→3
|
|
994
|
+
len_state = if length < NUM_LEN_TO_POS_STATES + MATCH_LEN_MIN
|
|
995
|
+
length - MATCH_LEN_MIN
|
|
996
|
+
else
|
|
997
|
+
NUM_LEN_TO_POS_STATES - 1
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
# Decode match distance
|
|
1001
|
+
# XZ Utils stores distance in rep0 without +1
|
|
1002
|
+
# The distance coder returns 0-based distance
|
|
1003
|
+
rep0 = @distance_coder.decode(@range_decoder, len_state)
|
|
1004
|
+
|
|
1005
|
+
# Check for SDK EOS marker FIRST (before validation)
|
|
1006
|
+
# EOS marker: rep0 == UINT32_MAX (0xFFFFFFFF)
|
|
1007
|
+
# XZ Utils checks: if (rep0 == UINT32_MAX) goto eopm;
|
|
1008
|
+
# EOPM is only allowed if @allow_eopm is true OR uncompressed_size is unknown
|
|
1009
|
+
# Reference: XZ Utils lzma_decoder.c:697-705, 874-888
|
|
1010
|
+
if rep0 == 0xFFFFFFFF
|
|
1011
|
+
if @allow_eopm || @uncompressed_size == 0xFFFFFFFFFFFFFFFF
|
|
1012
|
+
# XZ Utils pattern after detecting EOPM:
|
|
1013
|
+
# 1. Normalize range decoder (may read more input bytes)
|
|
1014
|
+
# 2. Check if range decoder is finished (code == 0)
|
|
1015
|
+
# Reference: lzma_decoder.c:881-887 (SEQ_EOPM case)
|
|
1016
|
+
@range_decoder.normalize
|
|
1017
|
+
|
|
1018
|
+
# Check if range decoder is finished (code == 0)
|
|
1019
|
+
unless @range_decoder.code.zero?
|
|
1020
|
+
raise Omnizip::DecompressionError,
|
|
1021
|
+
"EOPM detected but range decoder not finished (code=#{@range_decoder.code}). Corrupted stream."
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
return true # EOS marker detected, stop decoding
|
|
1025
|
+
else
|
|
1026
|
+
raise Omnizip::DecompressionError,
|
|
1027
|
+
"End-of-payload marker (EOPM) detected but not allowed (LZMA2 streams cannot have EOPM)"
|
|
1028
|
+
end
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
# Validate distance: ensure we have enough bytes in the buffer.
|
|
1032
|
+
# In this linear buffer model, @pos grows unbounded across chunks while
|
|
1033
|
+
# @dict_full is clamped at @dict_size. Use actual bytes written for validation.
|
|
1034
|
+
actual_bytes_written = @pos - LZ_DICT_INIT_POS
|
|
1035
|
+
unless actual_bytes_written > rep0
|
|
1036
|
+
raise Omnizip::DecompressionError,
|
|
1037
|
+
"Invalid distance: #{rep0} (bytes_written: #{actual_bytes_written})"
|
|
1038
|
+
end
|
|
1039
|
+
|
|
1040
|
+
# IMPORTANT: Limit match length to not exceed uncompressed_size
|
|
1041
|
+
# XZ Utils handles this by setting dict.limit and checking before each write
|
|
1042
|
+
# We need to ensure we don't exceed the target size
|
|
1043
|
+
if @uncompressed_size != 0xFFFFFFFFFFFFFFFF
|
|
1044
|
+
# Calculate how many bytes we can still decode in THIS chunk
|
|
1045
|
+
# @chunk_bytes_decoded is the bytes decoded in this chunk (starts from 0)
|
|
1046
|
+
# @uncompressed_size is the target for THIS chunk (not cumulative)
|
|
1047
|
+
remaining = @uncompressed_size - @chunk_bytes_decoded
|
|
1048
|
+
if length > remaining
|
|
1049
|
+
length = remaining
|
|
1050
|
+
end
|
|
1051
|
+
end
|
|
1052
|
+
|
|
1053
|
+
# Copy matched data from dictionary using XZ Utils dict_repeat pattern
|
|
1054
|
+
# See lz_decoder.h:211-213:
|
|
1055
|
+
# back = dict->pos - distance - 1;
|
|
1056
|
+
# if (distance >= dict->pos)
|
|
1057
|
+
# back += dict->size - LZ_DICT_REPEAT_MAX;
|
|
1058
|
+
#
|
|
1059
|
+
# Note: dict->pos in XZ Utils is the actual data position (same as our @dict_full)
|
|
1060
|
+
# Our @pos includes the LZ_DICT_INIT_POS offset, so we use @dict_full for calculations
|
|
1061
|
+
#
|
|
1062
|
+
# dict->size in XZ Utils = dict_size + 2 * LZ_DICT_REPEAT_MAX
|
|
1063
|
+
# Our dict_buf size = @dict_size + LZ_DICT_INIT_POS = @dict_size + 2 * LZ_DICT_REPEAT_MAX
|
|
1064
|
+
# Linear buffer: use @pos directly for back reference
|
|
1065
|
+
# @pos always points to the next write position, so @pos - rep0 - 1
|
|
1066
|
+
# gives the correct source position for the match copy
|
|
1067
|
+
buffer_back = @pos - rep0 - 1
|
|
1068
|
+
|
|
1069
|
+
# Copy bytes from dictionary and extend buffer as needed
|
|
1070
|
+
# XZ Utils dict_repeat pattern: dict->buf[dict->pos++] = dict->buf[back++]
|
|
1071
|
+
src_idx = dict_index(buffer_back)
|
|
1072
|
+
dst_idx = @dict_pos
|
|
1073
|
+
buf_end = @buf_end
|
|
1074
|
+
length.times do
|
|
1075
|
+
byte = @dict_buf.getbyte(src_idx)
|
|
1076
|
+
@dict_buf.setbyte(dst_idx, byte)
|
|
1077
|
+
src_idx += 1
|
|
1078
|
+
src_idx = LZ_DICT_INIT_POS if src_idx >= buf_end
|
|
1079
|
+
dst_idx += 1
|
|
1080
|
+
dst_idx = LZ_DICT_INIT_POS if dst_idx >= buf_end
|
|
1081
|
+
end
|
|
1082
|
+
@dict_pos = dst_idx
|
|
1083
|
+
|
|
1084
|
+
# Update state and position
|
|
1085
|
+
@state.update_match
|
|
1086
|
+
@pos += length
|
|
1087
|
+
|
|
1088
|
+
# Update dict_full (XZ Utils pattern)
|
|
1089
|
+
# When dict_full reaches dict_size, the dictionary is full
|
|
1090
|
+
# After that, dict_full stays at dict_size and has_wrapped = true
|
|
1091
|
+
unless @has_wrapped
|
|
1092
|
+
@dict_full = @pos - LZ_DICT_INIT_POS
|
|
1093
|
+
# Check if we've reached the maximum dictionary size
|
|
1094
|
+
if @dict_full >= @dict_size
|
|
1095
|
+
@has_wrapped = true
|
|
1096
|
+
@dict_full = @dict_size
|
|
1097
|
+
end
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
# Track bytes decoded in this chunk (for match length limiting)
|
|
1101
|
+
# IMPORTANT: Increment by length for match copies (multiple bytes at once)
|
|
1102
|
+
# This is needed for correct match length limiting when @uncompressed_size is set.
|
|
1103
|
+
# XZ Utils uses dict.limit for this, but we use @chunk_bytes_decoded.
|
|
1104
|
+
if @uncompressed_size != 0xFFFFFFFFFFFFFFFF
|
|
1105
|
+
@chunk_bytes_decoded += length
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
# Update rep distances - rotate and set new rep0
|
|
1109
|
+
# SDK rotation: rep3←rep2, rep2←rep1, rep1←rep0, rep0←rep0
|
|
1110
|
+
# XZ Utils stores the actual distance in rep0 (no +1)
|
|
1111
|
+
@rep3 = @rep2
|
|
1112
|
+
@rep2 = @rep1
|
|
1113
|
+
@rep1 = @rep0
|
|
1114
|
+
@rep0 = rep0
|
|
1115
|
+
|
|
1116
|
+
false # Not EOS, continue decoding
|
|
1117
|
+
end
|
|
1118
|
+
|
|
1119
|
+
# Decode a rep match
|
|
1120
|
+
#
|
|
1121
|
+
# SDK rep match decoding (from XZ Utils lzma_decoder.c):
|
|
1122
|
+
# - is_rep0: Use rep0
|
|
1123
|
+
# - is_rep0_long=0: Short rep (length=1, don't rotate)
|
|
1124
|
+
# - is_rep0_long=1: Long rep (decode length, keep rep0)
|
|
1125
|
+
# - is_rep1: Use rep1, rotate rep1→rep0
|
|
1126
|
+
# - is_rep2: Use rep2, rotate rep2→rep0
|
|
1127
|
+
# - Otherwise: Use rep3, rotate rep3→rep0
|
|
1128
|
+
# After rotation, rep0 always contains the actual distance to use
|
|
1129
|
+
#
|
|
1130
|
+
# @param pos_state [Integer] Position state
|
|
1131
|
+
# @return [Boolean] Always false (rep matches are never EOS)
|
|
1132
|
+
def decode_rep_match(pos_state)
|
|
1133
|
+
# Decode which rep distance to use
|
|
1134
|
+
is_rep0 = @range_decoder.decode_bit(@is_rep0_models[@state.value])
|
|
1135
|
+
|
|
1136
|
+
if is_rep0.zero?
|
|
1137
|
+
# Use rep0
|
|
1138
|
+
# XZ Utils: is_rep0_long[state][pos_state] where the array size is NUM_STATES * (1 << pb)
|
|
1139
|
+
is_rep0_long = @range_decoder.decode_bit(
|
|
1140
|
+
@is_rep0_long_models[(@state.value * @pb_shift) + pos_state],
|
|
1141
|
+
)
|
|
1142
|
+
|
|
1143
|
+
if is_rep0_long.zero?
|
|
1144
|
+
# Short rep (length=1)
|
|
1145
|
+
length = 1
|
|
1146
|
+
@state.update_short_rep
|
|
1147
|
+
else
|
|
1148
|
+
# Long rep with rep0
|
|
1149
|
+
length = @rep_length_coder.decode(@range_decoder,
|
|
1150
|
+
pos_state) + MATCH_LEN_MIN
|
|
1151
|
+
@state.update_rep
|
|
1152
|
+
end
|
|
1153
|
+
else
|
|
1154
|
+
# Not rep0, check rep1/rep2/rep3
|
|
1155
|
+
is_rep1 = @range_decoder.decode_bit(@is_rep1_models[@state.value])
|
|
1156
|
+
|
|
1157
|
+
if is_rep1.zero?
|
|
1158
|
+
# Use rep1 - XZ Utils pattern:
|
|
1159
|
+
# const uint32_t distance = rep1;
|
|
1160
|
+
# rep1 = rep0;
|
|
1161
|
+
# rep0 = distance;
|
|
1162
|
+
@rep1, @rep0 = @rep0, @rep1
|
|
1163
|
+
else
|
|
1164
|
+
# Not rep1, check rep2/rep3
|
|
1165
|
+
is_rep2 = @range_decoder.decode_bit(@is_rep2_models[@state.value])
|
|
1166
|
+
|
|
1167
|
+
if is_rep2.zero?
|
|
1168
|
+
# Use rep2 - XZ Utils pattern:
|
|
1169
|
+
# const uint32_t distance = rep2;
|
|
1170
|
+
# rep2 = rep1;
|
|
1171
|
+
# rep1 = rep0;
|
|
1172
|
+
# rep0 = distance;
|
|
1173
|
+
distance = @rep2
|
|
1174
|
+
else
|
|
1175
|
+
# Use rep3 - XZ Utils pattern:
|
|
1176
|
+
# const uint32_t distance = rep3;
|
|
1177
|
+
# rep3 = rep2;
|
|
1178
|
+
# rep2 = rep1;
|
|
1179
|
+
# rep1 = rep0;
|
|
1180
|
+
# rep0 = distance;
|
|
1181
|
+
distance = @rep3
|
|
1182
|
+
@rep3 = @rep2
|
|
1183
|
+
end
|
|
1184
|
+
@rep2 = @rep1
|
|
1185
|
+
@rep1 = @rep0
|
|
1186
|
+
@rep0 = distance
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1189
|
+
# Decode length for rep1/2/3
|
|
1190
|
+
length = @rep_length_coder.decode(@range_decoder,
|
|
1191
|
+
pos_state) + MATCH_LEN_MIN
|
|
1192
|
+
@state.update_rep
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
# After rotation, rep0 always contains the distance to use
|
|
1196
|
+
# XZ Utils stores distances without +1 offset
|
|
1197
|
+
distance = @rep0
|
|
1198
|
+
|
|
1199
|
+
# Validate distance: ensure we have enough bytes in the buffer.
|
|
1200
|
+
# In this linear buffer model, @pos grows unbounded across chunks while
|
|
1201
|
+
# @dict_full is clamped at @dict_size. Use actual bytes written for validation.
|
|
1202
|
+
actual_bytes_written = @pos - LZ_DICT_INIT_POS
|
|
1203
|
+
unless actual_bytes_written > distance
|
|
1204
|
+
raise Omnizip::DecompressionError, "Invalid rep distance: #{distance} (bytes_written: #{actual_bytes_written})"
|
|
1205
|
+
end
|
|
1206
|
+
|
|
1207
|
+
# IMPORTANT: Limit match length to not exceed uncompressed_size
|
|
1208
|
+
# XZ Utils handles this by setting dict.limit and checking before each write
|
|
1209
|
+
# We need to ensure we don't exceed the target size
|
|
1210
|
+
if @uncompressed_size != 0xFFFFFFFFFFFFFFFF
|
|
1211
|
+
# Calculate how many bytes we can still decode in THIS chunk
|
|
1212
|
+
# @chunk_bytes_decoded is the bytes decoded in this chunk (starts from 0)
|
|
1213
|
+
# @uncompressed_size is the target for THIS chunk (not cumulative)
|
|
1214
|
+
remaining = @uncompressed_size - @chunk_bytes_decoded
|
|
1215
|
+
if length > remaining
|
|
1216
|
+
length = remaining
|
|
1217
|
+
end
|
|
1218
|
+
end
|
|
1219
|
+
|
|
1220
|
+
# Copy matched data from dictionary using XZ Utils dict_repeat pattern
|
|
1221
|
+
# back = dict->pos - distance - 1;
|
|
1222
|
+
# if (distance >= dict->pos) back += dict->size - LZ_DICT_REPEAT_MAX;
|
|
1223
|
+
#
|
|
1224
|
+
# Note: dict->pos in XZ Utils is the actual data position (same as our @dict_full)
|
|
1225
|
+
# Our @pos includes the LZ_DICT_INIT_POS offset, so we use @dict_full for calculations
|
|
1226
|
+
#
|
|
1227
|
+
# dict->size in XZ Utils = dict_size + 2 * LZ_DICT_REPEAT_MAX
|
|
1228
|
+
# Our dict_buf size = @dict_size + LZ_DICT_INIT_POS = @dict_size + 2 * LZ_DICT_REPEAT_MAX
|
|
1229
|
+
# Linear buffer: use @pos directly for back reference
|
|
1230
|
+
buffer_back = @pos - distance - 1
|
|
1231
|
+
|
|
1232
|
+
# Copy bytes from dictionary
|
|
1233
|
+
# XZ Utils dict_repeat pattern: dict->buf[dict->pos++] = dict->buf[back++]
|
|
1234
|
+
src_idx = dict_index(buffer_back)
|
|
1235
|
+
dst_idx = @dict_pos
|
|
1236
|
+
buf_end = @buf_end
|
|
1237
|
+
length.times do
|
|
1238
|
+
byte = @dict_buf.getbyte(src_idx)
|
|
1239
|
+
@dict_buf.setbyte(dst_idx, byte)
|
|
1240
|
+
src_idx += 1
|
|
1241
|
+
src_idx = LZ_DICT_INIT_POS if src_idx >= buf_end
|
|
1242
|
+
dst_idx += 1
|
|
1243
|
+
dst_idx = LZ_DICT_INIT_POS if dst_idx >= buf_end
|
|
1244
|
+
end
|
|
1245
|
+
@dict_pos = dst_idx
|
|
1246
|
+
|
|
1247
|
+
# Update position
|
|
1248
|
+
@pos += length
|
|
1249
|
+
|
|
1250
|
+
# Update dict_full (XZ Utils pattern)
|
|
1251
|
+
# When dict_full reaches dict_size, the dictionary is full
|
|
1252
|
+
# After that, dict_full stays at dict_size and has_wrapped = true
|
|
1253
|
+
unless @has_wrapped
|
|
1254
|
+
@dict_full = @pos - LZ_DICT_INIT_POS
|
|
1255
|
+
# Check if we've reached the maximum dictionary size
|
|
1256
|
+
if @dict_full >= @dict_size
|
|
1257
|
+
@has_wrapped = true
|
|
1258
|
+
@dict_full = @dict_size
|
|
1259
|
+
end
|
|
1260
|
+
end
|
|
1261
|
+
|
|
1262
|
+
# Track bytes decoded in this chunk (for match length limiting)
|
|
1263
|
+
# IMPORTANT: Increment by length for match copies (multiple bytes at once)
|
|
1264
|
+
# This is needed for correct match length limiting when @uncompressed_size is set.
|
|
1265
|
+
# XZ Utils uses dict.limit for this, but we use @chunk_bytes_decoded.
|
|
1266
|
+
if @uncompressed_size != 0xFFFFFFFFFFFFFFFF
|
|
1267
|
+
@chunk_bytes_decoded += length
|
|
1268
|
+
end
|
|
1269
|
+
|
|
1270
|
+
false # Rep matches are never EOS
|
|
1271
|
+
end
|
|
1272
|
+
|
|
1273
|
+
# Calculate literal state index
|
|
1274
|
+
# XZ Utils literal_subcoder formula (from lzma_common.h:141-143):
|
|
1275
|
+
# ((probs) + 3 * (((((pos) << 8) + (prev_byte)) & (literal_mask)) << (lc)))
|
|
1276
|
+
# where literal_mask = (1 << (lc + lp)) - 1
|
|
1277
|
+
#
|
|
1278
|
+
# The key insight is that (pos << 8) + prev_byte is computed FIRST,
|
|
1279
|
+
# then masked, THEN shifted by lc. This is different from our old formula
|
|
1280
|
+
# which added pos_part and prev_part separately.
|
|
1281
|
+
#
|
|
1282
|
+
# IMPORTANT: The literal_subcoder macro returns:
|
|
1283
|
+
# probs + 3 * context_value_shifted
|
|
1284
|
+
# where context_value_shifted = context_value << lc
|
|
1285
|
+
#
|
|
1286
|
+
# For our implementation, we return context_value (unshifted) so that
|
|
1287
|
+
# the literal decoder can calculate the correct offset: 3 * context_value
|
|
1288
|
+
#
|
|
1289
|
+
# This creates (1 << (lc + lp)) unique contexts
|
|
1290
|
+
#
|
|
1291
|
+
# @return [Integer] Literal context value (unshifted, 0-7 for lc=3)
|
|
1292
|
+
def calculate_literal_state
|
|
1293
|
+
# XZ Utils dict_get0 pattern: dict->buf[dict->pos - 1]
|
|
1294
|
+
# For array buffer, use @pos - 1 as index
|
|
1295
|
+
prev_idx = @dict_pos == LZ_DICT_INIT_POS ? @buf_end - 1 : @dict_pos - 1
|
|
1296
|
+
prev_byte = @dict_full.positive? ? @dict_buf.getbyte(prev_idx) : 0
|
|
1297
|
+
|
|
1298
|
+
# Safeguard: if prev_byte is nil, use 0
|
|
1299
|
+
prev_byte = 0 if prev_byte.nil?
|
|
1300
|
+
|
|
1301
|
+
# Combine current output position and prev_byte, then apply cached mask.
|
|
1302
|
+
# XZ Utils uses dict.pos (continuous write position) for this calculation.
|
|
1303
|
+
# We use @pos - LZ_DICT_INIT_POS (total bytes decoded) instead of @dict_full,
|
|
1304
|
+
# because @dict_full is clamped at @dict_size after the circular buffer wraps,
|
|
1305
|
+
# which would freeze the position-dependent literal model selection.
|
|
1306
|
+
output_pos = @pos - LZ_DICT_INIT_POS
|
|
1307
|
+
((output_pos << 8) + prev_byte) & @literal_mask
|
|
1308
|
+
end
|
|
1309
|
+
end
|
|
1310
|
+
end
|
|
1311
|
+
end
|