Class: Omnizip::Formats::Rar::Rar5::Compression::Lzma

Inherits:
Object
  • Object
show all
Defined in:
lib/omnizip/formats/rar/rar5/compression/lzma.rb

Overview

LZMA compression method for RAR5

This adapter uses the existing LZMA implementation with RAR5-specific parameters and encoding.

RAR5 compression methods (from spec):

  • 0: STORE (no compression)
  • 1: FASTEST
  • 2: FAST
  • 3: NORMAL
  • 4: GOOD
  • 5: BEST

For methods 1-5, RAR5 uses LZMA compression with different dictionary sizes and compression levels.

Examples:

Compress data with LZMA

compressed = Lzma.compress("Hello, World!", level: 5)

Defined Under Namespace

Classes: LzmaOptions

Constant Summary collapse

METHOD_FASTEST =

Compression method identifier for RAR5 Methods 1-5 all use LZMA with different parameters

1
METHOD_FAST =
2
METHOD_NORMAL =
3
METHOD_GOOD =
4
METHOD_BEST =
5

Class Method Summary collapse

Class Method Details

.build_lzma_options(level) ⇒ LzmaOptions

Build LZMA options based on RAR5 compression level

Parameters:

  • level (Integer)

    RAR5 compression level (1-5)

Returns:



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/omnizip/formats/rar/rar5/compression/lzma.rb', line 166

def self.build_lzma_options(level)
  # RAR5 compression levels map to LZMA parameters
  # These are approximations based on typical RAR behavior
  dict_size = 1 << case level
                   when 1 then 18  # 256 KB (fastest)
                   when 2 then 20  # 1 MB (fast)
                   when 3 then 22  # 4 MB (normal)
                   when 4 then 23  # 8 MB (good)
                   when 5 then 24  # 16 MB (best)
                   else 22 # default: 4 MB
                   end

  # RAR5 uses specific LZMA parameters: lc=1, lp=2, pb=0
  # (Different from standalone LZMA which typically uses lc=3, lp=0, pb=2)
  LzmaOptions.new(level, dict_size, lc: 1, lp: 2, pb: 0)
end

.compress(data, options = {}) ⇒ Hash

Compress data using LZMA

Parameters:

  • data (String)

    Data to compress

  • options (Hash) (defaults to: {})

    Compression options

Options Hash (options):

  • :level (Integer)

    Compression level (1-5, default: 3)

Returns:

  • (Hash)

    Hash with :data (compressed) and :properties (9 bytes for extra area)



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/omnizip/formats/rar/rar5/compression/lzma.rb', line 43

def self.compress(data, options = {})
  level = options[:level] || METHOD_NORMAL

  # Create StringIO streams for LZMA
  input = StringIO.new(data)
  output = StringIO.new
  output.set_encoding(Encoding::BINARY)

  # Create LZMA encoder with RAR5-appropriate settings
  lzma = Algorithms::LZMA.new
  lzma_options = build_lzma_options(level)

  # Build encoder options hash that LZMA encoder will accept
  encoder_options = {
    dict_size: lzma_options.dict_size,
    lc: lzma_options.lc,
    lp: lzma_options.lp,
    pb: lzma_options.pb,
    level: level,
  }

  # Compress using LZMA with RAR5 parameters
  lzma.compress(input, output, encoder_options)

  # The LZMA encoder outputs:
  #   - 1 byte: properties (lc, lp, pb)
  #   - 4 bytes: dictionary size (little-endian)
  #   - 8 bytes: uncompressed size (little-endian)
  #   - remaining: compressed data
  #
  # RAR5 stores properties in file header extra area (type 0x03),
  # so we extract them separately
  compressed_with_header = output.string

  if compressed_with_header.bytesize > 13
    # Extract LZMA properties for RAR5 extra area
    # RAR5 stores: property byte (1) + dict size (4) = 5 bytes
    # NOT the uncompressed size (which is in the next 8 bytes of LZMA header)
    properties = compressed_with_header[0, 5]

    # Extract raw LZMA stream (skip 13-byte header)
    compressed_data = compressed_with_header[13..]

    {
      data: compressed_data,
      properties: properties,
    }
  else
    # Edge case: if compression produced less than 13 bytes (extremely rare),
    # return original data uncompressed with nil properties
    warn "LZMA output too small (#{compressed_with_header.bytesize} bytes), using STORE"
    {
      data: data,
      properties: nil,
    }
  end
end

.compression_info(level = METHOD_NORMAL) ⇒ Integer

Get compression info VINT value

For RAR5, compression_info encodes:

  • Bits 0-5: compression method (1-5 for LZMA)
  • Bits 6+: version (0 for now)

Parameters:

  • level (Integer) (defaults to: METHOD_NORMAL)

    Compression level (1-5)

Returns:

  • (Integer)

    Compression info value



157
158
159
160
# File 'lib/omnizip/formats/rar/rar5/compression/lzma.rb', line 157

def self.compression_info(level = METHOD_NORMAL)
  method = method_id(level)
  method & 0x3F # Bits 0-5 only
end

.decompress(data, options = {}) ⇒ String

Decompress LZMA-compressed data

Parameters:

  • data (String)

    Compressed data (raw LZMA stream without header)

  • options (Hash) (defaults to: {})

    Decompression options

Options Hash (options):

  • :properties (String)

    The 5-byte properties from compress (property byte + dict size)

  • :uncompressed_size (Integer)

    Expected uncompressed size (optional, for EOS marker mode)

Returns:

  • (String)

    Decompressed data



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/omnizip/formats/rar/rar5/compression/lzma.rb', line 108

def self.decompress(data, options = {})
  properties = options[:properties]
  uncompressed_size = options[:uncompressed_size]

  # Reconstruct the 13-byte LZMA header if properties are provided
  if properties && properties.bytesize >= 5
    # properties contains: 1 byte props + 4 bytes dict size
    # We need to add 8 bytes for uncompressed size
    header = properties.dup
    header += if uncompressed_size
                # Add uncompressed size as 8-byte little-endian
                [uncompressed_size].pack("Q<")
              else
                # Use -1 (0xFFFFFFFFFFFFFFFF) to indicate EOS marker mode
                [0xFFFFFFFFFFFFFFFF].pack("Q<")
              end
    full_data = header + data
  else
    # Data already includes LZMA header
    full_data = data
  end

  input = StringIO.new(full_data)
  output = StringIO.new
  output.set_encoding(Encoding::BINARY)

  # Use SDK decoder since RAR5 LZMA was compressed with SDK encoder
  decoder = Implementations::SevenZip::LZMA::Decoder.new(input)
  decoder.decode_stream(output)

  output.string
end

.method_id(level = METHOD_NORMAL) ⇒ Integer

Get compression method identifier for level

Parameters:

  • level (Integer) (defaults to: METHOD_NORMAL)

    Compression level (1-5)

Returns:

  • (Integer)

    Method ID



145
146
147
# File 'lib/omnizip/formats/rar/rar5/compression/lzma.rb', line 145

def self.method_id(level = METHOD_NORMAL)
  level.clamp(METHOD_FASTEST, METHOD_BEST)
end