Class: Xlsxrb::Ooxml::Cfb::Reader

Inherits:
Object
  • Object
show all
Defined in:
lib/xlsxrb/ooxml/cfb.rb,
sig/generated/xlsxrb/ooxml/cfb.rbs

Overview

Reads streams from a Compound File Binary buffer.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data) ⇒ Reader

Returns a new instance of Reader.

Parameters:

  • data (Object)

Raises:



69
70
71
72
73
74
75
76
77
# File 'lib/xlsxrb/ooxml/cfb.rb', line 69

def initialize(data)
  @data = data.b
  raise Xlsxrb::Error, "Invalid CFB file header signature" unless Reader.cfb?(@data)

  parse_header
  build_fat
  parse_directory
  load_mini_stream
end

Instance Attribute Details

#entriesObject (readonly)

Returns the value of attribute entries.

Returns:

  • (Object)


61
62
63
# File 'lib/xlsxrb/ooxml/cfb.rb', line 61

def entries
  @entries
end

Class Method Details

.cfb?(data) ⇒ Boolean

Parameters:

  • data (Object)

Returns:

  • (Boolean)


63
64
65
66
67
# File 'lib/xlsxrb/ooxml/cfb.rb', line 63

def self.cfb?(data)
  return false if data.nil? || data.bytesize < 8

  data[0, 8] == MAGIC
end

Instance Method Details

#build_fatObject

Returns:

  • (Object)


127
128
129
# File 'lib/xlsxrb/ooxml/cfb.rb', line 127

def build_fat
  load_difat_and_fat
end

#load_difat_and_fatObject

Returns:

  • (Object)


131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/xlsxrb/ooxml/cfb.rb', line 131

def load_difat_and_fat
  @difat_sectors = @header_difat.dup
  if @first_difat_sector != ENDOFCHAIN && @first_difat_sector != FREESECT
    curr = @first_difat_sector
    visited = {}
    while curr != ENDOFCHAIN && curr != FREESECT
      break if visited[curr]

      visited[curr] = true
      sec_data = read_sector(curr)
      entries_per_sec = (@sector_size / 4) - 1
      entries = sec_data[0, entries_per_sec * 4].unpack("V*")
      @difat_sectors.concat(entries)
      curr = sec_data[entries_per_sec * 4, 4].unpack1("V")
    end
  end

  # Read all FAT sectors
  @fat = []
  @difat_sectors.each do |fat_sec_id|
    break if [ENDOFCHAIN, FREESECT].include?(fat_sec_id)

    sec_data = read_sector(fat_sec_id)
    @fat.concat(sec_data.unpack("V*"))
  end
end

#load_mini_streamObject

Returns:

  • (Object)


202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/xlsxrb/ooxml/cfb.rb', line 202

def load_mini_stream
  @minifat = []
  if @first_minifat_sector != ENDOFCHAIN && @first_minifat_sector != FREESECT
    curr = @first_minifat_sector
    visited = {}
    while curr != ENDOFCHAIN && curr != FREESECT && curr < @fat.size
      break if visited[curr]

      visited[curr] = true
      sec_data = read_sector(curr)
      @minifat.concat(sec_data.unpack("V*"))
      curr = @fat[curr]
    end
  end

  root_entry = @entries.find(&:root?)
  @mini_stream = if root_entry && root_entry.start_sector != ENDOFCHAIN && root_entry.size.positive?
                   read_regular_stream_data(root_entry.start_sector, root_entry.size)
                 else
                   "".b
                 end
end

#parse_directoryObject

Returns:

  • (Object)


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
196
197
198
199
200
# File 'lib/xlsxrb/ooxml/cfb.rb', line 158

def parse_directory
  dir_data = +""
  curr = @first_dir_sector
  visited = {}
  while curr != ENDOFCHAIN && curr != FREESECT && curr < @fat.size
    break if visited[curr]

    visited[curr] = true
    dir_data << read_sector(curr)
    curr = @fat[curr]
  end

  @entries = []
  num_entries = dir_data.bytesize / 128
  num_entries.times do |i|
    entry_bytes = dir_data[i * 128, 128]
    next if entry_bytes.nil? || entry_bytes.bytesize < 128

    name_bytes = entry_bytes[0, 64]
    name_len = entry_bytes[0x40, 2].unpack1("v")
    type = entry_bytes[0x42, 1].ord
    next if type == OBJ_UNKNOWN

    name_str = if name_len > 2
                 name_bytes[0, name_len - 2].force_encoding("UTF-16LE").encode("UTF-8", invalid: :replace, undef: :replace)
               else
                 ""
               end

    entry = DirEntry.new(name: name_str, type: type)
    entry.entry_id = i
    entry.color = entry_bytes[0x43, 1].ord
    entry.left_sibling_id = entry_bytes[0x44, 4].unpack1("V")
    entry.right_sibling_id = entry_bytes[0x48, 4].unpack1("V")
    entry.child_id = entry_bytes[0x4C, 4].unpack1("V")
    entry.clsid = entry_bytes[0x50, 16]
    entry.state_flags = entry_bytes[0x60, 4].unpack1("V")
    entry.start_sector = entry_bytes[0x74, 4].unpack1("V")
    entry.size = entry_bytes[0x78, 8].unpack1("Q<")

    @entries << entry
  end
end

#parse_headerObject

Returns:

  • (Object)

Raises:



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/xlsxrb/ooxml/cfb.rb', line 96

def parse_header
  raise Xlsxrb::Error, "CFB file is too small" if @data.bytesize < 512

  _, major_ver = @data[0x18, 4].unpack("v2")
  @major_version = major_ver
  @sector_shift = @data[0x1E, 2].unpack1("v")
  @mini_sector_shift = @data[0x20, 2].unpack1("v")
  @sector_size = 1 << @sector_shift
  @mini_sector_size = 1 << @mini_sector_shift

  @num_dir_sectors = @data[0x28, 4].unpack1("V")
  @num_fat_sectors = @data[0x2C, 4].unpack1("V")
  @first_dir_sector = @data[0x30, 4].unpack1("V")
  @mini_cutoff = @data[0x38, 4].unpack1("V")
  @first_minifat_sector = @data[0x3C, 4].unpack1("V")
  @num_minifat_sectors = @data[0x40, 4].unpack1("V")
  @first_difat_sector = @data[0x44, 4].unpack1("V")
  @num_difat_sectors = @data[0x48, 4].unpack1("V")

  @header_difat = @data[0x4C, 436].unpack("V109").reject { |s| [FREESECT, ENDOFCHAIN].include?(s) }
end

#read_mini_stream_data(start_mini_sector, total_size) ⇒ Object

Parameters:

  • start_mini_sector (Object)
  • total_size (Object)

Returns:

  • (Object)


243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/xlsxrb/ooxml/cfb.rb', line 243

def read_mini_stream_data(start_mini_sector, total_size)
  return "".b if [ENDOFCHAIN, FREESECT].include?(start_mini_sector) || total_size.zero?

  result = +""
  curr = start_mini_sector
  visited = {}
  while curr != ENDOFCHAIN && curr != FREESECT && curr < @minifat.size
    break if visited[curr]

    visited[curr] = true
    offset = curr * @mini_sector_size
    result << (@mini_stream[offset, @mini_sector_size] || "".b)
    break if result.bytesize >= total_size

    curr = @minifat[curr]
  end
  result[0, total_size] || "".b
end

#read_regular_stream_data(start_sector, total_size) ⇒ Object

Parameters:

  • start_sector (Object)
  • total_size (Object)

Returns:

  • (Object)


225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/xlsxrb/ooxml/cfb.rb', line 225

def read_regular_stream_data(start_sector, total_size)
  return "".b if [ENDOFCHAIN, FREESECT].include?(start_sector) || total_size.zero?

  result = +""
  curr = start_sector
  visited = {}
  while curr != ENDOFCHAIN && curr != FREESECT && curr < @fat.size
    break if visited[curr]

    visited[curr] = true
    result << read_sector(curr)
    break if result.bytesize >= total_size

    curr = @fat[curr]
  end
  result[0, total_size] || "".b
end

#read_sector(sector_id) ⇒ Object

Parameters:

  • sector_id (Object)

Returns:

  • (Object)


122
123
124
125
# File 'lib/xlsxrb/ooxml/cfb.rb', line 122

def read_sector(sector_id)
  offset = sector_offset(sector_id)
  @data[offset, @sector_size] || "".b
end

#read_stream(name) ⇒ Object

Parameters:

  • name (Object)

Returns:

  • (Object)


83
84
85
86
87
88
89
90
91
92
# File 'lib/xlsxrb/ooxml/cfb.rb', line 83

def read_stream(name)
  entry = @entries.find { |e| e.name.casecmp?(name) && e.stream? }
  return nil unless entry

  if entry.size < @mini_cutoff && @mini_stream && !@minifat.empty?
    read_mini_stream_data(entry.start_sector, entry.size)
  else
    read_regular_stream_data(entry.start_sector, entry.size)
  end
end

#sector_offset(sector_id) ⇒ Object

Parameters:

  • sector_id (Object)

Returns:

  • (Object)


118
119
120
# File 'lib/xlsxrb/ooxml/cfb.rb', line 118

def sector_offset(sector_id)
  512 + (sector_id * @sector_size)
end

#stream_namesObject

Returns:

  • (Object)


79
80
81
# File 'lib/xlsxrb/ooxml/cfb.rb', line 79

def stream_names
  @entries.select(&:stream?).map(&:name)
end