Class: Cabriolet::OAB::Compressor

Inherits:
Object
  • Object
show all
Defined in:
lib/cabriolet/oab/compressor.rb

Overview

Compressor for OAB (Outlook Offline Address Book) files

OAB files use LZX compression. This compressor can create:

  • Full files (version 3.1): Complete address book data

  • Incremental patches (version 3.2): Binary patches (simplified)

NOTE: This implementation is based on the OAB format specification derived from libmspack’s decompressor. The original libmspack does not implement OAB compression. This is a best-effort implementation that may not produce files identical to Microsoft’s OAB generator.

Constant Summary collapse

DEFAULT_BUFFER_SIZE =

Default buffer size for I/O operations

4096
DEFAULT_BLOCK_SIZE =

Default block size (use 32KB like LZX frames)

32_768
VERSION_HI =

OAB version numbers

3
VERSION_LO_FULL =
1
VERSION_LO_PATCH =
2

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io_system = nil) ⇒ Compressor

Initialize OAB compressor

Parameters:

  • io_system (System::IOSystem, nil) (defaults to: nil)

    I/O system or nil for default



35
36
37
38
39
# File 'lib/cabriolet/oab/compressor.rb', line 35

def initialize(io_system = nil)
  @io_system = io_system || System::IOSystem.new
  @buffer_size = DEFAULT_BUFFER_SIZE
  @block_size = DEFAULT_BLOCK_SIZE
end

Instance Attribute Details

#block_sizeObject

Returns the value of attribute block_size.



19
20
21
# File 'lib/cabriolet/oab/compressor.rb', line 19

def block_size
  @block_size
end

#buffer_sizeObject

Returns the value of attribute buffer_size.



19
20
21
# File 'lib/cabriolet/oab/compressor.rb', line 19

def buffer_size
  @buffer_size
end

#io_systemObject (readonly)

Returns the value of attribute io_system.



18
19
20
# File 'lib/cabriolet/oab/compressor.rb', line 18

def io_system
  @io_system
end

Instance Method Details

#compress(input_file, output_file, **options) ⇒ Integer

Compress a full OAB file

Parameters:

  • input_file (String)

    Input file path

  • output_file (String)

    Compressed OAB output path

  • options (Hash)

    Compression options

Options Hash (**options):

  • :block_size (Integer)

    Block size (default: 32KB)

Returns:

  • (Integer)

    Bytes written

Raises:

  • (Error)

    if compression fails



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
# File 'lib/cabriolet/oab/compressor.rb', line 49

def compress(input_file, output_file, **options)
  block_size = options.fetch(:block_size, @block_size)

  input_handle = @io_system.open(input_file, Constants::MODE_READ)
  output_handle = @io_system.open(output_file, Constants::MODE_WRITE)

  begin
    # Get input size
    input_size = @io_system.seek(input_handle, 0, Constants::SEEK_END)
    @io_system.seek(input_handle, 0, Constants::SEEK_START)

    # Write header
    header = Binary::OABStructures::FullHeader.new
    header.version_hi = VERSION_HI
    header.version_lo = VERSION_LO_FULL
    header.block_max = block_size
    header.target_size = input_size

    header_data = header.to_binary_s
    bytes_written = @io_system.write(output_handle, header_data)

    # Compress data in blocks
    remaining = input_size
    while remaining.positive?
      block_bytes = compress_block(
        input_handle, output_handle, block_size, remaining
      )
      bytes_written += block_bytes
      remaining -= [block_size, remaining].min
    end

    bytes_written
  ensure
    @io_system.close(input_handle) if input_handle
    @io_system.close(output_handle) if output_handle
  end
end

#compress_data(data, output_file, **options) ⇒ Integer

Compress data from memory to OAB format

Parameters:

  • data (String)

    Input data to compress

  • output_file (String)

    Compressed OAB output path

  • options (Hash)

    Compression options

Options Hash (**options):

  • :block_size (Integer)

    Block size (default: 32KB)

Returns:

  • (Integer)

    Bytes written

Raises:

  • (Error)

    if compression fails



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/cabriolet/oab/compressor.rb', line 95

def compress_data(data, output_file, **options)
  block_size = options.fetch(:block_size, @block_size)

  input_handle = System::MemoryHandle.new(data, Constants::MODE_READ)
  output_handle = @io_system.open(output_file, Constants::MODE_WRITE)

  begin
    # Write header
    header = Binary::OABStructures::FullHeader.new
    header.version_hi = VERSION_HI
    header.version_lo = VERSION_LO_FULL
    header.block_max = block_size
    header.target_size = data.bytesize

    header_data = header.to_binary_s
    bytes_written = @io_system.write(output_handle, header_data)

    # Compress data in blocks
    remaining = data.bytesize
    while remaining.positive?
      block_bytes = compress_block(
        input_handle, output_handle, block_size, remaining
      )
      bytes_written += block_bytes
      remaining -= [block_size, remaining].min
    end

    bytes_written
  ensure
    @io_system.close(output_handle) if output_handle
  end
end

#compress_incremental(input_file, base_file, output_file, **options) ⇒ Integer

Create an incremental patch (simplified implementation)

This is a simplified patch format that just stores the new data compressed. A full implementation would generate binary diffs.

Parameters:

  • input_file (String)

    New version file path

  • base_file (String)

    Base version file path

  • output_file (String)

    Patch output path

  • options (Hash)

    Compression options

Options Hash (**options):

  • :block_size (Integer)

    Block size (default: 32KB)

Returns:

  • (Integer)

    Bytes written

Raises:

  • (Error)

    if compression fails



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/cabriolet/oab/compressor.rb', line 140

def compress_incremental(input_file, base_file, output_file, **options)
  block_size = options.fetch(:block_size, @block_size)

  # For now, just compress the new file with patch header
  # A full implementation would generate binary diffs
  input_handle = @io_system.open(input_file, Constants::MODE_READ)
  base_handle = @io_system.open(base_file, Constants::MODE_READ)
  output_handle = @io_system.open(output_file, Constants::MODE_WRITE)

  begin
    # Get file sizes
    input_size = @io_system.seek(input_handle, 0, Constants::SEEK_END)
    @io_system.seek(input_handle, 0, Constants::SEEK_START)

    base_size = @io_system.seek(base_handle, 0, Constants::SEEK_END)
    @io_system.seek(base_handle, 0, Constants::SEEK_START)

    # Read base data for CRC
    base_data = @io_system.read(base_handle, base_size)
    base_crc = Zlib.crc32(base_data)

    # Read target data for CRC
    target_data = @io_system.read(input_handle, input_size)
    @io_system.seek(input_handle, 0, Constants::SEEK_START)
    target_crc = Zlib.crc32(target_data)

    # Write patch header
    header = Binary::OABStructures::PatchHeader.new
    header.version_hi = VERSION_HI
    header.version_lo = VERSION_LO_PATCH
    header.block_max = [block_size, 16].max
    header.source_size = base_size
    header.target_size = input_size
    header.source_crc = base_crc
    header.target_crc = target_crc

    header_data = header.to_binary_s
    bytes_written = @io_system.write(output_handle, header_data)

    # Compress data in blocks (simplified - not true patches)
    remaining = input_size
    while remaining.positive?
      block_bytes = compress_patch_block(
        input_handle, output_handle, block_size, remaining, 0
      )
      bytes_written += block_bytes
      remaining -= [block_size, remaining].min
    end

    bytes_written
  ensure
    @io_system.close(input_handle) if input_handle
    @io_system.close(base_handle) if base_handle
    @io_system.close(output_handle) if output_handle
  end
end