Class: Xlsxrb::Ooxml::Cfb::Writer
- Inherits:
-
Object
- Object
- Xlsxrb::Ooxml::Cfb::Writer
- Defined in:
- lib/xlsxrb/ooxml/cfb.rb,
sig/generated/xlsxrb/ooxml/cfb.rbs
Overview
Writes named streams into a Compound File Binary (v3, 512-byte sectors) format with Mini Stream support.
Class Method Summary collapse
Instance Method Summary collapse
- #build ⇒ Object
- #compare_entry_names(a_name, b_name) ⇒ Object
-
#initialize(streams) ⇒ Writer
constructor
A new instance of Writer.
- #insert_entry_to_tree(entries, root_node, new_node) ⇒ Object
- #serialize_dir_entry(entry) ⇒ Object
Constructor Details
#initialize(streams) ⇒ Writer
Returns a new instance of Writer.
269 270 271 272 |
# File 'lib/xlsxrb/ooxml/cfb.rb', line 269 def initialize(streams) # streams: Hash of { String => String (bytes) } @streams = streams end |
Class Method Details
.write(streams) ⇒ Object
265 266 267 |
# File 'lib/xlsxrb/ooxml/cfb.rb', line 265 def self.write(streams) new(streams).build end |
Instance Method Details
#build ⇒ Object
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 |
# 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 |
#compare_entry_names(a_name, b_name) ⇒ Object
480 481 482 483 484 485 486 |
# File 'lib/xlsxrb/ooxml/cfb.rb', line 480 def compare_entry_names(a_name, b_name) # [MS-CFB] Section 2.6.1: Length comparison first, then uppercase UTF-16 code point comparison return -1 if a_name.length < b_name.length return 1 if a_name.length > b_name.length a_name.upcase <=> b_name.upcase end |
#insert_entry_to_tree(entries, root_node, new_node) ⇒ Object
465 466 467 468 469 470 471 472 473 474 475 476 477 478 |
# File 'lib/xlsxrb/ooxml/cfb.rb', line 465 def insert_entry_to_tree(entries, root_node, new_node) cmp = compare_entry_names(new_node.name, root_node.name) if cmp.negative? if root_node.left_sibling_id == NOSTREAM root_node.left_sibling_id = new_node.entry_id else insert_entry_to_tree(entries, entries[root_node.left_sibling_id], new_node) end elsif root_node.right_sibling_id == NOSTREAM root_node.right_sibling_id = new_node.entry_id else insert_entry_to_tree(entries, entries[root_node.right_sibling_id], new_node) end end |
#serialize_dir_entry(entry) ⇒ Object
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 |
# File 'lib/xlsxrb/ooxml/cfb.rb', line 441 def serialize_dir_entry(entry) return "\x00".b * 128 if entry.type == OBJ_UNKNOWN buf = +"" # Name in UTF-16LE with null terminator name_utf16 = "#{entry.name}\u0000".encode("UTF-16LE") name_bytes = name_utf16.b[0, 64].ljust(64, "\x00".b) buf << name_bytes buf << [name_utf16.bytesize].pack("v") # 0x40 Name length buf << [entry.type].pack("C") # 0x42 Object type buf << [entry.color].pack("C") # 0x43 Color (0 = Red/Black) buf << [entry.left_sibling_id].pack("V") # 0x44 Left sibling buf << [entry.right_sibling_id].pack("V") # 0x48 Right sibling buf << [entry.child_id].pack("V") # 0x4C Child ID buf << (entry.clsid || ("\x00".b * 16)) # 0x50 CLSID buf << [entry.state_flags].pack("V") # 0x60 State flags buf << [entry.created_time].pack("Q<") # 0x64 Created time buf << [entry.modified_time].pack("Q<") # 0x6C Modified time buf << [entry.start_sector].pack("V") # 0x74 Starting sector buf << [entry.size].pack("Q<") # 0x78 Stream size (uint64) buf.ljust(128, "\x00".b) end |