class Xlsxrb::Ooxml::Cfb::Writer
Writes named streams into a Compound File Binary (v3, 512-byte sectors) format with Mini Stream support.
Public Class Methods
(untyped streams) → untyped
Source
# File lib/xlsxrb/ooxml/cfb.rb, line 269 def initialize(streams) # streams: Hash of { String => String (bytes) } @streams = streams end
(untyped streams) → untyped
Source
# File lib/xlsxrb/ooxml/cfb.rb, line 265 def self.write(streams) new(streams).build end
Public Instance Methods
() → untyped
Source
# File lib/xlsxrb/ooxml/cfb.rb, line 274 def build sector_size = SECTOR_SIZE mini_sector_size = MINI_SECTOR_SIZE # Partition streams into mini streams (< 4096 bytes) and regular streams (>= 4096 bytes) regular_stream_entries = [] mini_stream_bytes = +"" minifat = [] # Directory Entries array dir_entries = [] root_entry = DirEntry.new(name: "Root Entry", type: OBJ_ROOT, start_sector: ENDOFCHAIN, size: 0) dir_entries << root_entry stream_names = @streams.keys stream_names.each_with_index do |name, idx| entry_id = idx + 1 data = @streams[name].b size = data.bytesize if size < MINI_STREAM_CUTOFF && size.positive? # Allocate in Mini Stream start_mini_sec = minifat.size num_mini_sec = (size + mini_sector_size - 1) / mini_sector_size num_mini_sec.times do |m_idx| chunk = data[m_idx * mini_sector_size, mini_sector_size] || "".b chunk = chunk.ljust(mini_sector_size, "\x00".b) if chunk.bytesize < mini_sector_size mini_stream_bytes << chunk minifat << (m_idx == num_mini_sec - 1 ? ENDOFCHAIN : (start_mini_sec + m_idx + 1)) end entry = DirEntry.new(name: name, type: OBJ_STREAM, start_sector: start_mini_sec, size: size) elsif size >= MINI_STREAM_CUTOFF # Allocate in Regular Stream (start_sector will be assigned later) entry = DirEntry.new(name: name, type: OBJ_STREAM, start_sector: ENDOFCHAIN, size: size) regular_stream_entries << [entry, data] else entry = DirEntry.new(name: name, type: OBJ_STREAM, start_sector: ENDOFCHAIN, size: 0) end entry.entry_id = entry_id dir_entries << entry end # Set up directory binary tree root_entry.child_id = @streams.empty? ? NOSTREAM : 1 if dir_entries.size > 1 root_child = dir_entries[1] (2...dir_entries.size).each do |i| insert_entry_to_tree(dir_entries, root_child, dir_entries[i]) end end # Pad directory entries to multiple of 4 (128 bytes * 4 = 512 bytes = 1 sector) dir_entries << DirEntry.new(type: OBJ_UNKNOWN) until (dir_entries.size % 4).zero? # Now allocate regular sectors: # 1. Regular stream data sectors # 2. Mini stream container sectors # 3. Mini FAT sectors # 4. Directory sector # 5. FAT sector allocated_sectors = [] sector_chains = [] # pairs of [start_sec, num_sec] # 1. Regular streams regular_stream_entries.each do |entry, data| start_sec = allocated_sectors.size num_sec = (data.bytesize + sector_size - 1) / sector_size num_sec.times do |s_idx| chunk = data[s_idx * sector_size, sector_size] || "".b chunk = chunk.ljust(sector_size, "\x00".b) if chunk.bytesize < sector_size allocated_sectors << chunk end entry.start_sector = start_sec sector_chains << [start_sec, num_sec] end # 2. Mini Stream container (assigned to Root Entry) if mini_stream_bytes.bytesize.positive? start_sec = allocated_sectors.size num_sec = (mini_stream_bytes.bytesize + sector_size - 1) / sector_size num_sec.times do |s_idx| chunk = mini_stream_bytes[s_idx * sector_size, sector_size] || "".b chunk = chunk.ljust(sector_size, "\x00".b) if chunk.bytesize < sector_size allocated_sectors << chunk end root_entry.start_sector = start_sec root_entry.size = mini_stream_bytes.bytesize sector_chains << [start_sec, num_sec] end # 3. Mini FAT sectors first_minifat_sec = ENDOFCHAIN num_minifat_sec = 0 if minifat.size.positive? first_minifat_sec = allocated_sectors.size minifat_bytes = minifat.pack("V*") num_minifat_sec = (minifat_bytes.bytesize + sector_size - 1) / sector_size num_minifat_sec.times do |s_idx| chunk = minifat_bytes[s_idx * sector_size, sector_size] || "".b chunk = chunk.ljust(sector_size, "\xFF".b) if chunk.bytesize < sector_size allocated_sectors << chunk end sector_chains << [first_minifat_sec, num_minifat_sec] end # 4. Directory sector dir_sector_id = allocated_sectors.size dir_sector_bytes = +"" dir_entries.each do |e| dir_sector_bytes << serialize_dir_entry(e) end allocated_sectors << dir_sector_bytes # 5. FAT sector fat_sector_id = allocated_sectors.size fat = Array.new(fat_sector_id + 1, FREESECT) # Populate regular sector chains in FAT sector_chains.each do |start_sec, num_sec| num_sec.times do |s_idx| fat[start_sec + s_idx] = s_idx == num_sec - 1 ? ENDOFCHAIN : (start_sec + s_idx + 1) end end fat[dir_sector_id] = ENDOFCHAIN fat[fat_sector_id] = FATSECT fat_bytes = fat.pack("V*").ljust(sector_size, "\xFF".b) allocated_sectors << fat_bytes # Build Header (512 bytes) header = +"" header << MAGIC # 0x00 (8 bytes) header << ("\x00".b * 16) # 0x08 CLSID header << [0x003B, 0x0003].pack("v2") # 0x18 Minor version (0x3B), Major version (3) header << [0xFFFE].pack("v") # 0x1C Byte order (Little Endian) header << [9].pack("v") # 0x1E Sector shift (512 bytes) header << [6].pack("v") # 0x20 Mini sector shift (64 bytes) header << ("\x00".b * 6) # 0x22 Reserved header << [0].pack("V") # 0x28 Number of Directory sectors (0 for v3) header << [1].pack("V") # 0x2C Number of FAT sectors (1) header << [dir_sector_id].pack("V") # 0x30 First Directory sector header << [0].pack("V") # 0x34 Transaction signature header << [MINI_STREAM_CUTOFF].pack("V") # 0x38 Mini stream cutoff size (4096) header << [first_minifat_sec].pack("V") # 0x3C First Mini FAT sector header << [num_minifat_sec].pack("V") # 0x40 Number of Mini FAT sectors header << [ENDOFCHAIN].pack("V") # 0x44 First DIFAT sector header << [0].pack("V") # 0x48 Number of DIFAT sectors # DIFAT array (109 entries * 4 bytes = 436 bytes) difat = Array.new(109, FREESECT) difat[0] = fat_sector_id header << difat.pack("V109") raise "Header size mismatch" unless header.bytesize == 512 # Combine Header + All Sectors output = +"" output << header allocated_sectors.each { |sec| output << sec } output end