284 lines
9.7 KiB
284 lines
9.7 KiB
# -*- coding: windows-1252 -*-
|
|
|
|
import struct
|
|
from .compat import xrange
|
|
|
|
# This implementation writes only 'Root Entry', 'Workbook' streams
|
|
# and 2 empty streams for aligning directory stream on sector boundary
|
|
#
|
|
# LAYOUT:
|
|
# 0 header
|
|
# 76 MSAT (1st part: 109 SID)
|
|
# 512 workbook stream
|
|
# ... additional MSAT sectors if streams' size > about 7 Mb == (109*512 * 128)
|
|
# ... SAT
|
|
# ... directory stream
|
|
#
|
|
# NOTE: this layout is "ad hoc". It can be more general. RTFM
|
|
|
|
class XlsDoc:
|
|
SECTOR_SIZE = 0x0200
|
|
MIN_LIMIT = 0x1000
|
|
|
|
SID_FREE_SECTOR = -1
|
|
SID_END_OF_CHAIN = -2
|
|
SID_USED_BY_SAT = -3
|
|
SID_USED_BY_MSAT = -4
|
|
|
|
def __init__(self):
|
|
#self.book_stream = '' # padded
|
|
self.book_stream_sect = []
|
|
|
|
self.dir_stream = ''
|
|
self.dir_stream_sect = []
|
|
|
|
self.packed_SAT = ''
|
|
self.SAT_sect = []
|
|
|
|
self.packed_MSAT_1st = ''
|
|
self.packed_MSAT_2nd = ''
|
|
self.MSAT_sect_2nd = []
|
|
|
|
self.header = ''
|
|
|
|
def _build_directory(self): # align on sector boundary
|
|
self.dir_stream = b''
|
|
|
|
dentry_name = u'Root Entry\x00'.encode('utf-16-le')
|
|
dentry_name_sz = len(dentry_name)
|
|
dentry_name_pad = b'\x00'*(64 - dentry_name_sz)
|
|
dentry_type = 0x05 # root storage
|
|
dentry_colour = 0x01 # black
|
|
dentry_did_left = -1
|
|
dentry_did_right = -1
|
|
dentry_did_root = 1
|
|
dentry_start_sid = -2
|
|
dentry_stream_sz = 0
|
|
|
|
self.dir_stream += struct.pack('<64s H 2B 3l 9L l L L',
|
|
dentry_name + dentry_name_pad,
|
|
dentry_name_sz,
|
|
dentry_type,
|
|
dentry_colour,
|
|
dentry_did_left,
|
|
dentry_did_right,
|
|
dentry_did_root,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
dentry_start_sid,
|
|
dentry_stream_sz,
|
|
0
|
|
)
|
|
|
|
dentry_name = u'Workbook\x00'.encode('utf-16-le')
|
|
dentry_name_sz = len(dentry_name)
|
|
dentry_name_pad = b'\x00'*(64 - dentry_name_sz)
|
|
dentry_type = 0x02 # user stream
|
|
dentry_colour = 0x01 # black
|
|
dentry_did_left = -1
|
|
dentry_did_right = -1
|
|
dentry_did_root = -1
|
|
dentry_start_sid = 0
|
|
dentry_stream_sz = self.book_stream_len
|
|
|
|
self.dir_stream += struct.pack('<64s H 2B 3l 9L l L L',
|
|
dentry_name + dentry_name_pad,
|
|
dentry_name_sz,
|
|
dentry_type,
|
|
dentry_colour,
|
|
dentry_did_left,
|
|
dentry_did_right,
|
|
dentry_did_root,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
dentry_start_sid,
|
|
dentry_stream_sz,
|
|
0
|
|
)
|
|
|
|
# padding
|
|
dentry_name = b''
|
|
dentry_name_sz = len(dentry_name)
|
|
dentry_name_pad = b'\x00'*(64 - dentry_name_sz)
|
|
dentry_type = 0x00 # empty
|
|
dentry_colour = 0x01 # black
|
|
dentry_did_left = -1
|
|
dentry_did_right = -1
|
|
dentry_did_root = -1
|
|
dentry_start_sid = -2
|
|
dentry_stream_sz = 0
|
|
|
|
self.dir_stream += struct.pack('<64s H 2B 3l 9L l L L',
|
|
dentry_name + dentry_name_pad,
|
|
dentry_name_sz,
|
|
dentry_type,
|
|
dentry_colour,
|
|
dentry_did_left,
|
|
dentry_did_right,
|
|
dentry_did_root,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
dentry_start_sid,
|
|
dentry_stream_sz,
|
|
0
|
|
) * 2
|
|
|
|
def _build_sat(self):
|
|
# Build SAT
|
|
book_sect_count = self.book_stream_len >> 9
|
|
dir_sect_count = len(self.dir_stream) >> 9
|
|
|
|
total_sect_count = book_sect_count + dir_sect_count
|
|
SAT_sect_count = 0
|
|
MSAT_sect_count = 0
|
|
SAT_sect_count_limit = 109
|
|
while total_sect_count > 128*SAT_sect_count or SAT_sect_count > SAT_sect_count_limit:
|
|
SAT_sect_count += 1
|
|
total_sect_count += 1
|
|
if SAT_sect_count > SAT_sect_count_limit:
|
|
MSAT_sect_count += 1
|
|
total_sect_count += 1
|
|
SAT_sect_count_limit += 127
|
|
|
|
|
|
SAT = [self.SID_FREE_SECTOR]*128*SAT_sect_count
|
|
|
|
sect = 0
|
|
while sect < book_sect_count - 1:
|
|
self.book_stream_sect.append(sect)
|
|
SAT[sect] = sect + 1
|
|
sect += 1
|
|
self.book_stream_sect.append(sect)
|
|
SAT[sect] = self.SID_END_OF_CHAIN
|
|
sect += 1
|
|
|
|
while sect < book_sect_count + MSAT_sect_count:
|
|
self.MSAT_sect_2nd.append(sect)
|
|
SAT[sect] = self.SID_USED_BY_MSAT
|
|
sect += 1
|
|
|
|
while sect < book_sect_count + MSAT_sect_count + SAT_sect_count:
|
|
self.SAT_sect.append(sect)
|
|
SAT[sect] = self.SID_USED_BY_SAT
|
|
sect += 1
|
|
|
|
while sect < book_sect_count + MSAT_sect_count + SAT_sect_count + dir_sect_count - 1:
|
|
self.dir_stream_sect.append(sect)
|
|
SAT[sect] = sect + 1
|
|
sect += 1
|
|
self.dir_stream_sect.append(sect)
|
|
SAT[sect] = self.SID_END_OF_CHAIN
|
|
sect += 1
|
|
|
|
self.packed_SAT = struct.pack('<%dl' % (SAT_sect_count*128), *SAT)
|
|
|
|
MSAT_1st = [self.SID_FREE_SECTOR]*109
|
|
for i, SAT_sect_num in zip(range(0, 109), self.SAT_sect):
|
|
MSAT_1st[i] = SAT_sect_num
|
|
self.packed_MSAT_1st = struct.pack('<109l', *MSAT_1st)
|
|
|
|
MSAT_2nd = [self.SID_FREE_SECTOR]*128*MSAT_sect_count
|
|
if MSAT_sect_count > 0:
|
|
MSAT_2nd[- 1] = self.SID_END_OF_CHAIN
|
|
|
|
i = 109
|
|
msat_sect = 0
|
|
sid_num = 0
|
|
while i < SAT_sect_count:
|
|
if (sid_num + 1) % 128 == 0:
|
|
#print 'link: ',
|
|
msat_sect += 1
|
|
if msat_sect < len(self.MSAT_sect_2nd):
|
|
MSAT_2nd[sid_num] = self.MSAT_sect_2nd[msat_sect]
|
|
else:
|
|
#print 'sid: ',
|
|
MSAT_2nd[sid_num] = self.SAT_sect[i]
|
|
i += 1
|
|
#print sid_num, MSAT_2nd[sid_num]
|
|
sid_num += 1
|
|
|
|
self.packed_MSAT_2nd = struct.pack('<%dl' % (MSAT_sect_count*128), *MSAT_2nd)
|
|
|
|
#print vars()
|
|
#print zip(range(0, sect), SAT)
|
|
#print self.book_stream_sect
|
|
#print self.MSAT_sect_2nd
|
|
#print MSAT_2nd
|
|
#print self.SAT_sect
|
|
#print self.dir_stream_sect
|
|
|
|
|
|
def _build_header(self):
|
|
doc_magic = b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1'
|
|
file_uid = b'\x00'*16
|
|
rev_num = b'\x3E\x00'
|
|
ver_num = b'\x03\x00'
|
|
byte_order = b'\xFE\xFF'
|
|
log_sect_size = struct.pack('<H', 9)
|
|
log_short_sect_size = struct.pack('<H', 6)
|
|
not_used0 = b'\x00'*10
|
|
total_sat_sectors = struct.pack('<L', len(self.SAT_sect))
|
|
dir_start_sid = struct.pack('<l', self.dir_stream_sect[0])
|
|
not_used1 = b'\x00'*4
|
|
min_stream_size = struct.pack('<L', 0x1000)
|
|
ssat_start_sid = struct.pack('<l', -2)
|
|
total_ssat_sectors = struct.pack('<L', 0)
|
|
|
|
if len(self.MSAT_sect_2nd) == 0:
|
|
msat_start_sid = struct.pack('<l', -2)
|
|
else:
|
|
msat_start_sid = struct.pack('<l', self.MSAT_sect_2nd[0])
|
|
|
|
total_msat_sectors = struct.pack('<L', len(self.MSAT_sect_2nd))
|
|
|
|
self.header = b''.join([ doc_magic,
|
|
file_uid,
|
|
rev_num,
|
|
ver_num,
|
|
byte_order,
|
|
log_sect_size,
|
|
log_short_sect_size,
|
|
not_used0,
|
|
total_sat_sectors,
|
|
dir_start_sid,
|
|
not_used1,
|
|
min_stream_size,
|
|
ssat_start_sid,
|
|
total_ssat_sectors,
|
|
msat_start_sid,
|
|
total_msat_sectors
|
|
])
|
|
|
|
|
|
def save(self, file_name_or_filelike_obj, stream):
|
|
# 1. Align stream on 0x1000 boundary (and therefore on sector boundary)
|
|
padding = b'\x00' * (0x1000 - (len(stream) % 0x1000))
|
|
self.book_stream_len = len(stream) + len(padding)
|
|
|
|
self._build_directory()
|
|
self._build_sat()
|
|
self._build_header()
|
|
|
|
f = file_name_or_filelike_obj
|
|
we_own_it = not hasattr(f, 'write')
|
|
if we_own_it:
|
|
f = open(file_name_or_filelike_obj, 'w+b')
|
|
f.write(self.header)
|
|
f.write(self.packed_MSAT_1st)
|
|
# There are reports of large writes failing when writing to "network shares" on Windows.
|
|
# MS says in KB899149 that it happens at 32KB less than 64MB.
|
|
# This is said to be alleviated by using "w+b" mode instead of "wb".
|
|
# One xlwt user has reported anomalous results at much smaller sizes,
|
|
# The fallback is to write the stream in 4 MB chunks.
|
|
try:
|
|
f.write(stream)
|
|
except IOError as e:
|
|
if e.errno != 22: # "Invalid argument" i.e. 'stream' is too big
|
|
raise # some other problem
|
|
chunk_size = 4 * 1024 * 1024
|
|
for offset in xrange(0, len(stream), chunk_size):
|
|
f.write(buffer(stream, offset, chunk_size))
|
|
f.write(padding)
|
|
f.write(self.packed_MSAT_2nd)
|
|
f.write(self.packed_SAT)
|
|
f.write(self.dir_stream)
|
|
if we_own_it:
|
|
f.close()
|