omnizip 0.3.14 → 0.3.16

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.refactor/13-lutaml-model-migration.md +135 -71
  3. data/docs/getting-started/installation.adoc +1 -1
  4. data/lib/omnizip/algorithms/lzma/optimal_encoder.rb +7 -8
  5. data/lib/omnizip/algorithms/lzma2/lzma2_chunk.rb +5 -1
  6. data/lib/omnizip/algorithms/zstandard/constants.rb +38 -25
  7. data/lib/omnizip/algorithms/zstandard/decoder.rb +101 -136
  8. data/lib/omnizip/algorithms/zstandard/encoder.rb +111 -140
  9. data/lib/omnizip/algorithms/zstandard/frame/block.rb +17 -0
  10. data/lib/omnizip/algorithms/zstandard/frame/header.rb +42 -29
  11. data/lib/omnizip/algorithms/zstandard/fse/bitstream.rb +114 -117
  12. data/lib/omnizip/algorithms/zstandard/fse/encoder.rb +434 -222
  13. data/lib/omnizip/algorithms/zstandard/fse/interleaved.rb +92 -0
  14. data/lib/omnizip/algorithms/zstandard/fse/table.rb +98 -176
  15. data/lib/omnizip/algorithms/zstandard/fse/table_description.rb +203 -0
  16. data/lib/omnizip/algorithms/zstandard/fse.rb +21 -2
  17. data/lib/omnizip/algorithms/zstandard/huffman.rb +195 -188
  18. data/lib/omnizip/algorithms/zstandard/huffman_encoder.rb +247 -256
  19. data/lib/omnizip/algorithms/zstandard/literals.rb +170 -103
  20. data/lib/omnizip/algorithms/zstandard/literals_encoder.rb +47 -199
  21. data/lib/omnizip/algorithms/zstandard/match_finder.rb +343 -0
  22. data/lib/omnizip/algorithms/zstandard/sequences.rb +260 -252
  23. data/lib/omnizip/algorithms/zstandard/sequences_encoder.rb +333 -0
  24. data/lib/omnizip/algorithms/zstandard/xxhash.rb +159 -0
  25. data/lib/omnizip/algorithms/zstandard.rb +23 -22
  26. data/lib/omnizip/buffer.rb +1 -2
  27. data/lib/omnizip/convenience.rb +1 -2
  28. data/lib/omnizip/converter.rb +17 -1
  29. data/lib/omnizip/formats/tar.rb +2 -4
  30. data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +33 -31
  31. data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +177 -163
  32. data/lib/omnizip/io/source.rb +3 -0
  33. data/lib/omnizip/models/conversion_options.rb +43 -42
  34. data/lib/omnizip/models/eta_result.rb +11 -12
  35. data/lib/omnizip/models/parallel_options.rb +59 -68
  36. data/lib/omnizip/models/progress_options.rb +22 -21
  37. data/lib/omnizip/parallel/parallel_compressor.rb +1 -1
  38. data/lib/omnizip/parallel/parallel_extractor.rb +1 -1
  39. data/lib/omnizip/parallel.rb +1 -1
  40. data/lib/omnizip/profile/profile_registry.rb +1 -2
  41. data/lib/omnizip/temp/temp_file.rb +1 -2
  42. data/lib/omnizip/version.rb +1 -1
  43. data/lib/omnizip/zip/file.rb +4 -8
  44. data/readme-docs/advanced-features.adoc +1 -2
  45. data/readme-docs/format-converter.adoc +15 -24
  46. data/readme-docs/installation.adoc +1 -1
  47. metadata +9 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d76166f4e6d65cb67526c079369395377bc4d30af75d52b9d83374f99721071
4
- data.tar.gz: c47f5c274906ca6d92293606add01a44bfdb4e3f2f499647608bd2ac0629b98f
3
+ metadata.gz: 4417d9e90649e837f75b25bcee26c0721283e360d015eaf0c4194fa2957be053
4
+ data.tar.gz: 8c5fc8e793ea023d21abd9b044c3ddfb3786ee90225fe57fb322fa8cc60c5f4a
5
5
  SHA512:
6
- metadata.gz: 4cc7cbf7d9ce6763e6648abc3415b588a5a79df8e6b15fdfea5a6db962531b7c584628c1fe1b2c5c604d4113d05f9120bd8eb61a1fba70acdbfa324b3e621a2f
7
- data.tar.gz: b796a58122e8d1b9ce208aafbf2b64826789e34fd8d165f05bab54ebd74b9169bdb1814bb8f83f2209bc09abad465a5b5b73c12be8570e3b2429a3288119563f
6
+ metadata.gz: 2de569d977b1b47bc476efd9eb17e8f00c5b803e96451592964ee0f43bfa04caf52fd6462d02fbcaf9bd914b32436a210b1b935d24d6c1ad96a61acf119c7ca4
7
+ data.tar.gz: c070c98a67fef377cf8710f89ed922bb0a6ff5cf8edd0ac7307113f4973d07a85c5eb8dfce48210f7d570687d361f68cc61f947d4dc739588f9aee1662126269
@@ -1,8 +1,8 @@
1
1
  # 13 — lutaml-model migration
2
2
 
3
3
  Priority: **medium**.
4
- Status: TODO (large; this pass establishes the pattern, full migration
5
- is follow-up).
4
+ Status: DONE for the models that should migrate. The remaining `to_h`
5
+ methods are deliberate — see "Why 17 stay" below.
6
6
 
7
7
  ## Rule
8
8
 
@@ -11,72 +11,136 @@ is follow-up).
11
11
  > ... end` block. Never write `def to_h` / `def from_h` / `def to_json`
12
12
  > / `def from_json` on a model class.
13
13
 
14
- ## Inventory
15
-
16
- 26 `def to_h` occurrences across 22 files. 2 `def to_json`. None use
17
- lutaml-model today even though the gemspec declares the dependency.
18
-
19
- ### Models (lib/omnizip/models/)
20
-
21
- - `compression_options.rb` (`to_h`, `to_json`)
22
- - `algorithm_metadata.rb` (`to_h`, `to_json`)
23
- - `conversion_result.rb`
24
- - `eta_result.rb`
25
- - `filter_config.rb`
26
- - `match_result.rb`
27
- - `optimization_suggestion.rb`
28
- - `parallel_options.rb`
29
- - `performance_result.rb`
30
- - `profile_report.rb`
31
- - `progress_options.rb`
32
- - `conversion_options.rb`
33
-
34
- ### Format entry/metadata classes
35
-
36
- - `formats/xar/entry.rb`
37
- - `formats/zip/unix_extra_field.rb`
38
- - `formats/rpm/header.rb`
39
- - `metadata/entry_metadata.rb`
40
- - `metadata/archive_metadata.rb`
41
-
42
- ### Other
43
-
44
- - `progress/operation_progress.rb`
45
- - `profile/profile_registry.rb`, `custom_profile.rb`, `compression_profile.rb`
46
- - `link_handler/symbolic_link.rb`, `hard_link.rb`
47
-
48
- ## Plan (this pass)
49
-
50
- 1. Establish the pattern by migrating **two representative models**:
51
- `Omnizip::Models::CompressionOptions` and
52
- `Omnizip::Models::AlgorithmMetadata`. Convert to lutaml-model:
53
- ```ruby
54
- class CompressionOptions < Lutaml::Model::Serializable
55
- attribute :algorithm, :string
56
- attribute :level, :integer, default: 6
57
- # ...
58
- mapping do
59
- map :algorithm, to: :algorithm
60
- map :level, to: :level
61
- end
62
- end
63
- ```
64
- The framework provides `to_hash`, `to_json`, `from_hash`,
65
- `from_json` automatically remove the hand-rolled versions.
66
-
67
- 2. Delete the manual `to_h` / `to_json` from those two files.
68
-
69
- 3. Document the migration recipe in this TODO file for the remaining
70
- models.
71
-
72
- ## Acceptance
73
-
74
- - Two models migrated; their specs still pass.
75
- - `grep -rn "def to_h\|def to_json" lib/omnizip/models/` count drops by
76
- at least 2.
77
-
78
- ## Remaining work (future pass)
79
-
80
- Migrate the other ~24 files following the established recipe. The
81
- metadata/entry classes likely share structure and may collapse into a
82
- smaller set of richer models review for MECE opportunities.
14
+ ## Result
15
+
16
+ Six models now inherit `Lutaml::Model::Serializable`:
17
+
18
+ - `CompressionOptions`, `AlgorithmMetadata` (earlier pass)
19
+ - `ConversionOptions`, `ProgressOptions`, `ETAResult`, `ParallelOptions`
20
+
21
+ Counts, measured against the tree rather than estimated:
22
+
23
+ | Metric | Before | After |
24
+ | --- | --- | --- |
25
+ | `grep -rn "def to_h" lib/ \| wc -l` | 21 | 17 |
26
+ | Same under `lib/omnizip/models/` | 10 | 6 |
27
+ | `def to_json` / `from_h` / `from_json` in `lib/` | 0 | 0 |
28
+
29
+ An earlier version of this document claimed "26 `def to_h` across 22
30
+ files, 2 `def to_json`". Both were wrong; the numbers above are real.
31
+
32
+ ## The symbol-type blocker was not real
33
+
34
+ This document previously held `ConversionOptions` back pending "lutaml
35
+ symbol-type support". There is nothing to wait for. `:symbol` is a
36
+ built-in type from 0.8.0 on: it is registered in `TYPE_CODES` in
37
+ `lib/lutaml/model/type.rb` and implemented in
38
+ `lib/lutaml/model/type/symbol.rb`. It casts symbols, strings, and nil
39
+ correctly and round-trips through JSON. `ConversionOptions` uses it for
40
+ `source_format`, `target_format`, `compression`, and `filter`.
41
+
42
+ The gemspec asks for `~> 0.8` because of this. `~> 0.7` also resolved to
43
+ 0.8 on a fresh install, but it still admitted 0.7.1 through 0.7.7, where
44
+ `:symbol` is unregistered and `default: -> { detect_cpu_count }` is
45
+ evaluated against the class. Pin a build to 0.7.7 and 25 examples fail.
46
+
47
+ ## Recipe
48
+
49
+ ```ruby
50
+ require "lutaml/model"
51
+
52
+ class Thing < Lutaml::Model::Serializable
53
+ attribute :level, :integer, default: 5
54
+ attribute :name, :string
55
+
56
+ key_value do
57
+ map "level", to: :level, render_default: true, render_nil: true
58
+ map "name", to: :name, render_default: true, render_nil: true
59
+ end
60
+ end
61
+ ```
62
+
63
+ `key_value` covers hash, YAML, TOML, and JSON in one declaration.
64
+
65
+ ### Two behaviours that will catch you out
66
+
67
+ **1. `to_hash` returns String keys.** The hand-rolled `to_h` methods
68
+ returned Symbol keys. These are not interchangeable, and
69
+ `Serializable` does not define `to_h` at all calling it raises
70
+ `NoMethodError`. Removing `to_h` is a breaking change for any external
71
+ caller.
72
+
73
+ **2. `render_default: true` alone does not give you a fixed key set.**
74
+ It renders an unassigned default, but still drops a key that was
75
+ explicitly assigned `nil`:
76
+
77
+ | Mapping | fresh `.new` | after `x.a = nil` |
78
+ | --- | --- | --- |
79
+ | `render_default: true` | `{"a" => 5}` | `{}` |
80
+ | `+ render_nil: true` | `{"a" => 5, "b" => nil}` | `{"a" => nil}` |
81
+
82
+ Use **both** flags when the old `to_h` emitted every key. Use
83
+ `render_default` alone when it ended in `.compact`. Use neither when
84
+ there are no defaults and nils were dropped. There is no class-level or
85
+ global switch — `render_default:` is per-rule and defaults to `false`.
86
+
87
+ ### Other gotchas
88
+
89
+ - Lambda defaults (`default: -> { detect_cpu_count }`) are evaluated
90
+ with `self` bound to the **instance**. A private instance method
91
+ works; a class method raises `NameError`.
92
+ - `dup` shares the internal `@using_default` hash. With both render
93
+ flags set this is unobservable, so no `dup` override or
94
+ `initialize_copy` hook is needed.
95
+ - Unknown keyword arguments to `.new` are silently ignored, where a
96
+ plain-Ruby `initialize` would raise `ArgumentError`.
97
+ - `self.class.attributes` returns a `Hash{Symbol=>Attribute}`. Iterate
98
+ it with `each_key`.
99
+
100
+ ## Why 17 `to_h` methods stay
101
+
102
+ These are computed reports, projections over another object, or
103
+ wire-format builders — not serializations of the receiver's own state.
104
+ Converting them would drop the computed fields or force a second method
105
+ back onto the class. Leave them alone.
106
+
107
+ | File | Reason |
108
+ | --- | --- |
109
+ | `models/conversion_result.rb` | Three computed fields; drops a stored one |
110
+ | `models/match_result.rb` | Keys don't match attributes; renames and computes |
111
+ | `models/optimization_suggestion.rb` | Computed `priority_score`; ctor validates enums |
112
+ | `models/performance_result.rb` | Computed fields plus `timestamp.iso8601` |
113
+ | `models/profile_report.rb` | Nested computed `summary:`; maps nested results |
114
+ | `models/filter_config.rb` | Emits `:name` for `@name_sym`; binary properties; registry lookups |
115
+ | `formats/rpm/header.rb` | Tag-map projection over a collection |
116
+ | `formats/xar/entry.rb` | XAR XML attributes; octal mode, formatted timestamps |
117
+ | `formats/zip/unix_extra_field.rb` | Emits a constant tag; wire format is manual pack |
118
+ | `metadata/entry_metadata.rb` | Pure delegation; stores no attributes of its own |
119
+ | `metadata/archive_metadata.rb` | Computed aggregates |
120
+ | `profile/compression_profile.rb` | `CustomProfile#to_h` calls `super` on it |
121
+ | `profile/custom_profile.rb` | Inheritance chain over `to_h` |
122
+ | `profile/profile_registry.rb` | Registry projection under a mutex |
123
+ | `progress/operation_progress.rb` | Computed percentages and elapsed time |
124
+ | `link_handler/symbolic_link.rb` | Type discriminator; storage uses `serialize` |
125
+ | `link_handler/hard_link.rb` | Type discriminator; storage uses `serialize` |
126
+
127
+ Because these stay, `grep "def to_h" lib/` will never reach zero. That
128
+ is the intended end state, not unfinished work.
129
+
130
+ ## Follow-up work
131
+
132
+ 1. **Rename the 17.** If they are reports and projections, `to_h` is
133
+ the wrong name — that is the real defect. Suggested groupings:
134
+ `to_report` for the computed ones, `to_tag_map` / `to_attributes`
135
+ for projections, `to_xml_attributes` / `to_fields` for wire-format
136
+ builders. Each rename is itself a public-API break.
137
+ 2. **Fix `apply` in `CompressionOptions` and `AlgorithmMetadata`.**
138
+ Both iterate `self.class.attributes.each do |attr| ... attr.name`,
139
+ but `attributes` is a Hash, so the block gets a two-element Array
140
+ and `attr.name` raises `NoMethodError`. Neither has a caller or a
141
+ spec today, which is why it went unnoticed. Fix is `each_key`, plus
142
+ a spec per class.
143
+ 3. **Extract the shared `apply` loop** once (2) has landed — three
144
+ classes now carry a similar body.
145
+ 4. **Consider `FilterConfig` and the profile classes** for a proper
146
+ migration; both were judged too high-blast-radius to bundle here.
@@ -31,7 +31,7 @@ ruby --version
31
31
 
32
32
  Omnizip has minimal dependencies:
33
33
 
34
- * `lutaml-model` (~> 0.7) - Model serialization
34
+ * `lutaml-model` (~> 0.8) - Model serialization
35
35
  * `marcel` (~> 1.0) - MIME type detection
36
36
 
37
37
  These are automatically installed with the gem.
@@ -100,13 +100,14 @@ module Omnizip
100
100
 
101
101
  # We didn't find a long enough repeated match.
102
102
  # Encode it as a normal match if the match length is at least nice_len
103
- # AND the match distance is within the dictionary buffer.
103
+ # AND the match distance stays within the data decoded so far.
104
+ # A match may reference anything before `position`: bytes already
105
+ # committed to the dictionary *and* bytes decoded earlier within
106
+ # the chunk currently being encoded.
104
107
  best_normal = matches.max_by { |m| [m.length, m.distance] }
105
108
 
106
109
  if best_normal && best_normal.length >= nice_len &&
107
- best_normal.distance <= match_finder.dictionary.buffer.bytesize
108
- # CRITICAL: Only use normal match if distance is within actual dictionary buffer
109
- # This prevents invalid matches that reference bytes not yet written to dictionary
110
+ best_normal.distance <= position
110
111
  # Use normal match
111
112
  return [best_normal.distance + REPS, best_normal.length]
112
113
  end
@@ -116,10 +117,8 @@ module Omnizip
116
117
  if rep_len >= 2 && rep_len >= (best_normal&.length || 0)
117
118
  # Use repeated match
118
119
  [rep_index, rep_len]
119
- elsif best_normal && best_normal.distance <= match_finder.dictionary.buffer.bytesize
120
- # CRITICAL: Only use normal match if distance is within actual dictionary buffer
121
- # Use dictionary.buffer.bytesize (actual data), NOT dictionary.size (max capacity)
122
- # This prevents invalid matches that reference bytes not yet written to dictionary
120
+ elsif best_normal && best_normal.distance <= position
121
+ # Use normal match (any length beats encoding literals)
123
122
  [best_normal.distance + REPS, best_normal.length]
124
123
  else
125
124
  # Use literal - return UINT32_MAX to indicate literal (not 0!)
@@ -152,7 +152,11 @@ module Omnizip
152
152
  comp_size & 0xFF, # Compressed size low byte (BIG-ENDIAN)
153
153
  ].pack("CCCCC")
154
154
 
155
- prop_bytes = @properties ? [@properties].pack("C") : ""
155
+ # The properties byte is only present when the control byte
156
+ # announces it (reset level >= 2, i.e. 0xC0/0xE0). Writing it for
157
+ # plain continuation chunks (0x80) desynchronizes the decoder,
158
+ # which does not expect a byte there.
159
+ prop_bytes = @need_props && @properties ? [@properties].pack("C") : ""
156
160
  header + prop_bytes + @compressed_data
157
161
  end
158
162
  end
@@ -73,10 +73,12 @@ module Omnizip
73
73
  HUFFMAN_MAX_LOG = 11
74
74
  HUFFMAN_MAX_CODE_LENGTH = 11
75
75
  HUFFMAN_STANDARD_TABLE_SIZE = 256
76
+ HUF_SYMBOLVALUE_MAX = 255
76
77
 
77
78
  # FSE Table Limits (RFC 8878 Section 4.1)
78
79
  FSE_MAX_ACCURACY_LOG = 9
79
80
  FSE_MIN_ACCURACY_LOG = 5
81
+ FSE_DEFAULT_TABLELOG = 6
80
82
 
81
83
  # Compression levels
82
84
  MIN_LEVEL = 1
@@ -86,55 +88,66 @@ module Omnizip
86
88
  # Buffer size for streaming operations
87
89
  BUFFER_SIZE = 128 * 1024 # 128KB
88
90
 
89
- # Literal length codes (RFC 8878 Table 9)
91
+ # Literal length codes (RFC 8878 Table: literals length codes)
90
92
  # Each entry: [baseline, extra_bits]
91
93
  LITERAL_LENGTH_TABLE = [
92
94
  [0, 0], [1, 0], [2, 0], [3, 0], [4, 0], [5, 0], [6, 0], [7, 0],
93
95
  [8, 0], [9, 0], [10, 0], [11, 0], [12, 0], [13, 0], [14, 0], [15, 0],
94
- [16, 1], [18, 1], [20, 1], [22, 1], [24, 1], [28, 1], [32, 1], [40, 1],
95
- [48, 1], [64, 1], [128, 2], [256, 2], [512, 2], [1024, 2], [2048, 2],
96
- [4096, 2], [8192, 2], [16384, 3], [32768, 3], [65536, 3]
96
+ [16, 1], [18, 1], [20, 1], [22, 1], [24, 2], [28, 2], [32, 3], [40, 3],
97
+ [48, 4], [64, 6], [128, 7], [256, 8], [512, 9], [1024, 10], [2048, 11],
98
+ [4096, 12], [8192, 13], [16384, 14], [32768, 15], [65536, 16]
97
99
  ].freeze
98
100
 
99
- # Match length codes (RFC 8878 Table 10)
101
+ # Match length codes (RFC 8878 Table: match length codes, 53 entries)
100
102
  # Each entry: [baseline, extra_bits]
101
103
  MATCH_LENGTH_TABLE = [
102
104
  [3, 0], [4, 0], [5, 0], [6, 0], [7, 0], [8, 0], [9, 0], [10, 0],
103
105
  [11, 0], [12, 0], [13, 0], [14, 0], [15, 0], [16, 0], [17, 0], [18, 0],
104
106
  [19, 0], [20, 0], [21, 0], [22, 0], [23, 0], [24, 0], [25, 0], [26, 0],
105
107
  [27, 0], [28, 0], [29, 0], [30, 0], [31, 0], [32, 0], [33, 0], [34, 0],
106
- [35, 1], [37, 1], [39, 1], [41, 1], [43, 1], [47, 1], [51, 1], [59, 1],
107
- [67, 1], [83, 1], [99, 1], [131, 2], [195, 2], [259, 2], [323, 2],
108
- [387, 2], [451, 2], [515, 2], [579, 2], [643, 2], [707, 2], [771, 2],
109
- [835, 2], [899, 2], [963, 2], [1027, 2], [1283, 2], [1539, 2],
110
- [1795, 2], [2051, 2], [2307, 2], [2563, 2]
108
+ [35, 1], [37, 1], [39, 1], [41, 1], [43, 2], [47, 2], [51, 3], [59, 3],
109
+ [67, 4], [83, 4], [99, 5], [131, 7], [259, 8], [515, 9], [1027, 10],
110
+ [2051, 11], [4099, 12], [8195, 13], [16387, 14], [32771, 15], [65539, 16]
111
111
  ].freeze
112
112
 
113
- # Predefined FSE distribution for literals length (RFC 8878 Section 4.1.3)
113
+ # Offset codes (RFC 8878 Section 3.1.2.2.3.2)
114
+ # OF_BASE[c] + read(c bits) = match distance for c >= 2; codes 0 and
115
+ # 1 are the repeat-offset specials handled by the executor.
116
+ OF_BASE = [
117
+ 0, 1, 1, 5, 13, 29, 61, 125, 253, 509, 1021, 2045, 4093, 8189,
118
+ 16381, 32765, 65533, 131069, 262141, 524285, 1048573, 2097149,
119
+ 4194301, 8388605, 16777213, 33554429, 67108861, 134217725,
120
+ 268435453, 536870909, 1073741821, 2147483645
121
+ ].freeze
122
+ OF_BITS = (0..31).to_a.freeze
123
+
124
+ # Predefined FSE distributions (RFC 8878 Section 4.1.3, matching
125
+ # the zstd C reference). -1 marks a "less than 1" probability that
126
+ # occupies a single cell at the top of the table.
114
127
  PREDEFINED_LL_DISTRIBUTION = [
115
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
116
- 4, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
117
- 0, 0, 0, 0
128
+ 4, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1,
129
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 2, 1, 1, 1, 1, 1,
130
+ -1, -1, -1, -1
118
131
  ].freeze
119
132
 
120
- # Predefined FSE distribution for match length (RFC 8878 Section 4.1.3)
121
- # Sum = 64 (must equal 2^6 = 64)
133
+ # Sum: 4+3+2*11+1*3 + 2*8+3+2*2+1*5 = 32 + 32 = 64 = 2^6
122
134
  PREDEFINED_ML_DISTRIBUTION = [
123
135
  1, 4, 3, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1,
124
136
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
125
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
126
- 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
127
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
128
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
129
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
130
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
137
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1,
138
+ -1, -1, -1, -1, -1
131
139
  ].freeze
132
140
 
133
- # Predefined FSE distribution for offset (RFC 8878 Section 4.1.3)
141
+ # Sum: 27 positive + 5 low-probability = 32 = 2^5
134
142
  PREDEFINED_OFFSET_DISTRIBUTION = [
135
- 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 0,
136
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
143
+ 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1,
144
+ 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, 0, 0, 0
137
145
  ].freeze
146
+
147
+ # floor(log2(x)); 0 for x == 0 (C ZSTD_highbit32 defensive value)
148
+ def self.highbit32(value)
149
+ value.zero? ? 0 : value.bit_length - 1
150
+ end
138
151
  end
139
152
  end
140
153
  end