aspose-cells-foss 25.12.1__py3-none-any.whl → 26.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aspose_cells/__init__.py +88 -0
- aspose_cells/auto_filter.py +527 -0
- aspose_cells/cell.py +483 -0
- aspose_cells/cell_value_handler.py +319 -0
- aspose_cells/cells.py +779 -0
- aspose_cells/cfb_handler.py +445 -0
- aspose_cells/cfb_writer.py +659 -0
- aspose_cells/cfb_writer_minimal.py +337 -0
- aspose_cells/comment_xml.py +475 -0
- aspose_cells/conditional_format.py +1185 -0
- aspose_cells/csv_handler.py +690 -0
- aspose_cells/data_validation.py +911 -0
- aspose_cells/document_properties.py +356 -0
- aspose_cells/encryption_crypto.py +247 -0
- aspose_cells/encryption_params.py +138 -0
- aspose_cells/hyperlink.py +372 -0
- aspose_cells/json_handler.py +185 -0
- aspose_cells/markdown_handler.py +583 -0
- aspose_cells/shared_strings.py +101 -0
- aspose_cells/style.py +841 -0
- aspose_cells/workbook.py +499 -0
- aspose_cells/workbook_hash_password.py +68 -0
- aspose_cells/workbook_properties.py +712 -0
- aspose_cells/worksheet.py +570 -0
- aspose_cells/worksheet_properties.py +1239 -0
- aspose_cells/xlsx_encryptor.py +403 -0
- aspose_cells/xml_autofilter_loader.py +195 -0
- aspose_cells/xml_autofilter_saver.py +173 -0
- aspose_cells/xml_conditional_format_loader.py +215 -0
- aspose_cells/xml_conditional_format_saver.py +351 -0
- aspose_cells/xml_datavalidation_loader.py +239 -0
- aspose_cells/xml_datavalidation_saver.py +245 -0
- aspose_cells/xml_hyperlink_handler.py +323 -0
- aspose_cells/xml_loader.py +986 -0
- aspose_cells/xml_properties_loader.py +512 -0
- aspose_cells/xml_properties_saver.py +607 -0
- aspose_cells/xml_saver.py +1306 -0
- aspose_cells_foss-26.2.2.dist-info/METADATA +190 -0
- aspose_cells_foss-26.2.2.dist-info/RECORD +41 -0
- {aspose_cells_foss-25.12.1.dist-info → aspose_cells_foss-26.2.2.dist-info}/WHEEL +1 -1
- aspose_cells_foss-26.2.2.dist-info/top_level.txt +1 -0
- aspose/__init__.py +0 -14
- aspose/cells/__init__.py +0 -31
- aspose/cells/cell.py +0 -350
- aspose/cells/constants.py +0 -44
- aspose/cells/converters/__init__.py +0 -13
- aspose/cells/converters/csv_converter.py +0 -55
- aspose/cells/converters/json_converter.py +0 -46
- aspose/cells/converters/markdown_converter.py +0 -453
- aspose/cells/drawing/__init__.py +0 -17
- aspose/cells/drawing/anchor.py +0 -172
- aspose/cells/drawing/collection.py +0 -233
- aspose/cells/drawing/image.py +0 -338
- aspose/cells/formats.py +0 -80
- aspose/cells/formula/__init__.py +0 -10
- aspose/cells/formula/evaluator.py +0 -360
- aspose/cells/formula/functions.py +0 -433
- aspose/cells/formula/tokenizer.py +0 -340
- aspose/cells/io/__init__.py +0 -27
- aspose/cells/io/csv/__init__.py +0 -8
- aspose/cells/io/csv/reader.py +0 -88
- aspose/cells/io/csv/writer.py +0 -98
- aspose/cells/io/factory.py +0 -138
- aspose/cells/io/interfaces.py +0 -48
- aspose/cells/io/json/__init__.py +0 -8
- aspose/cells/io/json/reader.py +0 -126
- aspose/cells/io/json/writer.py +0 -119
- aspose/cells/io/md/__init__.py +0 -8
- aspose/cells/io/md/reader.py +0 -161
- aspose/cells/io/md/writer.py +0 -334
- aspose/cells/io/models.py +0 -64
- aspose/cells/io/xlsx/__init__.py +0 -9
- aspose/cells/io/xlsx/constants.py +0 -312
- aspose/cells/io/xlsx/image_writer.py +0 -311
- aspose/cells/io/xlsx/reader.py +0 -284
- aspose/cells/io/xlsx/writer.py +0 -931
- aspose/cells/plugins/__init__.py +0 -6
- aspose/cells/plugins/docling_backend/__init__.py +0 -7
- aspose/cells/plugins/docling_backend/backend.py +0 -535
- aspose/cells/plugins/markitdown_plugin/__init__.py +0 -15
- aspose/cells/plugins/markitdown_plugin/plugin.py +0 -128
- aspose/cells/range.py +0 -210
- aspose/cells/style.py +0 -287
- aspose/cells/utils/__init__.py +0 -54
- aspose/cells/utils/coordinates.py +0 -68
- aspose/cells/utils/exceptions.py +0 -43
- aspose/cells/utils/validation.py +0 -102
- aspose/cells/workbook.py +0 -352
- aspose/cells/worksheet.py +0 -670
- aspose_cells_foss-25.12.1.dist-info/METADATA +0 -189
- aspose_cells_foss-25.12.1.dist-info/RECORD +0 -53
- aspose_cells_foss-25.12.1.dist-info/entry_points.txt +0 -2
- aspose_cells_foss-25.12.1.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CFB (Compound File Binary) Writer
|
|
3
|
+
|
|
4
|
+
Minimal but compliant implementation for encrypted Office documents.
|
|
5
|
+
Supports storages and mini streams (MS-CFB).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import io
|
|
9
|
+
import struct
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _Node:
|
|
13
|
+
def __init__(self, name, obj_type, data=None):
|
|
14
|
+
self.name = name
|
|
15
|
+
self.obj_type = obj_type
|
|
16
|
+
self.data = data
|
|
17
|
+
self.children = []
|
|
18
|
+
self.left = None
|
|
19
|
+
self.right = None
|
|
20
|
+
self.child = None
|
|
21
|
+
self.parent = None
|
|
22
|
+
self.color = 1
|
|
23
|
+
self.starting_sector = 0xFFFFFFFE
|
|
24
|
+
self.stream_size = 0 if data is None else len(data)
|
|
25
|
+
self.force_regular = False
|
|
26
|
+
self.manual_tree = False
|
|
27
|
+
self.did = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CFBWriter:
|
|
31
|
+
"""
|
|
32
|
+
Writes CFB (Compound File Binary) files according to MS-CFB specification.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
HEADER_SIGNATURE = 0xE11AB1A1E011CFD0
|
|
36
|
+
HEADER_CLSID = b'\x00' * 16
|
|
37
|
+
MINOR_VERSION = 0x003E
|
|
38
|
+
MAJOR_VERSION_3 = 0x0003
|
|
39
|
+
MAJOR_VERSION_4 = 0x0004
|
|
40
|
+
BYTE_ORDER = 0xFFFE
|
|
41
|
+
|
|
42
|
+
MAXREGSECT = 0xFFFFFFFA
|
|
43
|
+
DIFSECT = 0xFFFFFFFC
|
|
44
|
+
FATSECT = 0xFFFFFFFD
|
|
45
|
+
ENDOFCHAIN = 0xFFFFFFFE
|
|
46
|
+
FREESECT = 0xFFFFFFFF
|
|
47
|
+
|
|
48
|
+
STGTY_INVALID = 0
|
|
49
|
+
STGTY_STORAGE = 1
|
|
50
|
+
STGTY_STREAM = 2
|
|
51
|
+
STGTY_LOCKBYTES = 3
|
|
52
|
+
STGTY_PROPERTY = 4
|
|
53
|
+
STGTY_ROOT = 5
|
|
54
|
+
|
|
55
|
+
COLOR_RED = 0
|
|
56
|
+
COLOR_BLACK = 1
|
|
57
|
+
|
|
58
|
+
MINI_STREAM_CUTOFF = 4096
|
|
59
|
+
MINI_SECTOR_SIZE = 64
|
|
60
|
+
|
|
61
|
+
def __init__(self, sector_size=512):
|
|
62
|
+
if sector_size not in (512, 4096):
|
|
63
|
+
raise ValueError("Sector size must be 512 or 4096")
|
|
64
|
+
|
|
65
|
+
self.sector_size = sector_size
|
|
66
|
+
self.major_version = self.MAJOR_VERSION_3 if sector_size == 512 else self.MAJOR_VERSION_4
|
|
67
|
+
self.sector_shift = 9 if sector_size == 512 else 12
|
|
68
|
+
self.root = _Node("Root Entry", self.STGTY_ROOT)
|
|
69
|
+
|
|
70
|
+
def add_stream(self, name, data, force_regular=False):
|
|
71
|
+
if len(name) == 0:
|
|
72
|
+
raise ValueError("Stream name cannot be empty")
|
|
73
|
+
|
|
74
|
+
parts = name.split('/')
|
|
75
|
+
node = self.root
|
|
76
|
+
for part in parts[:-1]:
|
|
77
|
+
node = self._get_or_create_storage(node, part)
|
|
78
|
+
|
|
79
|
+
stream_node = _Node(parts[-1], self.STGTY_STREAM, data)
|
|
80
|
+
stream_node.force_regular = force_regular
|
|
81
|
+
node.children.append(stream_node)
|
|
82
|
+
|
|
83
|
+
def _get_or_create_storage(self, parent, name):
|
|
84
|
+
for child in parent.children:
|
|
85
|
+
if child.obj_type in (self.STGTY_STORAGE, self.STGTY_ROOT) and child.name == name:
|
|
86
|
+
return child
|
|
87
|
+
storage = _Node(name, self.STGTY_STORAGE)
|
|
88
|
+
parent.children.append(storage)
|
|
89
|
+
return storage
|
|
90
|
+
|
|
91
|
+
def write(self, file_path):
|
|
92
|
+
self._build_storage_trees(self.root)
|
|
93
|
+
all_nodes = self._collect_nodes()
|
|
94
|
+
self._assign_directory_ids(all_nodes)
|
|
95
|
+
|
|
96
|
+
layout = self._calculate_layout(all_nodes)
|
|
97
|
+
|
|
98
|
+
header = self._build_header(layout)
|
|
99
|
+
fat_data = self._build_fat(layout)
|
|
100
|
+
fat_sectors = self._split_into_sectors(fat_data)
|
|
101
|
+
dir_data = self._build_directory(all_nodes)
|
|
102
|
+
dir_sectors = self._split_into_sectors(dir_data)
|
|
103
|
+
mini_fat_data = self._build_minifat(layout)
|
|
104
|
+
mini_fat_sectors = self._split_into_sectors(mini_fat_data) if mini_fat_data else []
|
|
105
|
+
stream_sector_map = self._build_stream_sectors(layout)
|
|
106
|
+
|
|
107
|
+
sectors = [b'\x00' * self.sector_size for _ in range(layout['total_sectors'])]
|
|
108
|
+
|
|
109
|
+
for i, sector_index in enumerate(layout['fat_sectors_list']):
|
|
110
|
+
sectors[sector_index] = fat_sectors[i]
|
|
111
|
+
|
|
112
|
+
for i, sector in enumerate(dir_sectors):
|
|
113
|
+
sectors[layout['dir_start'] + i] = sector
|
|
114
|
+
|
|
115
|
+
for i, sector in enumerate(mini_fat_sectors):
|
|
116
|
+
sectors[layout['mini_fat_start'] + i] = sector
|
|
117
|
+
|
|
118
|
+
for sector_index, data in stream_sector_map.items():
|
|
119
|
+
sectors[sector_index] = data
|
|
120
|
+
|
|
121
|
+
with open(file_path, 'wb') as f:
|
|
122
|
+
f.write(header)
|
|
123
|
+
for sector in sectors:
|
|
124
|
+
f.write(sector)
|
|
125
|
+
|
|
126
|
+
def _build_storage_trees(self, node):
|
|
127
|
+
if node.obj_type in (self.STGTY_STORAGE, self.STGTY_ROOT):
|
|
128
|
+
if node.children and not node.manual_tree:
|
|
129
|
+
if node.obj_type == self.STGTY_ROOT and self._try_build_excel_tree(node):
|
|
130
|
+
pass
|
|
131
|
+
else:
|
|
132
|
+
node.child = self._build_rb_tree(node.children)
|
|
133
|
+
for child in node.children:
|
|
134
|
+
self._build_storage_trees(child)
|
|
135
|
+
|
|
136
|
+
def _try_build_excel_tree(self, root):
|
|
137
|
+
names = {child.name: child for child in root.children}
|
|
138
|
+
if "EncryptedPackage" not in names or "EncryptionInfo" not in names or "\x06DataSpaces" not in names:
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
enc_info = names["EncryptionInfo"]
|
|
142
|
+
enc_pkg = names["EncryptedPackage"]
|
|
143
|
+
data_spaces = names["\x06DataSpaces"]
|
|
144
|
+
|
|
145
|
+
# Root tree
|
|
146
|
+
root.child = enc_info
|
|
147
|
+
enc_info.left = data_spaces
|
|
148
|
+
enc_info.right = enc_pkg
|
|
149
|
+
enc_pkg.left = None
|
|
150
|
+
enc_pkg.right = None
|
|
151
|
+
data_spaces.left = None
|
|
152
|
+
data_spaces.right = None
|
|
153
|
+
data_spaces.manual_tree = True
|
|
154
|
+
|
|
155
|
+
# Colors to match Excel's typical layout
|
|
156
|
+
root.color = self.COLOR_RED
|
|
157
|
+
enc_pkg.color = self.COLOR_RED
|
|
158
|
+
data_spaces.color = self.COLOR_RED
|
|
159
|
+
enc_info.color = self.COLOR_BLACK
|
|
160
|
+
|
|
161
|
+
# Build DataSpaces subtree
|
|
162
|
+
ds_children = {child.name: child for child in data_spaces.children}
|
|
163
|
+
required = {"Version", "DataSpaceMap", "DataSpaceInfo", "TransformInfo"}
|
|
164
|
+
if not required.issubset(ds_children.keys()):
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
version = ds_children["Version"]
|
|
168
|
+
data_space_map = ds_children["DataSpaceMap"]
|
|
169
|
+
data_space_info = ds_children["DataSpaceInfo"]
|
|
170
|
+
transform_info = ds_children["TransformInfo"]
|
|
171
|
+
|
|
172
|
+
data_spaces.child = data_space_map
|
|
173
|
+
data_space_map.left = version
|
|
174
|
+
data_space_map.right = data_space_info
|
|
175
|
+
version.left = None
|
|
176
|
+
version.right = None
|
|
177
|
+
data_space_info.left = None
|
|
178
|
+
data_space_info.right = transform_info
|
|
179
|
+
transform_info.left = None
|
|
180
|
+
transform_info.right = None
|
|
181
|
+
|
|
182
|
+
data_space_map.color = self.COLOR_BLACK
|
|
183
|
+
version.color = self.COLOR_BLACK
|
|
184
|
+
data_space_info.color = self.COLOR_BLACK
|
|
185
|
+
transform_info.color = self.COLOR_RED
|
|
186
|
+
data_space_info.manual_tree = True
|
|
187
|
+
transform_info.manual_tree = True
|
|
188
|
+
|
|
189
|
+
# DataSpaceInfo child
|
|
190
|
+
dsi_children = {child.name: child for child in data_space_info.children}
|
|
191
|
+
if "StrongEncryptionDataSpace" in dsi_children:
|
|
192
|
+
strong_ds = dsi_children["StrongEncryptionDataSpace"]
|
|
193
|
+
data_space_info.child = strong_ds
|
|
194
|
+
strong_ds.left = None
|
|
195
|
+
strong_ds.right = None
|
|
196
|
+
strong_ds.color = self.COLOR_BLACK
|
|
197
|
+
strong_ds.manual_tree = True
|
|
198
|
+
|
|
199
|
+
# TransformInfo child
|
|
200
|
+
ti_children = {child.name: child for child in transform_info.children}
|
|
201
|
+
if "StrongEncryptionTransform" in ti_children:
|
|
202
|
+
strong_tr = ti_children["StrongEncryptionTransform"]
|
|
203
|
+
transform_info.child = strong_tr
|
|
204
|
+
strong_tr.left = None
|
|
205
|
+
strong_tr.right = None
|
|
206
|
+
strong_tr.color = self.COLOR_BLACK
|
|
207
|
+
strong_tr.manual_tree = True
|
|
208
|
+
|
|
209
|
+
st_children = {child.name: child for child in strong_tr.children}
|
|
210
|
+
if "\x06Primary" in st_children:
|
|
211
|
+
primary = st_children["\x06Primary"]
|
|
212
|
+
strong_tr.child = primary
|
|
213
|
+
primary.left = None
|
|
214
|
+
primary.right = None
|
|
215
|
+
primary.color = self.COLOR_BLACK
|
|
216
|
+
primary.manual_tree = True
|
|
217
|
+
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
def _build_rb_tree(self, nodes):
|
|
221
|
+
nodes_sorted = sorted(nodes, key=lambda n: (n.name.upper(), n.name))
|
|
222
|
+
root = None
|
|
223
|
+
for node in nodes_sorted:
|
|
224
|
+
node.left = None
|
|
225
|
+
node.right = None
|
|
226
|
+
node.parent = None
|
|
227
|
+
node.color = self.COLOR_RED
|
|
228
|
+
root = self._rb_insert(root, node)
|
|
229
|
+
if root is not None:
|
|
230
|
+
root.color = self.COLOR_BLACK
|
|
231
|
+
return root
|
|
232
|
+
|
|
233
|
+
def _compare_names(self, a, b):
|
|
234
|
+
a_up = a.upper()
|
|
235
|
+
b_up = b.upper()
|
|
236
|
+
if a_up < b_up:
|
|
237
|
+
return -1
|
|
238
|
+
if a_up > b_up:
|
|
239
|
+
return 1
|
|
240
|
+
if a < b:
|
|
241
|
+
return -1
|
|
242
|
+
if a > b:
|
|
243
|
+
return 1
|
|
244
|
+
return 0
|
|
245
|
+
|
|
246
|
+
def _rb_insert(self, root, node):
|
|
247
|
+
parent = None
|
|
248
|
+
current = root
|
|
249
|
+
while current is not None:
|
|
250
|
+
parent = current
|
|
251
|
+
if self._compare_names(node.name, current.name) < 0:
|
|
252
|
+
current = current.left
|
|
253
|
+
else:
|
|
254
|
+
current = current.right
|
|
255
|
+
node.parent = parent
|
|
256
|
+
if parent is None:
|
|
257
|
+
root = node
|
|
258
|
+
elif self._compare_names(node.name, parent.name) < 0:
|
|
259
|
+
parent.left = node
|
|
260
|
+
else:
|
|
261
|
+
parent.right = node
|
|
262
|
+
|
|
263
|
+
return self._rb_insert_fixup(root, node)
|
|
264
|
+
|
|
265
|
+
def _rb_insert_fixup(self, root, node):
|
|
266
|
+
while node.parent is not None and node.parent.color == self.COLOR_RED:
|
|
267
|
+
if node.parent.parent is None:
|
|
268
|
+
node.parent.color = self.COLOR_BLACK
|
|
269
|
+
break
|
|
270
|
+
if node.parent == node.parent.parent.left:
|
|
271
|
+
uncle = node.parent.parent.right
|
|
272
|
+
if uncle is not None and uncle.color == self.COLOR_RED:
|
|
273
|
+
node.parent.color = self.COLOR_BLACK
|
|
274
|
+
uncle.color = self.COLOR_BLACK
|
|
275
|
+
node.parent.parent.color = self.COLOR_RED
|
|
276
|
+
node = node.parent.parent
|
|
277
|
+
else:
|
|
278
|
+
if node == node.parent.right:
|
|
279
|
+
node = node.parent
|
|
280
|
+
root = self._rotate_left(root, node)
|
|
281
|
+
node.parent.color = self.COLOR_BLACK
|
|
282
|
+
node.parent.parent.color = self.COLOR_RED
|
|
283
|
+
root = self._rotate_right(root, node.parent.parent)
|
|
284
|
+
else:
|
|
285
|
+
uncle = node.parent.parent.left
|
|
286
|
+
if uncle is not None and uncle.color == self.COLOR_RED:
|
|
287
|
+
node.parent.color = self.COLOR_BLACK
|
|
288
|
+
uncle.color = self.COLOR_BLACK
|
|
289
|
+
node.parent.parent.color = self.COLOR_RED
|
|
290
|
+
node = node.parent.parent
|
|
291
|
+
else:
|
|
292
|
+
if node == node.parent.left:
|
|
293
|
+
node = node.parent
|
|
294
|
+
root = self._rotate_right(root, node)
|
|
295
|
+
node.parent.color = self.COLOR_BLACK
|
|
296
|
+
node.parent.parent.color = self.COLOR_RED
|
|
297
|
+
root = self._rotate_left(root, node.parent.parent)
|
|
298
|
+
return root
|
|
299
|
+
|
|
300
|
+
def _rotate_left(self, root, x):
|
|
301
|
+
y = x.right
|
|
302
|
+
x.right = y.left
|
|
303
|
+
if y.left is not None:
|
|
304
|
+
y.left.parent = x
|
|
305
|
+
y.parent = x.parent
|
|
306
|
+
if x.parent is None:
|
|
307
|
+
root = y
|
|
308
|
+
elif x == x.parent.left:
|
|
309
|
+
x.parent.left = y
|
|
310
|
+
else:
|
|
311
|
+
x.parent.right = y
|
|
312
|
+
y.left = x
|
|
313
|
+
x.parent = y
|
|
314
|
+
return root
|
|
315
|
+
|
|
316
|
+
def _rotate_right(self, root, y):
|
|
317
|
+
x = y.left
|
|
318
|
+
y.left = x.right
|
|
319
|
+
if x.right is not None:
|
|
320
|
+
x.right.parent = y
|
|
321
|
+
x.parent = y.parent
|
|
322
|
+
if y.parent is None:
|
|
323
|
+
root = x
|
|
324
|
+
elif y == y.parent.right:
|
|
325
|
+
y.parent.right = x
|
|
326
|
+
else:
|
|
327
|
+
y.parent.left = x
|
|
328
|
+
x.right = y
|
|
329
|
+
y.parent = x
|
|
330
|
+
return root
|
|
331
|
+
|
|
332
|
+
def _collect_nodes(self):
|
|
333
|
+
nodes = []
|
|
334
|
+
|
|
335
|
+
def walk(node):
|
|
336
|
+
nodes.append(node)
|
|
337
|
+
for child in node.children:
|
|
338
|
+
walk(child)
|
|
339
|
+
|
|
340
|
+
walk(self.root)
|
|
341
|
+
return nodes
|
|
342
|
+
|
|
343
|
+
def _assign_directory_ids(self, nodes):
|
|
344
|
+
for idx, node in enumerate(nodes):
|
|
345
|
+
node.did = idx
|
|
346
|
+
|
|
347
|
+
def _calculate_layout(self, nodes):
|
|
348
|
+
layout = {}
|
|
349
|
+
entries_per_sector = self.sector_size // 128
|
|
350
|
+
dir_sectors_needed = (len(nodes) + entries_per_sector - 1) // entries_per_sector
|
|
351
|
+
|
|
352
|
+
streams = [n for n in nodes if n.obj_type == self.STGTY_STREAM]
|
|
353
|
+
mini_streams = [s for s in streams if s.stream_size < self.MINI_STREAM_CUTOFF and not s.force_regular]
|
|
354
|
+
regular_streams = [s for s in streams if s.stream_size >= self.MINI_STREAM_CUTOFF or s.force_regular]
|
|
355
|
+
|
|
356
|
+
# Build mini stream data
|
|
357
|
+
mini_stream_data = b''
|
|
358
|
+
mini_sector_index = 0
|
|
359
|
+
for s in mini_streams:
|
|
360
|
+
s.starting_sector = mini_sector_index
|
|
361
|
+
data = s.data or b''
|
|
362
|
+
s.stream_size = len(data)
|
|
363
|
+
pad = (-len(data)) % self.MINI_SECTOR_SIZE
|
|
364
|
+
mini_stream_data += data + (b'\x00' * pad)
|
|
365
|
+
mini_sector_index += (len(data) + pad) // self.MINI_SECTOR_SIZE
|
|
366
|
+
|
|
367
|
+
mini_stream_size = len(mini_stream_data)
|
|
368
|
+
if mini_stream_size and mini_stream_size < 1920:
|
|
369
|
+
pad = 1920 - mini_stream_size
|
|
370
|
+
mini_stream_data += b'\x00' * pad
|
|
371
|
+
mini_stream_size = 1920
|
|
372
|
+
mini_sector_index = mini_stream_size // self.MINI_SECTOR_SIZE
|
|
373
|
+
self.root.stream_size = mini_stream_size
|
|
374
|
+
|
|
375
|
+
mini_fat_entries = mini_sector_index
|
|
376
|
+
entries_per_fat_sector = self.sector_size // 4
|
|
377
|
+
mini_fat_sectors = (mini_fat_entries + entries_per_fat_sector - 1) // entries_per_fat_sector
|
|
378
|
+
mini_stream_sectors = (mini_stream_size + self.sector_size - 1) // self.sector_size if mini_stream_size else 0
|
|
379
|
+
|
|
380
|
+
regular_stream_sectors = 0
|
|
381
|
+
for s in regular_streams:
|
|
382
|
+
s.stream_size = len(s.data or b'')
|
|
383
|
+
regular_stream_sectors += (s.stream_size + self.sector_size - 1) // self.sector_size
|
|
384
|
+
|
|
385
|
+
total_data_sectors = dir_sectors_needed + mini_fat_sectors + mini_stream_sectors + regular_stream_sectors
|
|
386
|
+
fat_sectors_needed = (total_data_sectors + entries_per_fat_sector - 1) // entries_per_fat_sector
|
|
387
|
+
|
|
388
|
+
for _ in range(10):
|
|
389
|
+
total_sectors = fat_sectors_needed + total_data_sectors
|
|
390
|
+
new_fat_sectors = (total_sectors + entries_per_fat_sector - 1) // entries_per_fat_sector
|
|
391
|
+
if new_fat_sectors == fat_sectors_needed:
|
|
392
|
+
break
|
|
393
|
+
fat_sectors_needed = new_fat_sectors
|
|
394
|
+
|
|
395
|
+
# Keep at least 3 FAT sectors for Excel compatibility
|
|
396
|
+
if fat_sectors_needed < 3:
|
|
397
|
+
fat_sectors_needed = 3
|
|
398
|
+
|
|
399
|
+
# Layout with FAT sector 0 and remaining FAT sectors at end
|
|
400
|
+
current_sector = 0
|
|
401
|
+
fat_sectors_list = [0]
|
|
402
|
+
current_sector += 1
|
|
403
|
+
|
|
404
|
+
layout['dir_start'] = current_sector
|
|
405
|
+
layout['dir_sectors'] = dir_sectors_needed
|
|
406
|
+
current_sector += dir_sectors_needed
|
|
407
|
+
|
|
408
|
+
layout['mini_fat_start'] = current_sector if mini_fat_sectors else self.ENDOFCHAIN
|
|
409
|
+
layout['mini_fat_sectors'] = mini_fat_sectors
|
|
410
|
+
current_sector += mini_fat_sectors
|
|
411
|
+
|
|
412
|
+
layout['mini_stream_start'] = current_sector if mini_stream_sectors else self.ENDOFCHAIN
|
|
413
|
+
layout['mini_stream_sectors'] = mini_stream_sectors
|
|
414
|
+
current_sector += mini_stream_sectors
|
|
415
|
+
|
|
416
|
+
for s in regular_streams:
|
|
417
|
+
sectors_needed = (s.stream_size + self.sector_size - 1) // self.sector_size
|
|
418
|
+
if sectors_needed == 0:
|
|
419
|
+
sectors_needed = 1
|
|
420
|
+
s.starting_sector = current_sector
|
|
421
|
+
current_sector += sectors_needed
|
|
422
|
+
|
|
423
|
+
for _ in range(fat_sectors_needed - 1):
|
|
424
|
+
fat_sectors_list.append(current_sector)
|
|
425
|
+
current_sector += 1
|
|
426
|
+
|
|
427
|
+
layout['stream_info'] = {
|
|
428
|
+
'mini_streams': mini_streams,
|
|
429
|
+
'regular_streams': regular_streams,
|
|
430
|
+
'mini_stream_data': mini_stream_data
|
|
431
|
+
}
|
|
432
|
+
layout['total_sectors'] = current_sector
|
|
433
|
+
layout['fat_sectors'] = fat_sectors_needed
|
|
434
|
+
layout['fat_sectors_list'] = fat_sectors_list
|
|
435
|
+
|
|
436
|
+
if mini_stream_sectors:
|
|
437
|
+
self.root.starting_sector = layout['mini_stream_start']
|
|
438
|
+
else:
|
|
439
|
+
self.root.starting_sector = self.ENDOFCHAIN
|
|
440
|
+
self.root.stream_size = 0
|
|
441
|
+
|
|
442
|
+
return layout
|
|
443
|
+
|
|
444
|
+
def _build_header(self, layout):
|
|
445
|
+
header = io.BytesIO()
|
|
446
|
+
|
|
447
|
+
header.write(struct.pack('<Q', self.HEADER_SIGNATURE))
|
|
448
|
+
header.write(self.HEADER_CLSID)
|
|
449
|
+
header.write(struct.pack('<H', self.MINOR_VERSION))
|
|
450
|
+
header.write(struct.pack('<H', self.major_version))
|
|
451
|
+
header.write(struct.pack('<H', self.BYTE_ORDER))
|
|
452
|
+
header.write(struct.pack('<H', self.sector_shift))
|
|
453
|
+
header.write(struct.pack('<H', 6))
|
|
454
|
+
header.write(b'\x00' * 6)
|
|
455
|
+
|
|
456
|
+
if self.major_version == self.MAJOR_VERSION_4:
|
|
457
|
+
header.write(struct.pack('<I', layout['total_sectors']))
|
|
458
|
+
else:
|
|
459
|
+
header.write(struct.pack('<I', 0))
|
|
460
|
+
|
|
461
|
+
header.write(struct.pack('<I', layout['fat_sectors']))
|
|
462
|
+
header.write(struct.pack('<I', layout['dir_start']))
|
|
463
|
+
header.write(struct.pack('<I', 0))
|
|
464
|
+
header.write(struct.pack('<I', self.MINI_STREAM_CUTOFF))
|
|
465
|
+
header.write(struct.pack('<I', layout['mini_fat_start']))
|
|
466
|
+
header.write(struct.pack('<I', layout['mini_fat_sectors']))
|
|
467
|
+
header.write(struct.pack('<I', self.ENDOFCHAIN))
|
|
468
|
+
header.write(struct.pack('<I', 0))
|
|
469
|
+
|
|
470
|
+
for i in range(109):
|
|
471
|
+
if i < len(layout['fat_sectors_list']):
|
|
472
|
+
header.write(struct.pack('<I', layout['fat_sectors_list'][i]))
|
|
473
|
+
else:
|
|
474
|
+
header.write(struct.pack('<I', self.FREESECT))
|
|
475
|
+
|
|
476
|
+
data = header.getvalue()
|
|
477
|
+
if len(data) < 512:
|
|
478
|
+
data += b'\x00' * (512 - len(data))
|
|
479
|
+
return data[:512]
|
|
480
|
+
|
|
481
|
+
def _build_fat(self, layout):
|
|
482
|
+
total_sectors = layout['total_sectors']
|
|
483
|
+
fat = [self.FREESECT] * total_sectors
|
|
484
|
+
|
|
485
|
+
# FAT sectors
|
|
486
|
+
for sector in layout['fat_sectors_list']:
|
|
487
|
+
fat[sector] = self.FATSECT
|
|
488
|
+
|
|
489
|
+
# Directory sectors
|
|
490
|
+
for i in range(layout['dir_sectors']):
|
|
491
|
+
sector = layout['dir_start'] + i
|
|
492
|
+
next_sector = layout['dir_start'] + i + 1
|
|
493
|
+
fat[sector] = next_sector if i < layout['dir_sectors'] - 1 else self.ENDOFCHAIN
|
|
494
|
+
|
|
495
|
+
# MiniFAT sectors
|
|
496
|
+
if layout['mini_fat_sectors']:
|
|
497
|
+
for i in range(layout['mini_fat_sectors']):
|
|
498
|
+
sector = layout['mini_fat_start'] + i
|
|
499
|
+
next_sector = layout['mini_fat_start'] + i + 1
|
|
500
|
+
fat[sector] = next_sector if i < layout['mini_fat_sectors'] - 1 else self.ENDOFCHAIN
|
|
501
|
+
|
|
502
|
+
# Mini stream sectors
|
|
503
|
+
if layout['mini_stream_sectors']:
|
|
504
|
+
for i in range(layout['mini_stream_sectors']):
|
|
505
|
+
sector = layout['mini_stream_start'] + i
|
|
506
|
+
next_sector = layout['mini_stream_start'] + i + 1
|
|
507
|
+
fat[sector] = next_sector if i < layout['mini_stream_sectors'] - 1 else self.ENDOFCHAIN
|
|
508
|
+
|
|
509
|
+
# Regular stream sectors
|
|
510
|
+
for s in layout['stream_info']['regular_streams']:
|
|
511
|
+
sectors_needed = (s.stream_size + self.sector_size - 1) // self.sector_size
|
|
512
|
+
for i in range(sectors_needed):
|
|
513
|
+
sector = s.starting_sector + i
|
|
514
|
+
next_sector = s.starting_sector + i + 1
|
|
515
|
+
fat[sector] = next_sector if i < sectors_needed - 1 else self.ENDOFCHAIN
|
|
516
|
+
|
|
517
|
+
fat_bytes = io.BytesIO()
|
|
518
|
+
for entry in fat:
|
|
519
|
+
fat_bytes.write(struct.pack('<I', entry))
|
|
520
|
+
|
|
521
|
+
entries_per_sector = self.sector_size // 4
|
|
522
|
+
total_fat_entries = layout['fat_sectors'] * entries_per_sector
|
|
523
|
+
entries_written = len(fat)
|
|
524
|
+
for _ in range(entries_written, total_fat_entries):
|
|
525
|
+
fat_bytes.write(struct.pack('<I', self.FREESECT))
|
|
526
|
+
|
|
527
|
+
return fat_bytes.getvalue()
|
|
528
|
+
|
|
529
|
+
def _build_minifat(self, layout):
|
|
530
|
+
mini_streams = layout['stream_info']['mini_streams']
|
|
531
|
+
mini_stream_data = layout['stream_info']['mini_stream_data']
|
|
532
|
+
if not mini_stream_data:
|
|
533
|
+
return b''
|
|
534
|
+
|
|
535
|
+
mini_sector_count = len(mini_stream_data) // self.MINI_SECTOR_SIZE
|
|
536
|
+
mini_fat = [self.FREESECT] * mini_sector_count
|
|
537
|
+
|
|
538
|
+
for s in mini_streams:
|
|
539
|
+
start = s.starting_sector
|
|
540
|
+
sectors = (s.stream_size + self.MINI_SECTOR_SIZE - 1) // self.MINI_SECTOR_SIZE
|
|
541
|
+
for i in range(sectors):
|
|
542
|
+
idx = start + i
|
|
543
|
+
mini_fat[idx] = (start + i + 1) if i < sectors - 1 else self.ENDOFCHAIN
|
|
544
|
+
|
|
545
|
+
data = io.BytesIO()
|
|
546
|
+
for entry in mini_fat:
|
|
547
|
+
data.write(struct.pack('<I', entry))
|
|
548
|
+
|
|
549
|
+
entries_per_sector = self.sector_size // 4
|
|
550
|
+
total_entries = layout['mini_fat_sectors'] * entries_per_sector
|
|
551
|
+
entries_written = len(mini_fat)
|
|
552
|
+
for _ in range(entries_written, total_entries):
|
|
553
|
+
data.write(struct.pack('<I', self.FREESECT))
|
|
554
|
+
|
|
555
|
+
return data.getvalue()
|
|
556
|
+
|
|
557
|
+
def _build_directory(self, nodes):
|
|
558
|
+
directory = io.BytesIO()
|
|
559
|
+
|
|
560
|
+
for node in nodes:
|
|
561
|
+
left = node.left.did if node.left is not None else 0xFFFFFFFF
|
|
562
|
+
right = node.right.did if node.right is not None else 0xFFFFFFFF
|
|
563
|
+
child = node.child.did if node.child is not None else 0xFFFFFFFF
|
|
564
|
+
|
|
565
|
+
starting_sector = node.starting_sector
|
|
566
|
+
stream_size = node.stream_size
|
|
567
|
+
if node.obj_type == self.STGTY_STORAGE:
|
|
568
|
+
# Excel uses 0 for storage entries with no stream.
|
|
569
|
+
starting_sector = 0
|
|
570
|
+
stream_size = 0
|
|
571
|
+
|
|
572
|
+
entry = self._create_directory_entry(
|
|
573
|
+
name=node.name,
|
|
574
|
+
obj_type=node.obj_type,
|
|
575
|
+
color=node.color,
|
|
576
|
+
left_sibling=left,
|
|
577
|
+
right_sibling=right,
|
|
578
|
+
child_did=child,
|
|
579
|
+
clsid=b'\x00' * 16,
|
|
580
|
+
state_bits=0,
|
|
581
|
+
creation_time=0,
|
|
582
|
+
modified_time=0,
|
|
583
|
+
starting_sector=starting_sector,
|
|
584
|
+
stream_size=stream_size
|
|
585
|
+
)
|
|
586
|
+
directory.write(entry)
|
|
587
|
+
|
|
588
|
+
entries_per_sector = self.sector_size // 128
|
|
589
|
+
total_entries = ((len(nodes) + entries_per_sector - 1) // entries_per_sector) * entries_per_sector
|
|
590
|
+
for _ in range(len(nodes), total_entries):
|
|
591
|
+
directory.write(b'\xFF' * 128)
|
|
592
|
+
|
|
593
|
+
return directory.getvalue()
|
|
594
|
+
|
|
595
|
+
def _create_directory_entry(self, name, obj_type, color, left_sibling, right_sibling,
|
|
596
|
+
child_did, clsid, state_bits, creation_time, modified_time,
|
|
597
|
+
starting_sector, stream_size):
|
|
598
|
+
entry = io.BytesIO()
|
|
599
|
+
|
|
600
|
+
name_bytes = name.encode('utf-16le')
|
|
601
|
+
if len(name_bytes) > 62:
|
|
602
|
+
name_bytes = name_bytes[:62]
|
|
603
|
+
entry.write(name_bytes)
|
|
604
|
+
entry.write(b'\x00' * (64 - len(name_bytes)))
|
|
605
|
+
|
|
606
|
+
name_len = len(name_bytes) + 2
|
|
607
|
+
entry.write(struct.pack('<H', name_len))
|
|
608
|
+
entry.write(struct.pack('<B', obj_type))
|
|
609
|
+
entry.write(struct.pack('<B', color))
|
|
610
|
+
entry.write(struct.pack('<I', left_sibling))
|
|
611
|
+
entry.write(struct.pack('<I', right_sibling))
|
|
612
|
+
entry.write(struct.pack('<I', child_did))
|
|
613
|
+
entry.write(clsid)
|
|
614
|
+
entry.write(struct.pack('<I', state_bits))
|
|
615
|
+
entry.write(struct.pack('<Q', creation_time))
|
|
616
|
+
entry.write(struct.pack('<Q', modified_time))
|
|
617
|
+
entry.write(struct.pack('<I', starting_sector))
|
|
618
|
+
entry.write(struct.pack('<Q', stream_size))
|
|
619
|
+
|
|
620
|
+
data = entry.getvalue()
|
|
621
|
+
assert len(data) == 128
|
|
622
|
+
return data
|
|
623
|
+
|
|
624
|
+
def _build_stream_sectors(self, layout):
|
|
625
|
+
sector_map = {}
|
|
626
|
+
|
|
627
|
+
mini_stream_data = layout['stream_info']['mini_stream_data']
|
|
628
|
+
if mini_stream_data:
|
|
629
|
+
offset = 0
|
|
630
|
+
for i in range(layout['mini_stream_sectors']):
|
|
631
|
+
sector = mini_stream_data[offset:offset + self.sector_size]
|
|
632
|
+
if len(sector) < self.sector_size:
|
|
633
|
+
sector += b'\x00' * (self.sector_size - len(sector))
|
|
634
|
+
sector_map[layout['mini_stream_start'] + i] = sector
|
|
635
|
+
offset += self.sector_size
|
|
636
|
+
|
|
637
|
+
for s in layout['stream_info']['regular_streams']:
|
|
638
|
+
data = s.data or b''
|
|
639
|
+
offset = 0
|
|
640
|
+
sectors_needed = (s.stream_size + self.sector_size - 1) // self.sector_size
|
|
641
|
+
for i in range(sectors_needed):
|
|
642
|
+
sector = data[offset:offset + self.sector_size]
|
|
643
|
+
if len(sector) < self.sector_size:
|
|
644
|
+
sector += b'\x00' * (self.sector_size - len(sector))
|
|
645
|
+
sector_map[s.starting_sector + i] = sector
|
|
646
|
+
offset += self.sector_size
|
|
647
|
+
|
|
648
|
+
return sector_map
|
|
649
|
+
|
|
650
|
+
def _split_into_sectors(self, data):
|
|
651
|
+
sectors = []
|
|
652
|
+
offset = 0
|
|
653
|
+
while offset < len(data):
|
|
654
|
+
sector = data[offset:offset + self.sector_size]
|
|
655
|
+
if len(sector) < self.sector_size:
|
|
656
|
+
sector += b'\x00' * (self.sector_size - len(sector))
|
|
657
|
+
sectors.append(sector)
|
|
658
|
+
offset += self.sector_size
|
|
659
|
+
return sectors
|