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.
Files changed (632) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +32 -0
  4. data/.rubocop_todo.yml +293 -0
  5. data/CHANGELOG.md +1647 -0
  6. data/CONTEXT.md +68 -0
  7. data/COPYING +502 -0
  8. data/Gemfile +19 -0
  9. data/LICENSE +12 -0
  10. data/README.adoc +1327 -0
  11. data/Rakefile +15 -0
  12. data/TODO.refactor/00-overview.md +67 -0
  13. data/TODO.refactor/01-registry-base-class.md +81 -0
  14. data/TODO.refactor/02-error-hierarchy.md +57 -0
  15. data/TODO.refactor/03-autoload-entry-point.md +69 -0
  16. data/TODO.refactor/04-filter-base-consolidation.md +49 -0
  17. data/TODO.refactor/05-send-private-methods.md +64 -0
  18. data/TODO.refactor/06-instance-variable-access.md +69 -0
  19. data/TODO.refactor/07-respond-to-replacement.md +237 -0
  20. data/TODO.refactor/08-spec-doubles.md +27 -0
  21. data/TODO.refactor/09-cli-shared-module.md +35 -0
  22. data/TODO.refactor/10-format-detector-ocp.md +34 -0
  23. data/TODO.refactor/11-convenience-decoupling.md +38 -0
  24. data/TODO.refactor/12-thread-safety.md +8 -0
  25. data/TODO.refactor/13-lutaml-model-migration.md +158 -0
  26. data/TODO.refactor/14-add-missing-specs.md +25 -0
  27. data/benchmark/README.md +260 -0
  28. data/benchmark/benchmark_suite.rb +125 -0
  29. data/benchmark/compression_bench.rb +181 -0
  30. data/benchmark/filter_bench.rb +180 -0
  31. data/benchmark/models/benchmark_result.rb +59 -0
  32. data/benchmark/models/comparison_result.rb +69 -0
  33. data/benchmark/profile_suite.rb +167 -0
  34. data/benchmark/reporter.rb +150 -0
  35. data/benchmark/run_benchmarks.rb +66 -0
  36. data/benchmark/test_data.rb +137 -0
  37. data/config/formats/rar3_spec.yml +92 -0
  38. data/config/formats/rar5_spec.yml +102 -0
  39. data/docs/.github/workflows/docs.yml +142 -0
  40. data/docs/.gitignore +21 -0
  41. data/docs/.lychee.toml +67 -0
  42. data/docs/Gemfile +13 -0
  43. data/docs/RAR_WRITE_SUPPORT.md +26 -0
  44. data/docs/README.md +101 -0
  45. data/docs/_config.yml +112 -0
  46. data/docs/assets/logo.svg +1 -0
  47. data/docs/assets/omnizip-logo.pdf +1540 -11
  48. data/docs/comparison/feature-matrix.adoc +694 -0
  49. data/docs/comparison/index.adoc +113 -0
  50. data/docs/comparison/vs-7zip.adoc +309 -0
  51. data/docs/comparison/vs-peazip.adoc +77 -0
  52. data/docs/comparison/vs-rubyzip.adoc +342 -0
  53. data/docs/comparison/vs-winrar.adoc +100 -0
  54. data/docs/compatibility.adoc +579 -0
  55. data/docs/concepts/index.adoc +129 -0
  56. data/docs/developer/architecture.adoc +256 -0
  57. data/docs/developer/contributing.adoc +158 -0
  58. data/docs/developer/index.adoc +25 -0
  59. data/docs/developer/testing.adoc +212 -0
  60. data/docs/getting-started/basic-usage.adoc +263 -0
  61. data/docs/getting-started/index.adoc +42 -0
  62. data/docs/getting-started/installation.adoc +138 -0
  63. data/docs/getting-started/quick-start.adoc +183 -0
  64. data/docs/getting-started/your-first-archive.adoc +219 -0
  65. data/docs/guides/advanced-features/encryption.adoc +277 -0
  66. data/docs/guides/advanced-features/index.adoc +59 -0
  67. data/docs/guides/advanced-features/parallel-processing.adoc +144 -0
  68. data/docs/guides/advanced-features/progress-tracking.adoc +124 -0
  69. data/docs/guides/advanced-features/streaming.adoc +142 -0
  70. data/docs/guides/archive-formats/gzip-format.adoc +105 -0
  71. data/docs/guides/archive-formats/index.adoc +160 -0
  72. data/docs/guides/archive-formats/ole-format.adoc +151 -0
  73. data/docs/guides/archive-formats/rar-format.adoc +104 -0
  74. data/docs/guides/archive-formats/rar5.adoc +521 -0
  75. data/docs/guides/archive-formats/rpm-format.adoc +108 -0
  76. data/docs/guides/archive-formats/seven-zip-format.adoc +35 -0
  77. data/docs/guides/archive-formats/tar-format.adoc +106 -0
  78. data/docs/guides/archive-formats/xz-format.adoc +118 -0
  79. data/docs/guides/archive-formats/zip-format.adoc +35 -0
  80. data/docs/guides/compression-algorithms/bzip2.adoc +114 -0
  81. data/docs/guides/compression-algorithms/deflate.adoc +313 -0
  82. data/docs/guides/compression-algorithms/index.adoc +190 -0
  83. data/docs/guides/compression-algorithms/lzma.adoc +394 -0
  84. data/docs/guides/compression-algorithms/lzma2.adoc +244 -0
  85. data/docs/guides/compression-algorithms/ppmd.adoc +313 -0
  86. data/docs/guides/compression-algorithms/zstandard.adoc +296 -0
  87. data/docs/guides/creating-archives.adoc +348 -0
  88. data/docs/guides/extracting-archives.adoc +53 -0
  89. data/docs/guides/format-conversion.adoc +64 -0
  90. data/docs/guides/index.adoc +49 -0
  91. data/docs/guides/migration-rubyzip.adoc +217 -0
  92. data/docs/guides/parity-archives.adoc +605 -0
  93. data/docs/guides/performance-tuning.adoc +88 -0
  94. data/docs/index.adoc +228 -0
  95. data/docs/lychee.toml +67 -0
  96. data/docs/reference/api/overview.adoc +187 -0
  97. data/docs/reference/cli/compress-command.adoc +114 -0
  98. data/docs/reference/cli/overview.adoc +140 -0
  99. data/docs/reference/index.adoc +26 -0
  100. data/docs/resources/faq.adoc +185 -0
  101. data/docs/resources/quick-reference.adoc +222 -0
  102. data/docs/rust-backend.md +0 -0
  103. data/docs/troubleshooting/index.adoc +207 -0
  104. data/docs/xar_format.md +216 -0
  105. data/examples/api_comparison.rb +205 -0
  106. data/examples/deflate64_example.rb +96 -0
  107. data/examples/par2_demo.rb +121 -0
  108. data/examples/quick_start_native.rb +150 -0
  109. data/examples/quick_start_rubyzip.rb +115 -0
  110. data/examples/rubyzip_compatibility_demo.rb +194 -0
  111. data/exe/omnizip +27 -0
  112. data/lib/omnizip/algorithm.rb +176 -0
  113. data/lib/omnizip/algorithm_registry.rb +65 -0
  114. data/lib/omnizip/algorithms/.keep +0 -0
  115. data/lib/omnizip/algorithms/bzip2/bwt.rb +236 -0
  116. data/lib/omnizip/algorithms/bzip2/bz2.rb +687 -0
  117. data/lib/omnizip/algorithms/bzip2/rle.rb +146 -0
  118. data/lib/omnizip/algorithms/bzip2.rb +108 -0
  119. data/lib/omnizip/algorithms/deflate/constants.rb +28 -0
  120. data/lib/omnizip/algorithms/deflate/decoder.rb +37 -0
  121. data/lib/omnizip/algorithms/deflate/encoder.rb +45 -0
  122. data/lib/omnizip/algorithms/deflate.rb +135 -0
  123. data/lib/omnizip/algorithms/deflate64/constants.rb +45 -0
  124. data/lib/omnizip/algorithms/deflate64/decoder.rb +151 -0
  125. data/lib/omnizip/algorithms/deflate64/encoder.rb +95 -0
  126. data/lib/omnizip/algorithms/deflate64/huffman_coder.rb +352 -0
  127. data/lib/omnizip/algorithms/deflate64/lz77_encoder.rb +140 -0
  128. data/lib/omnizip/algorithms/deflate64.rb +118 -0
  129. data/lib/omnizip/algorithms/lzma/bit_model.rb +120 -0
  130. data/lib/omnizip/algorithms/lzma/constants.rb +112 -0
  131. data/lib/omnizip/algorithms/lzma/decoder.rb +146 -0
  132. data/lib/omnizip/algorithms/lzma/dictionary.rb +70 -0
  133. data/lib/omnizip/algorithms/lzma/distance_coder.rb +326 -0
  134. data/lib/omnizip/algorithms/lzma/encoder.rb +139 -0
  135. data/lib/omnizip/algorithms/lzma/length_coder.rb +172 -0
  136. data/lib/omnizip/algorithms/lzma/literal_decoder.rb +204 -0
  137. data/lib/omnizip/algorithms/lzma/literal_encoder.rb +208 -0
  138. data/lib/omnizip/algorithms/lzma/lzip_decoder.rb +368 -0
  139. data/lib/omnizip/algorithms/lzma/lzma1_encoder.rb +105 -0
  140. data/lib/omnizip/algorithms/lzma/lzma_alone_decoder.rb +194 -0
  141. data/lib/omnizip/algorithms/lzma/lzma_state.rb +128 -0
  142. data/lib/omnizip/algorithms/lzma/match.rb +32 -0
  143. data/lib/omnizip/algorithms/lzma/match_finder.rb +233 -0
  144. data/lib/omnizip/algorithms/lzma/match_finder_config.rb +142 -0
  145. data/lib/omnizip/algorithms/lzma/match_finder_factory.rb +84 -0
  146. data/lib/omnizip/algorithms/lzma/optimal_encoder.rb +159 -0
  147. data/lib/omnizip/algorithms/lzma/probability_models.rb +72 -0
  148. data/lib/omnizip/algorithms/lzma/range_coder.rb +82 -0
  149. data/lib/omnizip/algorithms/lzma/range_decoder.rb +274 -0
  150. data/lib/omnizip/algorithms/lzma/range_encoder.rb +202 -0
  151. data/lib/omnizip/algorithms/lzma/state.rb +127 -0
  152. data/lib/omnizip/algorithms/lzma/xz_buffered_range_encoder.rb +323 -0
  153. data/lib/omnizip/algorithms/lzma/xz_encoder.rb +420 -0
  154. data/lib/omnizip/algorithms/lzma/xz_encoder_fast.rb +640 -0
  155. data/lib/omnizip/algorithms/lzma/xz_match_finder_adapter.rb +224 -0
  156. data/lib/omnizip/algorithms/lzma/xz_price_calculator.rb +167 -0
  157. data/lib/omnizip/algorithms/lzma/xz_probability_models.rb +258 -0
  158. data/lib/omnizip/algorithms/lzma/xz_range_encoder.rb +223 -0
  159. data/lib/omnizip/algorithms/lzma/xz_range_encoder_exact.rb +314 -0
  160. data/lib/omnizip/algorithms/lzma/xz_state.rb +108 -0
  161. data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +1311 -0
  162. data/lib/omnizip/algorithms/lzma.rb +266 -0
  163. data/lib/omnizip/algorithms/lzma2/chunk_manager.rb +180 -0
  164. data/lib/omnizip/algorithms/lzma2/constants.rb +38 -0
  165. data/lib/omnizip/algorithms/lzma2/encoder.rb +145 -0
  166. data/lib/omnizip/algorithms/lzma2/lzma2_chunk.rb +165 -0
  167. data/lib/omnizip/algorithms/lzma2/properties.rb +179 -0
  168. data/lib/omnizip/algorithms/lzma2/simple_lzma2_encoder.rb +122 -0
  169. data/lib/omnizip/algorithms/lzma2/xz_encoder_adapter.rb +85 -0
  170. data/lib/omnizip/algorithms/lzma2.rb +133 -0
  171. data/lib/omnizip/algorithms/ppmd7/constants.rb +74 -0
  172. data/lib/omnizip/algorithms/ppmd7/context.rb +151 -0
  173. data/lib/omnizip/algorithms/ppmd7/decoder.rb +127 -0
  174. data/lib/omnizip/algorithms/ppmd7/encoder.rb +153 -0
  175. data/lib/omnizip/algorithms/ppmd7/model.rb +245 -0
  176. data/lib/omnizip/algorithms/ppmd7/symbol_state.rb +57 -0
  177. data/lib/omnizip/algorithms/ppmd7.rb +146 -0
  178. data/lib/omnizip/algorithms/ppmd8/constants.rb +61 -0
  179. data/lib/omnizip/algorithms/ppmd8/context.rb +32 -0
  180. data/lib/omnizip/algorithms/ppmd8/decoder.rb +104 -0
  181. data/lib/omnizip/algorithms/ppmd8/encoder.rb +135 -0
  182. data/lib/omnizip/algorithms/ppmd8/model.rb +246 -0
  183. data/lib/omnizip/algorithms/ppmd8/restoration_method.rb +78 -0
  184. data/lib/omnizip/algorithms/ppmd8.rb +122 -0
  185. data/lib/omnizip/algorithms/ppmd_base.rb +142 -0
  186. data/lib/omnizip/algorithms/registration.rb +28 -0
  187. data/lib/omnizip/algorithms/zstandard/constants.rb +158 -0
  188. data/lib/omnizip/algorithms/zstandard/decoder.rb +249 -0
  189. data/lib/omnizip/algorithms/zstandard/dictionary.rb +73 -0
  190. data/lib/omnizip/algorithms/zstandard/encoder.rb +294 -0
  191. data/lib/omnizip/algorithms/zstandard/frame/block.rb +143 -0
  192. data/lib/omnizip/algorithms/zstandard/frame/header.rb +233 -0
  193. data/lib/omnizip/algorithms/zstandard/frame.rb +33 -0
  194. data/lib/omnizip/algorithms/zstandard/fse/bitstream.rb +183 -0
  195. data/lib/omnizip/algorithms/zstandard/fse/encoder.rb +534 -0
  196. data/lib/omnizip/algorithms/zstandard/fse/interleaved.rb +92 -0
  197. data/lib/omnizip/algorithms/zstandard/fse/table.rb +188 -0
  198. data/lib/omnizip/algorithms/zstandard/fse/table_description.rb +203 -0
  199. data/lib/omnizip/algorithms/zstandard/fse.rb +53 -0
  200. data/lib/omnizip/algorithms/zstandard/huffman.rb +276 -0
  201. data/lib/omnizip/algorithms/zstandard/huffman_encoder.rb +327 -0
  202. data/lib/omnizip/algorithms/zstandard/ldm.rb +121 -0
  203. data/lib/omnizip/algorithms/zstandard/literals.rb +241 -0
  204. data/lib/omnizip/algorithms/zstandard/literals_encoder.rb +96 -0
  205. data/lib/omnizip/algorithms/zstandard/match_finder.rb +483 -0
  206. data/lib/omnizip/algorithms/zstandard/sequences.rb +350 -0
  207. data/lib/omnizip/algorithms/zstandard/sequences_encoder.rb +333 -0
  208. data/lib/omnizip/algorithms/zstandard/xxhash.rb +159 -0
  209. data/lib/omnizip/algorithms/zstandard.rb +210 -0
  210. data/lib/omnizip/algorithms.rb +35 -0
  211. data/lib/omnizip/archive/builder.rb +48 -0
  212. data/lib/omnizip/archive/reader_session.rb +67 -0
  213. data/lib/omnizip/archive.rb +86 -0
  214. data/lib/omnizip/archive_handler.rb +82 -0
  215. data/lib/omnizip/archive_handlers/cpio_handler.rb +47 -0
  216. data/lib/omnizip/archive_handlers/iso_handler.rb +54 -0
  217. data/lib/omnizip/archive_handlers/ole_handler.rb +37 -0
  218. data/lib/omnizip/archive_handlers/rar_handler.rb +96 -0
  219. data/lib/omnizip/archive_handlers/rpm_handler.rb +45 -0
  220. data/lib/omnizip/archive_handlers/seven_zip_handler.rb +89 -0
  221. data/lib/omnizip/archive_handlers/tar_handler.rb +75 -0
  222. data/lib/omnizip/archive_handlers/xar_handler.rb +49 -0
  223. data/lib/omnizip/archive_handlers/zip_handler.rb +115 -0
  224. data/lib/omnizip/archive_handlers.rb +18 -0
  225. data/lib/omnizip/backends.rb +160 -0
  226. data/lib/omnizip/buffer/memory_archive.rb +246 -0
  227. data/lib/omnizip/buffer/memory_extractor.rb +234 -0
  228. data/lib/omnizip/buffer/seven_zip_bridge.rb +171 -0
  229. data/lib/omnizip/buffer.rb +187 -0
  230. data/lib/omnizip/checksum_registry.rb +41 -0
  231. data/lib/omnizip/checksums/crc32.rb +101 -0
  232. data/lib/omnizip/checksums/crc64.rb +102 -0
  233. data/lib/omnizip/checksums/crc_base.rb +158 -0
  234. data/lib/omnizip/checksums/registration.rb +16 -0
  235. data/lib/omnizip/checksums/verifier.rb +130 -0
  236. data/lib/omnizip/checksums.rb +11 -0
  237. data/lib/omnizip/chunked/memory_manager.rb +194 -0
  238. data/lib/omnizip/chunked/reader.rb +78 -0
  239. data/lib/omnizip/chunked/writer.rb +123 -0
  240. data/lib/omnizip/chunked.rb +123 -0
  241. data/lib/omnizip/cli/output_formatter.rb +112 -0
  242. data/lib/omnizip/cli.rb +604 -0
  243. data/lib/omnizip/commands/.keep +0 -0
  244. data/lib/omnizip/commands/archive_create_command.rb +245 -0
  245. data/lib/omnizip/commands/archive_extract_command.rb +231 -0
  246. data/lib/omnizip/commands/archive_list_command.rb +204 -0
  247. data/lib/omnizip/commands/archive_repair_command.rb +129 -0
  248. data/lib/omnizip/commands/archive_verify_command.rb +115 -0
  249. data/lib/omnizip/commands/compress_command.rb +115 -0
  250. data/lib/omnizip/commands/decompress_command.rb +149 -0
  251. data/lib/omnizip/commands/list_command.rb +51 -0
  252. data/lib/omnizip/commands/metadata_command.rb +215 -0
  253. data/lib/omnizip/commands/parity_create_command.rb +114 -0
  254. data/lib/omnizip/commands/parity_repair_command.rb +120 -0
  255. data/lib/omnizip/commands/parity_verify_command.rb +122 -0
  256. data/lib/omnizip/commands/profile_list_command.rb +54 -0
  257. data/lib/omnizip/commands/profile_show_command.rb +44 -0
  258. data/lib/omnizip/commands.rb +21 -0
  259. data/lib/omnizip/convenience.rb +389 -0
  260. data/lib/omnizip/converter/conversion_registry.rb +50 -0
  261. data/lib/omnizip/converter/conversion_strategy.rb +108 -0
  262. data/lib/omnizip/converter/extract_repack_strategy.rb +51 -0
  263. data/lib/omnizip/converter/seven_zip_to_zip_strategy.rb +43 -0
  264. data/lib/omnizip/converter/zip_to_seven_zip_strategy.rb +54 -0
  265. data/lib/omnizip/converter.rb +124 -0
  266. data/lib/omnizip/crypto/aes256/cipher.rb +104 -0
  267. data/lib/omnizip/crypto/aes256/constants.rb +28 -0
  268. data/lib/omnizip/crypto/aes256/key_derivation.rb +100 -0
  269. data/lib/omnizip/crypto/aes256.rb +102 -0
  270. data/lib/omnizip/crypto.rb +11 -0
  271. data/lib/omnizip/entry.rb +44 -0
  272. data/lib/omnizip/error.rb +110 -0
  273. data/lib/omnizip/eta/exponential_smoothing_estimator.rb +95 -0
  274. data/lib/omnizip/eta/moving_average_estimator.rb +96 -0
  275. data/lib/omnizip/eta/rate_calculator.rb +104 -0
  276. data/lib/omnizip/eta/sample_history.rb +143 -0
  277. data/lib/omnizip/eta/time_estimator.rb +106 -0
  278. data/lib/omnizip/eta.rb +68 -0
  279. data/lib/omnizip/extraction/filter_chain.rb +179 -0
  280. data/lib/omnizip/extraction/glob_pattern.rb +140 -0
  281. data/lib/omnizip/extraction/pattern_matcher.rb +66 -0
  282. data/lib/omnizip/extraction/predicate_pattern.rb +52 -0
  283. data/lib/omnizip/extraction/regex_pattern.rb +50 -0
  284. data/lib/omnizip/extraction/selective_extractor.rb +247 -0
  285. data/lib/omnizip/extraction.rb +115 -0
  286. data/lib/omnizip/file_type/mime_classifier.rb +144 -0
  287. data/lib/omnizip/file_type.rb +117 -0
  288. data/lib/omnizip/filter.rb +141 -0
  289. data/lib/omnizip/filter_pipeline.rb +108 -0
  290. data/lib/omnizip/filter_registry.rb +113 -0
  291. data/lib/omnizip/filters/bcj.rb +277 -0
  292. data/lib/omnizip/filters/bcj2/constants.rb +53 -0
  293. data/lib/omnizip/filters/bcj2/decoder.rb +263 -0
  294. data/lib/omnizip/filters/bcj2/encoder.rb +58 -0
  295. data/lib/omnizip/filters/bcj2/stream_data.rb +91 -0
  296. data/lib/omnizip/filters/bcj2.rb +93 -0
  297. data/lib/omnizip/filters/bcj_arm.rb +174 -0
  298. data/lib/omnizip/filters/bcj_arm64.rb +242 -0
  299. data/lib/omnizip/filters/bcj_ia64.rb +194 -0
  300. data/lib/omnizip/filters/bcj_ppc.rb +188 -0
  301. data/lib/omnizip/filters/bcj_sparc.rb +174 -0
  302. data/lib/omnizip/filters/bcj_x86.rb +191 -0
  303. data/lib/omnizip/filters/delta.rb +195 -0
  304. data/lib/omnizip/filters/xz_delta.rb +258 -0
  305. data/lib/omnizip/filters.rb +20 -0
  306. data/lib/omnizip/format_detector.rb +166 -0
  307. data/lib/omnizip/formats/.keep +0 -0
  308. data/lib/omnizip/formats/bzip2_file.rb +159 -0
  309. data/lib/omnizip/formats/cpio/bounded_io.rb +66 -0
  310. data/lib/omnizip/formats/cpio/constants.rb +55 -0
  311. data/lib/omnizip/formats/cpio/entry.rb +389 -0
  312. data/lib/omnizip/formats/cpio/reader.rb +194 -0
  313. data/lib/omnizip/formats/cpio/writer.rb +232 -0
  314. data/lib/omnizip/formats/cpio.rb +132 -0
  315. data/lib/omnizip/formats/format_spec_loader.rb +228 -0
  316. data/lib/omnizip/formats/gzip.rb +236 -0
  317. data/lib/omnizip/formats/iso/directory_builder.rb +321 -0
  318. data/lib/omnizip/formats/iso/directory_record.rb +162 -0
  319. data/lib/omnizip/formats/iso/joliet.rb +204 -0
  320. data/lib/omnizip/formats/iso/path_table.rb +125 -0
  321. data/lib/omnizip/formats/iso/reader.rb +194 -0
  322. data/lib/omnizip/formats/iso/rock_ridge.rb +349 -0
  323. data/lib/omnizip/formats/iso/volume_builder.rb +319 -0
  324. data/lib/omnizip/formats/iso/volume_descriptor.rb +168 -0
  325. data/lib/omnizip/formats/iso/writer.rb +543 -0
  326. data/lib/omnizip/formats/iso.rb +133 -0
  327. data/lib/omnizip/formats/lzip.rb +161 -0
  328. data/lib/omnizip/formats/lzma_alone.rb +149 -0
  329. data/lib/omnizip/formats/msi/cab_extractor.rb +181 -0
  330. data/lib/omnizip/formats/msi/constants.rb +157 -0
  331. data/lib/omnizip/formats/msi/directory_resolver.rb +188 -0
  332. data/lib/omnizip/formats/msi/entry.rb +134 -0
  333. data/lib/omnizip/formats/msi/reader.rb +301 -0
  334. data/lib/omnizip/formats/msi/string_pool.rb +246 -0
  335. data/lib/omnizip/formats/msi/table_parser.rb +244 -0
  336. data/lib/omnizip/formats/msi.rb +73 -0
  337. data/lib/omnizip/formats/ole/allocation_table.rb +263 -0
  338. data/lib/omnizip/formats/ole/constants.rb +61 -0
  339. data/lib/omnizip/formats/ole/dirent.rb +439 -0
  340. data/lib/omnizip/formats/ole/header.rb +199 -0
  341. data/lib/omnizip/formats/ole/ranges_io.rb +347 -0
  342. data/lib/omnizip/formats/ole/storage.rb +482 -0
  343. data/lib/omnizip/formats/ole/types/variant.rb +338 -0
  344. data/lib/omnizip/formats/ole.rb +143 -0
  345. data/lib/omnizip/formats/rar/archive_repairer.rb +240 -0
  346. data/lib/omnizip/formats/rar/archive_verifier.rb +192 -0
  347. data/lib/omnizip/formats/rar/block_parser.rb +282 -0
  348. data/lib/omnizip/formats/rar/compression/bit_stream.rb +180 -0
  349. data/lib/omnizip/formats/rar/compression/dispatcher.rb +210 -0
  350. data/lib/omnizip/formats/rar/compression/lz77_huffman/decoder.rb +212 -0
  351. data/lib/omnizip/formats/rar/compression/lz77_huffman/encoder.rb +154 -0
  352. data/lib/omnizip/formats/rar/compression/lz77_huffman/huffman_builder.rb +217 -0
  353. data/lib/omnizip/formats/rar/compression/lz77_huffman/huffman_coder.rb +189 -0
  354. data/lib/omnizip/formats/rar/compression/lz77_huffman/match_finder.rb +135 -0
  355. data/lib/omnizip/formats/rar/compression/lz77_huffman/sliding_window.rb +165 -0
  356. data/lib/omnizip/formats/rar/compression/lz77_huffman.rb +25 -0
  357. data/lib/omnizip/formats/rar/compression/ppmd/context.rb +103 -0
  358. data/lib/omnizip/formats/rar/compression/ppmd/decoder.rb +238 -0
  359. data/lib/omnizip/formats/rar/compression/ppmd/encoder.rb +250 -0
  360. data/lib/omnizip/formats/rar/compression/ppmd.rb +16 -0
  361. data/lib/omnizip/formats/rar/compression.rb +17 -0
  362. data/lib/omnizip/formats/rar/compression_method_registry.rb +106 -0
  363. data/lib/omnizip/formats/rar/constants.rb +88 -0
  364. data/lib/omnizip/formats/rar/decompressor.rb +316 -0
  365. data/lib/omnizip/formats/rar/external_writer.rb +311 -0
  366. data/lib/omnizip/formats/rar/header.rb +209 -0
  367. data/lib/omnizip/formats/rar/license_validator.rb +111 -0
  368. data/lib/omnizip/formats/rar/models/rar_archive.rb +77 -0
  369. data/lib/omnizip/formats/rar/models/rar_entry.rb +65 -0
  370. data/lib/omnizip/formats/rar/models/rar_volume.rb +56 -0
  371. data/lib/omnizip/formats/rar/models.rb +14 -0
  372. data/lib/omnizip/formats/rar/parity_handler.rb +299 -0
  373. data/lib/omnizip/formats/rar/rar5/compression/lzma.rb +200 -0
  374. data/lib/omnizip/formats/rar/rar5/compression/lzss.rb +584 -0
  375. data/lib/omnizip/formats/rar/rar5/compression/store.rb +60 -0
  376. data/lib/omnizip/formats/rar/rar5/crc32.rb +39 -0
  377. data/lib/omnizip/formats/rar/rar5/encryption/aes256_cbc.rb +96 -0
  378. data/lib/omnizip/formats/rar/rar5/encryption/encryption_header.rb +114 -0
  379. data/lib/omnizip/formats/rar/rar5/encryption/encryption_manager.rb +162 -0
  380. data/lib/omnizip/formats/rar/rar5/encryption/key_derivation.rb +97 -0
  381. data/lib/omnizip/formats/rar/rar5/header.rb +208 -0
  382. data/lib/omnizip/formats/rar/rar5/models/encryption_options.rb +74 -0
  383. data/lib/omnizip/formats/rar/rar5/models/recovery_options.rb +63 -0
  384. data/lib/omnizip/formats/rar/rar5/models/solid_options.rb +63 -0
  385. data/lib/omnizip/formats/rar/rar5/models/volume_options.rb +74 -0
  386. data/lib/omnizip/formats/rar/rar5/multi_volume/ARCHITECTURE.md +290 -0
  387. data/lib/omnizip/formats/rar/rar5/multi_volume/volume_manager.rb +260 -0
  388. data/lib/omnizip/formats/rar/rar5/multi_volume/volume_splitter.rb +155 -0
  389. data/lib/omnizip/formats/rar/rar5/multi_volume/volume_writer.rb +176 -0
  390. data/lib/omnizip/formats/rar/rar5/solid/solid_encoder.rb +108 -0
  391. data/lib/omnizip/formats/rar/rar5/solid/solid_manager.rb +139 -0
  392. data/lib/omnizip/formats/rar/rar5/solid/solid_stream.rb +121 -0
  393. data/lib/omnizip/formats/rar/rar5/vint.rb +53 -0
  394. data/lib/omnizip/formats/rar/rar5/writer.rb +540 -0
  395. data/lib/omnizip/formats/rar/rar_format_base.rb +239 -0
  396. data/lib/omnizip/formats/rar/reader.rb +377 -0
  397. data/lib/omnizip/formats/rar/recovery_record.rb +245 -0
  398. data/lib/omnizip/formats/rar/volume_manager.rb +166 -0
  399. data/lib/omnizip/formats/rar/writer.rb +437 -0
  400. data/lib/omnizip/formats/rar.rb +193 -0
  401. data/lib/omnizip/formats/rar3/compressor.rb +71 -0
  402. data/lib/omnizip/formats/rar3/decompressor.rb +64 -0
  403. data/lib/omnizip/formats/rar3/reader.rb +107 -0
  404. data/lib/omnizip/formats/rar3/writer.rb +220 -0
  405. data/lib/omnizip/formats/rar3.rb +15 -0
  406. data/lib/omnizip/formats/rar5/compressor.rb +71 -0
  407. data/lib/omnizip/formats/rar5/decompressor.rb +64 -0
  408. data/lib/omnizip/formats/rar5/reader.rb +130 -0
  409. data/lib/omnizip/formats/rar5/writer.rb +65 -0
  410. data/lib/omnizip/formats/rar5.rb +15 -0
  411. data/lib/omnizip/formats/rpm/constants.rb +58 -0
  412. data/lib/omnizip/formats/rpm/entry.rb +106 -0
  413. data/lib/omnizip/formats/rpm/header.rb +106 -0
  414. data/lib/omnizip/formats/rpm/lead.rb +132 -0
  415. data/lib/omnizip/formats/rpm/tag.rb +228 -0
  416. data/lib/omnizip/formats/rpm/writer.rb +652 -0
  417. data/lib/omnizip/formats/rpm.rb +458 -0
  418. data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +274 -0
  419. data/lib/omnizip/formats/seven_zip/coder_chain.rb +169 -0
  420. data/lib/omnizip/formats/seven_zip/constants.rb +126 -0
  421. data/lib/omnizip/formats/seven_zip/encoded_header.rb +110 -0
  422. data/lib/omnizip/formats/seven_zip/encrypted_header.rb +139 -0
  423. data/lib/omnizip/formats/seven_zip/file_collector.rb +143 -0
  424. data/lib/omnizip/formats/seven_zip/header.rb +102 -0
  425. data/lib/omnizip/formats/seven_zip/header_encryptor.rb +132 -0
  426. data/lib/omnizip/formats/seven_zip/header_writer.rb +425 -0
  427. data/lib/omnizip/formats/seven_zip/models/coder_info.rb +30 -0
  428. data/lib/omnizip/formats/seven_zip/models/file_entry.rb +65 -0
  429. data/lib/omnizip/formats/seven_zip/models/folder.rb +67 -0
  430. data/lib/omnizip/formats/seven_zip/models/stream_info.rb +42 -0
  431. data/lib/omnizip/formats/seven_zip/models.rb +20 -0
  432. data/lib/omnizip/formats/seven_zip/parser.rb +671 -0
  433. data/lib/omnizip/formats/seven_zip/reader.rb +626 -0
  434. data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +611 -0
  435. data/lib/omnizip/formats/seven_zip/split_archive_writer.rb +308 -0
  436. data/lib/omnizip/formats/seven_zip/stream_compressor.rb +151 -0
  437. data/lib/omnizip/formats/seven_zip/stream_decompressor.rb +166 -0
  438. data/lib/omnizip/formats/seven_zip/writer.rb +801 -0
  439. data/lib/omnizip/formats/seven_zip.rb +179 -0
  440. data/lib/omnizip/formats/tar/constants.rb +73 -0
  441. data/lib/omnizip/formats/tar/entry.rb +98 -0
  442. data/lib/omnizip/formats/tar/header.rb +206 -0
  443. data/lib/omnizip/formats/tar/reader.rb +119 -0
  444. data/lib/omnizip/formats/tar/writer.rb +212 -0
  445. data/lib/omnizip/formats/tar.rb +74 -0
  446. data/lib/omnizip/formats/xar/constants.rb +119 -0
  447. data/lib/omnizip/formats/xar/entry.rb +268 -0
  448. data/lib/omnizip/formats/xar/header.rb +223 -0
  449. data/lib/omnizip/formats/xar/reader.rb +400 -0
  450. data/lib/omnizip/formats/xar/toc.rb +570 -0
  451. data/lib/omnizip/formats/xar/writer.rb +461 -0
  452. data/lib/omnizip/formats/xar.rb +145 -0
  453. data/lib/omnizip/formats/xz/reader.rb +121 -0
  454. data/lib/omnizip/formats/xz/writer.rb +416 -0
  455. data/lib/omnizip/formats/xz.rb +210 -0
  456. data/lib/omnizip/formats/xz_const.rb +49 -0
  457. data/lib/omnizip/formats/xz_impl/block_decoder.rb +761 -0
  458. data/lib/omnizip/formats/xz_impl/block_encoder.rb +304 -0
  459. data/lib/omnizip/formats/xz_impl/block_header.rb +210 -0
  460. data/lib/omnizip/formats/xz_impl/block_header_parser.rb +182 -0
  461. data/lib/omnizip/formats/xz_impl/index_decoder.rb +174 -0
  462. data/lib/omnizip/formats/xz_impl/index_encoder.rb +120 -0
  463. data/lib/omnizip/formats/xz_impl/stream_decoder.rb +470 -0
  464. data/lib/omnizip/formats/xz_impl/stream_encoder.rb +95 -0
  465. data/lib/omnizip/formats/xz_impl/stream_footer.rb +79 -0
  466. data/lib/omnizip/formats/xz_impl/stream_footer_parser.rb +115 -0
  467. data/lib/omnizip/formats/xz_impl/stream_header.rb +53 -0
  468. data/lib/omnizip/formats/xz_impl/stream_header_parser.rb +106 -0
  469. data/lib/omnizip/formats/xz_impl/vli.rb +128 -0
  470. data/lib/omnizip/formats/xz_impl.rb +52 -0
  471. data/lib/omnizip/formats/zip/central_directory_header.rb +223 -0
  472. data/lib/omnizip/formats/zip/constants.rb +69 -0
  473. data/lib/omnizip/formats/zip/end_of_central_directory.rb +132 -0
  474. data/lib/omnizip/formats/zip/local_file_header.rb +136 -0
  475. data/lib/omnizip/formats/zip/reader.rb +376 -0
  476. data/lib/omnizip/formats/zip/unix_extra_field.rb +153 -0
  477. data/lib/omnizip/formats/zip/writer.rb +489 -0
  478. data/lib/omnizip/formats/zip/zip64_end_of_central_directory.rb +102 -0
  479. data/lib/omnizip/formats/zip/zip64_end_of_central_directory_locator.rb +64 -0
  480. data/lib/omnizip/formats/zip/zip64_extra_field.rb +112 -0
  481. data/lib/omnizip/formats/zip.rb +50 -0
  482. data/lib/omnizip/formats.rb +29 -0
  483. data/lib/omnizip/implementations/base/lzma2_decoder_base.rb +75 -0
  484. data/lib/omnizip/implementations/base/lzma2_encoder_base.rb +128 -0
  485. data/lib/omnizip/implementations/base/lzma_decoder_base.rb +83 -0
  486. data/lib/omnizip/implementations/base/lzma_encoder_base.rb +108 -0
  487. data/lib/omnizip/implementations/base/state_machine_base.rb +182 -0
  488. data/lib/omnizip/implementations/base.rb +19 -0
  489. data/lib/omnizip/implementations/rust/archive.rb +93 -0
  490. data/lib/omnizip/implementations/rust/library.rb +221 -0
  491. data/lib/omnizip/implementations/rust.rb +26 -0
  492. data/lib/omnizip/implementations/seven_zip/lzma/decoder.rb +428 -0
  493. data/lib/omnizip/implementations/seven_zip/lzma/encoder.rb +456 -0
  494. data/lib/omnizip/implementations/seven_zip/lzma/match_finder.rb +285 -0
  495. data/lib/omnizip/implementations/seven_zip/lzma/range_decoder.rb +197 -0
  496. data/lib/omnizip/implementations/seven_zip/lzma/range_encoder.rb +194 -0
  497. data/lib/omnizip/implementations/seven_zip/lzma/state_machine.rb +139 -0
  498. data/lib/omnizip/implementations/seven_zip/lzma.rb +21 -0
  499. data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +56 -0
  500. data/lib/omnizip/implementations/seven_zip/lzma2.rb +12 -0
  501. data/lib/omnizip/implementations/seven_zip.rb +11 -0
  502. data/lib/omnizip/implementations/xz_utils/lzma2/decoder.rb +547 -0
  503. data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +748 -0
  504. data/lib/omnizip/implementations/xz_utils/lzma2.rb +13 -0
  505. data/lib/omnizip/implementations/xz_utils.rb +10 -0
  506. data/lib/omnizip/implementations.rb +25 -0
  507. data/lib/omnizip/io/buffered_input.rb +147 -0
  508. data/lib/omnizip/io/buffered_output.rb +106 -0
  509. data/lib/omnizip/io/source.rb +143 -0
  510. data/lib/omnizip/io/stream_manager.rb +117 -0
  511. data/lib/omnizip/io.rb +15 -0
  512. data/lib/omnizip/link_handler/hard_link.rb +79 -0
  513. data/lib/omnizip/link_handler/symbolic_link.rb +74 -0
  514. data/lib/omnizip/link_handler.rb +129 -0
  515. data/lib/omnizip/metadata/archive_metadata.rb +114 -0
  516. data/lib/omnizip/metadata/entry_metadata.rb +153 -0
  517. data/lib/omnizip/metadata/metadata_editor.rb +169 -0
  518. data/lib/omnizip/metadata/metadata_registry.rb +71 -0
  519. data/lib/omnizip/metadata/metadata_validator.rb +101 -0
  520. data/lib/omnizip/metadata.rb +58 -0
  521. data/lib/omnizip/models/.keep +0 -0
  522. data/lib/omnizip/models/algorithm_metadata.rb +41 -0
  523. data/lib/omnizip/models/attribute_apply.rb +40 -0
  524. data/lib/omnizip/models/compression_options.rb +53 -0
  525. data/lib/omnizip/models/conversion_options.rb +79 -0
  526. data/lib/omnizip/models/conversion_result.rb +141 -0
  527. data/lib/omnizip/models/eta_result.rb +45 -0
  528. data/lib/omnizip/models/extraction_rule.rb +115 -0
  529. data/lib/omnizip/models/filter_chain.rb +142 -0
  530. data/lib/omnizip/models/filter_config.rb +148 -0
  531. data/lib/omnizip/models/match_result.rb +141 -0
  532. data/lib/omnizip/models/optimization_suggestion.rb +95 -0
  533. data/lib/omnizip/models/parallel_options.rb +116 -0
  534. data/lib/omnizip/models/performance_result.rb +85 -0
  535. data/lib/omnizip/models/profile_report.rb +116 -0
  536. data/lib/omnizip/models/progress_options.rb +39 -0
  537. data/lib/omnizip/models/split_options.rb +116 -0
  538. data/lib/omnizip/optimization_registry.rb +45 -0
  539. data/lib/omnizip/parallel/engine.rb +52 -0
  540. data/lib/omnizip/parallel/job_queue.rb +209 -0
  541. data/lib/omnizip/parallel/job_scheduler.rb +225 -0
  542. data/lib/omnizip/parallel/parallel_compressor.rb +326 -0
  543. data/lib/omnizip/parallel/parallel_extractor.rb +238 -0
  544. data/lib/omnizip/parallel/thread_pool.rb +60 -0
  545. data/lib/omnizip/parallel/worker_pool.rb +227 -0
  546. data/lib/omnizip/parallel.rb +156 -0
  547. data/lib/omnizip/parity/chunked_block_processor.rb +194 -0
  548. data/lib/omnizip/parity/galois16.rb +145 -0
  549. data/lib/omnizip/parity/models/creator_packet.rb +71 -0
  550. data/lib/omnizip/parity/models/file_description_packet.rb +131 -0
  551. data/lib/omnizip/parity/models/ifsc_packet.rb +111 -0
  552. data/lib/omnizip/parity/models/main_packet.rb +126 -0
  553. data/lib/omnizip/parity/models/packet.rb +156 -0
  554. data/lib/omnizip/parity/models/packet_registry.rb +102 -0
  555. data/lib/omnizip/parity/models/recovery_slice_packet.rb +76 -0
  556. data/lib/omnizip/parity/models.rb +27 -0
  557. data/lib/omnizip/parity/par2_creator.rb +496 -0
  558. data/lib/omnizip/parity/par2_repairer.rb +409 -0
  559. data/lib/omnizip/parity/par2_verifier.rb +373 -0
  560. data/lib/omnizip/parity/par2cmdline_algorithm.rb +108 -0
  561. data/lib/omnizip/parity/par2cmdline_coefficients.rb +78 -0
  562. data/lib/omnizip/parity/reed_solomon_decoder.rb +263 -0
  563. data/lib/omnizip/parity/reed_solomon_encoder.rb +108 -0
  564. data/lib/omnizip/parity/reed_solomon_matrix.rb +339 -0
  565. data/lib/omnizip/parity.rb +207 -0
  566. data/lib/omnizip/password/encryption_registry.rb +26 -0
  567. data/lib/omnizip/password/encryption_strategy.rb +96 -0
  568. data/lib/omnizip/password/password_validator.rb +129 -0
  569. data/lib/omnizip/password/winzip_aes_strategy.rb +190 -0
  570. data/lib/omnizip/password/zip_crypto_strategy.rb +135 -0
  571. data/lib/omnizip/password.rb +88 -0
  572. data/lib/omnizip/pipe/stream_compressor.rb +124 -0
  573. data/lib/omnizip/pipe/stream_decompressor.rb +173 -0
  574. data/lib/omnizip/pipe.rb +120 -0
  575. data/lib/omnizip/platform/ntfs_streams.rb +200 -0
  576. data/lib/omnizip/platform.rb +189 -0
  577. data/lib/omnizip/profile/archive_profile.rb +36 -0
  578. data/lib/omnizip/profile/balanced_profile.rb +31 -0
  579. data/lib/omnizip/profile/binary_profile.rb +33 -0
  580. data/lib/omnizip/profile/compression_profile.rb +157 -0
  581. data/lib/omnizip/profile/custom_profile.rb +155 -0
  582. data/lib/omnizip/profile/fast_profile.rb +31 -0
  583. data/lib/omnizip/profile/maximum_profile.rb +31 -0
  584. data/lib/omnizip/profile/profile_detector.rb +108 -0
  585. data/lib/omnizip/profile/profile_registry.rb +160 -0
  586. data/lib/omnizip/profile/text_profile.rb +33 -0
  587. data/lib/omnizip/profile.rb +261 -0
  588. data/lib/omnizip/profiler/memory_profiler.rb +66 -0
  589. data/lib/omnizip/profiler/method_profiler.rb +49 -0
  590. data/lib/omnizip/profiler/report_generator.rb +158 -0
  591. data/lib/omnizip/profiler.rb +192 -0
  592. data/lib/omnizip/progress/callback_reporter.rb +34 -0
  593. data/lib/omnizip/progress/console_reporter.rb +59 -0
  594. data/lib/omnizip/progress/log_reporter.rb +89 -0
  595. data/lib/omnizip/progress/operation_progress.rb +118 -0
  596. data/lib/omnizip/progress/progress_bar.rb +156 -0
  597. data/lib/omnizip/progress/progress_reporter.rb +40 -0
  598. data/lib/omnizip/progress/progress_tracker.rb +215 -0
  599. data/lib/omnizip/progress/silent_reporter.rb +22 -0
  600. data/lib/omnizip/progress.rb +131 -0
  601. data/lib/omnizip/registry.rb +111 -0
  602. data/lib/omnizip/rubyzip_compat.rb +61 -0
  603. data/lib/omnizip/temp/safe_extract.rb +124 -0
  604. data/lib/omnizip/temp/temp_file.rb +123 -0
  605. data/lib/omnizip/temp/temp_file_pool.rb +109 -0
  606. data/lib/omnizip/temp.rb +184 -0
  607. data/lib/omnizip/version.rb +5 -0
  608. data/lib/omnizip/zip/entry.rb +160 -0
  609. data/lib/omnizip/zip/file.rb +485 -0
  610. data/lib/omnizip/zip/input_stream.rb +270 -0
  611. data/lib/omnizip/zip/output_stream.rb +320 -0
  612. data/lib/omnizip/zip.rb +23 -0
  613. data/lib/omnizip.rb +157 -0
  614. data/lib/tasks/msi_test_files.rake +66 -0
  615. data/lib/tasks/platform_gems.rake +147 -0
  616. data/lib/tasks/rust_backend.rake +48 -0
  617. data/readme-docs/advanced-features.adoc +514 -0
  618. data/readme-docs/api-usage.adoc +445 -0
  619. data/readme-docs/architecture.adoc +450 -0
  620. data/readme-docs/archive-formats.adoc +479 -0
  621. data/readme-docs/cli-usage.adoc +222 -0
  622. data/readme-docs/compression-algorithms.adoc +446 -0
  623. data/readme-docs/compression-profiles.adoc +247 -0
  624. data/readme-docs/encryption-checksums.adoc +328 -0
  625. data/readme-docs/format-converter.adoc +316 -0
  626. data/readme-docs/installation.adoc +228 -0
  627. data/readme-docs/par2-archives.adoc +608 -0
  628. data/readme-docs/performance-profiler.adoc +389 -0
  629. data/readme-docs/preprocessing-filters.adoc +280 -0
  630. data/vendor/libomnizip_ffi.so +0 -0
  631. data/xz-file-format-1.2.1.txt +1174 -0
  632. 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