Class: Cabriolet::OAB::Decompressor

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

Overview

Decompressor for OAB (Outlook Offline Address Book) files

OAB files use LZX compression and come in two formats:

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

  • Incremental patches (version 3.2): Binary patches applied to base file

This implementation is based on libmspack’s oabd.c

NOTE: This implementation cannot be fully validated due to lack of test fixtures. OAB files are specialized Outlook data files. Testing relies on round-trip compression/decompression.

Constant Summary collapse

DEFAULT_BUFFER_SIZE =

Default buffer size for I/O operations

4096

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io_system = nil) ⇒ Decompressor

Initialize OAB decompressor

Parameters:

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

    I/O system or nil for default



28
29
30
31
# File 'lib/cabriolet/oab/decompressor.rb', line 28

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

Instance Attribute Details

#buffer_sizeObject

Returns the value of attribute buffer_size.



20
21
22
# File 'lib/cabriolet/oab/decompressor.rb', line 20

def buffer_size
  @buffer_size
end

#io_systemObject (readonly)

Returns the value of attribute io_system.



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

def io_system
  @io_system
end

Instance Method Details

#decompress(input_file, output_file) ⇒ Integer

Decompress a full OAB file

Parameters:

  • input_file (String)

    Compressed OAB file path

  • output_file (String)

    Decompressed output path

Returns:

  • (Integer)

    Bytes written

Raises:

  • (Error)

    if decompression fails



39
40
41
42
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
# File 'lib/cabriolet/oab/decompressor.rb', line 39

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

  begin
    # Read and validate header
    header_data = @io_system.read(input_handle, 16)
    raise Error, "Failed to read OAB header" if header_data.length < 16

    header = Binary::OABStructures::FullHeader.read(header_data)
    raise Error, "Invalid OAB header" unless header.valid?

    block_max = header.block_max
    target_size = header.target_size
    total_written = 0

    # Process blocks until target size reached
    while target_size.positive?
      total_written += decompress_block(
        input_handle, output_handle, block_max, target_size
      )
      target_size -= [block_max, target_size].min
    end

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

#decompress_incremental(patch_file, base_file, output_file) ⇒ Integer

Decompress an incremental patch file

Parameters:

  • patch_file (String)

    Compressed patch file path

  • base_file (String)

    Base (uncompressed) file path

  • output_file (String)

    Output file path

Returns:

  • (Integer)

    Bytes written

Raises:

  • (Error)

    if decompression fails



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/cabriolet/oab/decompressor.rb', line 77

def decompress_incremental(patch_file, base_file, output_file)
  patch_handle = @io_system.open(patch_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
    # Read and validate patch header
    header_data = @io_system.read(patch_handle, 28)
    if header_data.length < 28
      raise Error,
            "Failed to read OAB patch header"
    end

    header = Binary::OABStructures::PatchHeader.read(header_data)
    raise Error, "Invalid OAB patch header" unless header.valid?

    block_max = [header.block_max, 16].max # At least 16 for header
    target_size = header.target_size
    total_written = 0

    # Process patch blocks until target size reached
    while target_size.positive?
      total_written += decompress_patch_block(
        patch_handle, base_handle, output_handle, block_max, target_size
      )
      target_size = header.target_size - total_written
    end

    total_written
  ensure
    @io_system.close(patch_handle) if patch_handle
    @io_system.close(base_handle) if base_handle
    @io_system.close(output_handle) if output_handle
  end
end