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.
Files changed (93) hide show
  1. aspose_cells/__init__.py +88 -0
  2. aspose_cells/auto_filter.py +527 -0
  3. aspose_cells/cell.py +483 -0
  4. aspose_cells/cell_value_handler.py +319 -0
  5. aspose_cells/cells.py +779 -0
  6. aspose_cells/cfb_handler.py +445 -0
  7. aspose_cells/cfb_writer.py +659 -0
  8. aspose_cells/cfb_writer_minimal.py +337 -0
  9. aspose_cells/comment_xml.py +475 -0
  10. aspose_cells/conditional_format.py +1185 -0
  11. aspose_cells/csv_handler.py +690 -0
  12. aspose_cells/data_validation.py +911 -0
  13. aspose_cells/document_properties.py +356 -0
  14. aspose_cells/encryption_crypto.py +247 -0
  15. aspose_cells/encryption_params.py +138 -0
  16. aspose_cells/hyperlink.py +372 -0
  17. aspose_cells/json_handler.py +185 -0
  18. aspose_cells/markdown_handler.py +583 -0
  19. aspose_cells/shared_strings.py +101 -0
  20. aspose_cells/style.py +841 -0
  21. aspose_cells/workbook.py +499 -0
  22. aspose_cells/workbook_hash_password.py +68 -0
  23. aspose_cells/workbook_properties.py +712 -0
  24. aspose_cells/worksheet.py +570 -0
  25. aspose_cells/worksheet_properties.py +1239 -0
  26. aspose_cells/xlsx_encryptor.py +403 -0
  27. aspose_cells/xml_autofilter_loader.py +195 -0
  28. aspose_cells/xml_autofilter_saver.py +173 -0
  29. aspose_cells/xml_conditional_format_loader.py +215 -0
  30. aspose_cells/xml_conditional_format_saver.py +351 -0
  31. aspose_cells/xml_datavalidation_loader.py +239 -0
  32. aspose_cells/xml_datavalidation_saver.py +245 -0
  33. aspose_cells/xml_hyperlink_handler.py +323 -0
  34. aspose_cells/xml_loader.py +986 -0
  35. aspose_cells/xml_properties_loader.py +512 -0
  36. aspose_cells/xml_properties_saver.py +607 -0
  37. aspose_cells/xml_saver.py +1306 -0
  38. aspose_cells_foss-26.2.2.dist-info/METADATA +190 -0
  39. aspose_cells_foss-26.2.2.dist-info/RECORD +41 -0
  40. {aspose_cells_foss-25.12.1.dist-info → aspose_cells_foss-26.2.2.dist-info}/WHEEL +1 -1
  41. aspose_cells_foss-26.2.2.dist-info/top_level.txt +1 -0
  42. aspose/__init__.py +0 -14
  43. aspose/cells/__init__.py +0 -31
  44. aspose/cells/cell.py +0 -350
  45. aspose/cells/constants.py +0 -44
  46. aspose/cells/converters/__init__.py +0 -13
  47. aspose/cells/converters/csv_converter.py +0 -55
  48. aspose/cells/converters/json_converter.py +0 -46
  49. aspose/cells/converters/markdown_converter.py +0 -453
  50. aspose/cells/drawing/__init__.py +0 -17
  51. aspose/cells/drawing/anchor.py +0 -172
  52. aspose/cells/drawing/collection.py +0 -233
  53. aspose/cells/drawing/image.py +0 -338
  54. aspose/cells/formats.py +0 -80
  55. aspose/cells/formula/__init__.py +0 -10
  56. aspose/cells/formula/evaluator.py +0 -360
  57. aspose/cells/formula/functions.py +0 -433
  58. aspose/cells/formula/tokenizer.py +0 -340
  59. aspose/cells/io/__init__.py +0 -27
  60. aspose/cells/io/csv/__init__.py +0 -8
  61. aspose/cells/io/csv/reader.py +0 -88
  62. aspose/cells/io/csv/writer.py +0 -98
  63. aspose/cells/io/factory.py +0 -138
  64. aspose/cells/io/interfaces.py +0 -48
  65. aspose/cells/io/json/__init__.py +0 -8
  66. aspose/cells/io/json/reader.py +0 -126
  67. aspose/cells/io/json/writer.py +0 -119
  68. aspose/cells/io/md/__init__.py +0 -8
  69. aspose/cells/io/md/reader.py +0 -161
  70. aspose/cells/io/md/writer.py +0 -334
  71. aspose/cells/io/models.py +0 -64
  72. aspose/cells/io/xlsx/__init__.py +0 -9
  73. aspose/cells/io/xlsx/constants.py +0 -312
  74. aspose/cells/io/xlsx/image_writer.py +0 -311
  75. aspose/cells/io/xlsx/reader.py +0 -284
  76. aspose/cells/io/xlsx/writer.py +0 -931
  77. aspose/cells/plugins/__init__.py +0 -6
  78. aspose/cells/plugins/docling_backend/__init__.py +0 -7
  79. aspose/cells/plugins/docling_backend/backend.py +0 -535
  80. aspose/cells/plugins/markitdown_plugin/__init__.py +0 -15
  81. aspose/cells/plugins/markitdown_plugin/plugin.py +0 -128
  82. aspose/cells/range.py +0 -210
  83. aspose/cells/style.py +0 -287
  84. aspose/cells/utils/__init__.py +0 -54
  85. aspose/cells/utils/coordinates.py +0 -68
  86. aspose/cells/utils/exceptions.py +0 -43
  87. aspose/cells/utils/validation.py +0 -102
  88. aspose/cells/workbook.py +0 -352
  89. aspose/cells/worksheet.py +0 -670
  90. aspose_cells_foss-25.12.1.dist-info/METADATA +0 -189
  91. aspose_cells_foss-25.12.1.dist-info/RECORD +0 -53
  92. aspose_cells_foss-25.12.1.dist-info/entry_points.txt +0 -2
  93. 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