extract-msg 0.48.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 (95) hide show
  1. extract_msg/__init__.py +71 -0
  2. extract_msg/__main__.py +115 -0
  3. extract_msg/_rtf/__init__.py +21 -0
  4. extract_msg/_rtf/create_doc.py +29 -0
  5. extract_msg/_rtf/inject_rtf.py +174 -0
  6. extract_msg/_rtf/token.py +35 -0
  7. extract_msg/_rtf/tokenize_rtf.py +253 -0
  8. extract_msg/attachments/__init__.py +144 -0
  9. extract_msg/attachments/attachment.py +188 -0
  10. extract_msg/attachments/attachment_base.py +730 -0
  11. extract_msg/attachments/broken_att.py +42 -0
  12. extract_msg/attachments/custom_att.py +173 -0
  13. extract_msg/attachments/custom_att_handler/__init__.py +80 -0
  14. extract_msg/attachments/custom_att_handler/custom_handler.py +134 -0
  15. extract_msg/attachments/custom_att_handler/lnk_obj_att.py +109 -0
  16. extract_msg/attachments/custom_att_handler/outlook_image_dib.py +141 -0
  17. extract_msg/attachments/emb_msg_att.py +134 -0
  18. extract_msg/attachments/signed_att.py +234 -0
  19. extract_msg/attachments/unsupported_att.py +40 -0
  20. extract_msg/attachments/web_att.py +74 -0
  21. extract_msg/constants/__init__.py +294 -0
  22. extract_msg/constants/ps.py +46 -0
  23. extract_msg/constants/re.py +42 -0
  24. extract_msg/constants/st.py +110 -0
  25. extract_msg/data/logging-config/logging-nt.json +64 -0
  26. extract_msg/data/logging-config/logging-posix.json +64 -0
  27. extract_msg/encoding/__init__.py +277 -0
  28. extract_msg/encoding/_dt/__init__.py +4 -0
  29. extract_msg/encoding/_dt/_mac_ce.py +8 -0
  30. extract_msg/encoding/_dt/_mac_cyrillic.py +8 -0
  31. extract_msg/encoding/_dt/_mac_greek.py +8 -0
  32. extract_msg/encoding/_dt/_mac_iceland.py +8 -0
  33. extract_msg/encoding/_dt/_mac_turkish.py +8 -0
  34. extract_msg/encoding/_dt/_win874_dec.py +8 -0
  35. extract_msg/encoding/_dt/_win950_dec.py +6 -0
  36. extract_msg/encoding/utils.py +329 -0
  37. extract_msg/enums.py +2067 -0
  38. extract_msg/exceptions.py +170 -0
  39. extract_msg/msg_classes/__init__.py +49 -0
  40. extract_msg/msg_classes/appointment.py +195 -0
  41. extract_msg/msg_classes/calendar.py +92 -0
  42. extract_msg/msg_classes/calendar_base.py +496 -0
  43. extract_msg/msg_classes/contact.py +1335 -0
  44. extract_msg/msg_classes/journal.py +131 -0
  45. extract_msg/msg_classes/meeting_cancellation.py +108 -0
  46. extract_msg/msg_classes/meeting_exception.py +55 -0
  47. extract_msg/msg_classes/meeting_forward.py +115 -0
  48. extract_msg/msg_classes/meeting_related.py +65 -0
  49. extract_msg/msg_classes/meeting_request.py +189 -0
  50. extract_msg/msg_classes/meeting_response.py +74 -0
  51. extract_msg/msg_classes/message.py +12 -0
  52. extract_msg/msg_classes/message_base.py +1382 -0
  53. extract_msg/msg_classes/message_signed.py +17 -0
  54. extract_msg/msg_classes/message_signed_base.py +168 -0
  55. extract_msg/msg_classes/msg.py +1172 -0
  56. extract_msg/msg_classes/post.py +53 -0
  57. extract_msg/msg_classes/sticky_note.py +67 -0
  58. extract_msg/msg_classes/task.py +363 -0
  59. extract_msg/msg_classes/task_request.py +86 -0
  60. extract_msg/null_date.py +34 -0
  61. extract_msg/ole_writer.py +1086 -0
  62. extract_msg/open_msg.py +204 -0
  63. extract_msg/properties/__init__.py +23 -0
  64. extract_msg/properties/named.py +481 -0
  65. extract_msg/properties/prop.py +506 -0
  66. extract_msg/properties/properties_store.py +476 -0
  67. extract_msg/py.typed +0 -0
  68. extract_msg/recipient.py +362 -0
  69. extract_msg/structures/__init__.py +34 -0
  70. extract_msg/structures/_helpers.py +345 -0
  71. extract_msg/structures/business_card.py +422 -0
  72. extract_msg/structures/cfoas.py +108 -0
  73. extract_msg/structures/contact_link_entry.py +51 -0
  74. extract_msg/structures/dev_mode_a.py +621 -0
  75. extract_msg/structures/dv_target_device.py +168 -0
  76. extract_msg/structures/entry_id.py +697 -0
  77. extract_msg/structures/misc_id.py +222 -0
  78. extract_msg/structures/mon_stream.py +55 -0
  79. extract_msg/structures/odt.py +74 -0
  80. extract_msg/structures/ole_pres.py +302 -0
  81. extract_msg/structures/ole_stream_struct.py +117 -0
  82. extract_msg/structures/recurrence_pattern.py +199 -0
  83. extract_msg/structures/report_tag.py +127 -0
  84. extract_msg/structures/system_time.py +158 -0
  85. extract_msg/structures/time_zone_definition.py +111 -0
  86. extract_msg/structures/time_zone_struct.py +102 -0
  87. extract_msg/structures/toc_entry.py +137 -0
  88. extract_msg/structures/tz_rule.py +148 -0
  89. extract_msg/utils.py +1270 -0
  90. extract_msg-0.48.2.dist-info/LICENSE.txt +674 -0
  91. extract_msg-0.48.2.dist-info/METADATA +321 -0
  92. extract_msg-0.48.2.dist-info/RECORD +95 -0
  93. extract_msg-0.48.2.dist-info/WHEEL +5 -0
  94. extract_msg-0.48.2.dist-info/entry_points.txt +2 -0
  95. extract_msg-0.48.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1086 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ __all__ = [
5
+ 'DirectoryEntry',
6
+ 'OleWriter',
7
+ ]
8
+
9
+
10
+ import copy
11
+ import re
12
+
13
+ from typing import (
14
+ Dict, Iterator, List, Optional, SupportsBytes, Tuple, TYPE_CHECKING,
15
+ Union
16
+ )
17
+
18
+ from . import constants
19
+ from .constants import MSG_PATH
20
+ from .enums import Color, DirectoryEntryType
21
+ from .exceptions import TooManySectorsError
22
+ from .utils import ceilDiv, dictGetCasedKey, inputToMsgPath
23
+ from olefile.olefile import OleDirectoryEntry, OleFileIO
24
+ from red_black_dict_mod import RedBlackTree
25
+
26
+
27
+ # Allow for nice type checking.
28
+ if TYPE_CHECKING:
29
+ from .msg_classes import MSGFile
30
+
31
+
32
+ class DirectoryEntry:
33
+ """
34
+ An internal representation of a stream or storage in the OleWriter.
35
+
36
+ Originals should be inaccessible outside of the class.
37
+ """
38
+ name: str = ''
39
+ rightChild: Optional[DirectoryEntry] = None
40
+ leftChild: Optional[DirectoryEntry] = None
41
+ childTreeRoot: Optional[DirectoryEntry] = None
42
+ stateBits: int = 0
43
+ creationTime: int = 0
44
+ modifiedTime: int = 0
45
+ type: DirectoryEntryType = DirectoryEntryType.UNALLOCATED
46
+
47
+ # These get set after things have been sorted by the red black tree.
48
+ id: int = -1
49
+ # This is the ID for the left child. The terminology in the docs is really
50
+ # annoying.
51
+ leftSiblingID: int = 0xFFFFFFFF
52
+ rightSiblingID: int = 0xFFFFFFFF
53
+ # This is the ID for the root of the child tree, if any.
54
+ childID: int = 0xFFFFFFFF
55
+ startingSectorLocation: int = 0
56
+ color: Color = Color.BLACK
57
+
58
+ clsid: bytes = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
59
+ data: bytes = b''
60
+
61
+ def __bytes__(self) -> bytes:
62
+ return self.toBytes()
63
+
64
+ def toBytes(self) -> bytes:
65
+ """
66
+ Converts the entry to bytes to be writen to a file.
67
+ """
68
+ # First write the name and the name length.
69
+ if len(self.name) > 31:
70
+ raise ValueError('Name is too long for directory entry.')
71
+ if len(self.name) < 1:
72
+ raise ValueError('Directory entry must have a name.')
73
+ if re.search('/\\\\:!', self.name):
74
+ raise ValueError('Directory entry name contains an illegal character.')
75
+
76
+ nameBytes = self.name.encode('utf-16-le')
77
+
78
+ return constants.st.ST_CF_DIR_ENTRY.pack(
79
+ nameBytes,
80
+ len(nameBytes) + 2,
81
+ self.type,
82
+ self.color,
83
+ self.leftSiblingID,
84
+ self.rightSiblingID,
85
+ self.childID,
86
+ self.clsid,
87
+ self.stateBits,
88
+ self.creationTime,
89
+ self.modifiedTime,
90
+ self.startingSectorLocation,
91
+ getattr(self, 'streamSize', len(self.data)),
92
+ )
93
+
94
+
95
+
96
+ class OleWriter:
97
+ """
98
+ Takes data to write to a compound binary format file, as specified in
99
+ [MS-CFB].
100
+ """
101
+ def __init__(self, rootClsid: bytes = constants.DEFAULT_CLSID):
102
+ self.__rootEntry = DirectoryEntry()
103
+ self.__rootEntry.name = "Root Entry"
104
+ self.__rootEntry.type = DirectoryEntryType.ROOT_STORAGE
105
+ self.__rootEntry.clsid = rootClsid
106
+ # The root entry will always exist, so this must be at least 1.
107
+ self.__dirEntryCount = 1
108
+ self.__dirEntries = {}
109
+ self.__largeEntries: List[DirectoryEntry] = []
110
+ self.__largeEntrySectors = 0
111
+ self.__numMinifatSectors = 0
112
+
113
+ # In a future version, this will be setable as an optional argument.
114
+ self.__version = 3
115
+
116
+ def __getContainingStorage(self, path: List[str], entryExists: bool = True, create: bool = False) -> Dict:
117
+ """
118
+ Finds the storage ``dict`` internally where the entry specified by
119
+ :param path: would be created.
120
+
121
+ :param entryExists: If ``True``, throws an error when the requested
122
+ entry does not yet exist.
123
+ :param create: If ``True``, creates missing storages with default
124
+ settings.
125
+
126
+ :raises OSError: If :param create: is ``False`` and the path could not
127
+ be found. Also raised if :param entryExists: is ``True`` and the
128
+ requested entry does not exist.
129
+ :raises ValueError: Tried to access an interal stream or tried to use
130
+ both the create option and the entryExists option is ``True``.
131
+
132
+ :returns: The storage ``dict`` that the entry is in.
133
+ """
134
+ if not path:
135
+ raise OSError('Path cannot be empty.')
136
+
137
+ # Quick check for incompatability between create and entryExists.
138
+ if create and entryExists:
139
+ raise ValueError(':param create: and :param entryExists: cannot both be True (an entry cannot exist if it is being created).')
140
+
141
+ # Check that the path is not an internal entry. Given the validation on
142
+ # paths that most functions should do because of the call to
143
+ # inputToMsgPath, this shouldn't actually be necessary.
144
+ if any(x.startswith('::') for x in path):
145
+ raise ValueError('Found internal name in path.')
146
+
147
+ _dir = self.__dirEntries
148
+
149
+ for index, name in enumerate(path[:-1]):
150
+ # If no entry in the current stream matches the path, raise an
151
+ # OSError, *unless* the option to create storages is True.
152
+ if name.lower() not in map(str.lower, _dir.keys()):
153
+ if create:
154
+ self.addEntry(path[:index + 1], storage = True)
155
+ else:
156
+ raise OSError(f'Entry not found: {name}')
157
+ _dir = _dir[dictGetCasedKey(_dir, name)]
158
+
159
+ # If the current item is not a storage and we have more to the path,
160
+ # raise an OSError.
161
+ if not isinstance(_dir, dict):
162
+ raise OSError('Attempted to access children of a stream.')
163
+
164
+ if entryExists and path[-1].lower() not in map(str.lower, _dir.keys()):
165
+ raise OSError(f'Entry not found: {path[-1]}')
166
+
167
+ return _dir
168
+
169
+ def __getEntry(self, path: List[str]) -> DirectoryEntry:
170
+ """
171
+ Finds and returns an existing ``DirectoryEntry`` instance in the writer.
172
+
173
+ :raises OSError: If the entry does not exist.
174
+ :raises ValueError: If access to an internal item is attempted.
175
+ """
176
+ _dir = self.__getContainingStorage(path)
177
+ item = _dir[dictGetCasedKey(_dir, path[-1])]
178
+ if isinstance(item, dict):
179
+ return item['::DirectoryEntry']
180
+ else:
181
+ return item
182
+
183
+ def __modifyEntry(self, entry: DirectoryEntry, **kwargs):
184
+ """
185
+ Edits the DirectoryEntry with the data provided.
186
+
187
+ Common code used for :meth:`addEntry` and :meth:`editEntry`.
188
+
189
+ :raises TypeError: Attempted to modify the data of a storage.
190
+ :raises ValueError: Some part of the data given to modify the various
191
+ properties was invalid. See the the listed methods for details.
192
+ """
193
+ # Extract the arguments.
194
+ data = kwargs.get('data')
195
+ clsid = kwargs.get('clsid')
196
+ creationTime = kwargs.get('creationTime')
197
+ modifiedTime = kwargs.get('modifiedTime')
198
+ stateBits = kwargs.get('stateBits')
199
+
200
+ # I don't like that I have repeated if statements for checking each of
201
+ # the arguments, but I need to make sure nothing changes if something is
202
+ # invalid.
203
+ if data is not None:
204
+ if entry.type is not DirectoryEntryType.STREAM:
205
+ raise TypeError('Cannot set the data of a storage object.')
206
+ if not isinstance(data, bytes):
207
+ try:
208
+ data = bytes(data)
209
+ except Exception:
210
+ raise ValueError('Data must be a bytes instance or convertable to bytes if set.')
211
+ # Check the length of data. In future versions, this may be a
212
+ # different check which is done when swapping between version 3 and
213
+ # 4 of the compound file binary file format.
214
+ if len(data) > 0x80000000:
215
+ raise ValueError('Current version of extract_msg does not support streams greater than 2 GB in OLE files.')
216
+
217
+ if clsid is not None:
218
+ if not isinstance(clsid, bytes):
219
+ raise ValueError('CLSID must be bytes.')
220
+ if len(clsid) != 16:
221
+ raise ValueError('CLSID must be 16 bytes.')
222
+
223
+ if creationTime is not None:
224
+ if entry.type is DirectoryEntryType.STREAM:
225
+ raise ValueError('Modification of creation time cannot be done on a stream.')
226
+ if not isinstance(creationTime, int) or creationTime < 0 or creationTime > 0xFFFFFFFFFFFFFFFF:
227
+ raise ValueError('Creation time must be a positive 8 byte int.')
228
+
229
+ if modifiedTime is not None:
230
+ if entry.type is DirectoryEntryType.STREAM:
231
+ raise ValueError('Modification of modified time cannot be done on a stream.')
232
+ if not isinstance(modifiedTime, int) or modifiedTime < 0 or modifiedTime > 0xFFFFFFFFFFFFFFFF:
233
+ raise ValueError('Modified time must be a positive 8 byte int.')
234
+
235
+ if stateBits is not None:
236
+ if not isinstance(stateBits, int) or stateBits < 0 or stateBits > 0xFFFFFFFF:
237
+ raise ValueError('State bits must be a positive 4 byte int.')
238
+
239
+ # Now that all our checks have passed, let's set our data.
240
+ if data is not None:
241
+ entry.data = data
242
+ if clsid is not None:
243
+ entry.clsid = clsid
244
+ if creationTime is not None:
245
+ entry.creationTime = creationTime
246
+ if modifiedTime is not None:
247
+ entry.modifiedTime = modifiedTime
248
+ if stateBits is not None:
249
+ entry.stateBits = stateBits
250
+
251
+ def __recalculateSectors(self) -> None:
252
+ """
253
+ Recalculates several of the internal variables used for saving that
254
+ specify the number of sectors and where things should go.
255
+ """
256
+ self.__dirEntryCount = 0
257
+ self.__numMinifatSectors = 0
258
+ self.__largeEntries.clear()
259
+ self.__largeEntrySectors = 0
260
+
261
+ for entry in self.__walkEntries():
262
+ self.__dirEntryCount += 1
263
+ if entry.type == DirectoryEntryType.STREAM:
264
+ if len(entry.data) < 4096:
265
+ self.__numMinifatSectors += ceilDiv(len(entry.data), 64)
266
+ else:
267
+ self.__largeEntries.append(entry)
268
+ self.__largeEntrySectors += ceilDiv(len(entry.data), self.__sectorSize)
269
+
270
+ def __walkEntries(self) -> Iterator[DirectoryEntry]:
271
+ """
272
+ Returns a generator that will walk the entires recursively.
273
+
274
+ Each item returned by it will be a DirectoryEntry instance.
275
+ """
276
+ toProcess = [self.__dirEntries]
277
+ yield self.__rootEntry
278
+
279
+ while len(toProcess) > 0:
280
+ for name, item in toProcess.pop(0).items():
281
+ if not name.startswith('::'):
282
+ if isinstance(item, dict):
283
+ yield item['::DirectoryEntry']
284
+ toProcess.append(item)
285
+ else:
286
+ yield item
287
+
288
+ @property
289
+ def __dirEntsPerSector(self) -> int:
290
+ """
291
+ The number of Directory Entries that can fit in a sector.
292
+ """
293
+ return self.__sectorSize // 128
294
+
295
+ @property
296
+ def __linksPerSector(self) -> int:
297
+ """
298
+ The number of links per FAT/DIFAT sector.
299
+ """
300
+ return self.__sectorSize // 4
301
+
302
+ @property
303
+ def __miniSectorsPerSector(self) -> int:
304
+ """
305
+ The number of mini sectors that a regular sector will hold.
306
+ """
307
+ return self.__sectorSize // 64
308
+
309
+ @property
310
+ def __numberOfSectors(self) -> int:
311
+ # Most of this should be pretty self evident, but line by line the
312
+ # calculation is as such:
313
+ # 1. How many sectors are needed for the directory entries.
314
+ # 2. How many FAT sectors are needed for the MiniStream.
315
+ # 3. How many sectors are needed for the MiniFat (ceil divide #2 by 16).
316
+ # 4. The number of FAT sectors needed to store the larger data.
317
+ return ceilDiv(self.__dirEntryCount, 4) + \
318
+ self.__numMinifat + \
319
+ ceilDiv(self.__numMinifat, 16) + \
320
+ self.__largeEntrySectors
321
+
322
+ @property
323
+ def __numMinifat(self) -> int:
324
+ """
325
+ The number of FAT sectors needed to store the mini stream.
326
+ """
327
+ return ceilDiv(64 * self.__numMinifatSectors, self.__sectorSize)
328
+
329
+ @property
330
+ def __sectorSize(self) -> int:
331
+ """
332
+ The size of each sector, in bytes.
333
+ """
334
+ return 512 if self.__version == 3 else 4096
335
+
336
+ def _cleanupEntries(self) -> None:
337
+ """
338
+ Cleans up the node connections by walking the tree and removing
339
+ references that were added during writing.
340
+ """
341
+ self.__largeEntries.clear()
342
+ for entry in self.__walkEntries():
343
+ entry.id = -1
344
+ entry.leftChild = None
345
+ entry.rightChild = None
346
+ entry.childTreeRoot = None
347
+ entry.leftSiblingID = 0xFFFFFFFF
348
+ entry.rightSiblingID = 0xFFFFFFFF
349
+ entry.childID = 0xFFFFFFFF
350
+
351
+ def _getFatSectors(self) -> Tuple[int, int, int]:
352
+ """
353
+ Returns a tuple containing the number of FAT sectors, the number of
354
+ DIFAT sectors, and the total number of sectors the saved file will have.
355
+ """
356
+ # Right now we just use an annoying while loop to get the numbers.
357
+ numDifat = 0
358
+ # All divisions are ceiling divisions,.
359
+ numFat = ceilDiv(self.__numberOfSectors or 1, self.__linksPerSector - 1)
360
+ newNumFat = 1
361
+ while numFat != newNumFat:
362
+ numFat = newNumFat
363
+ numDifat = ceilDiv(max(numFat - 109, 0), self.__linksPerSector - 1)
364
+ newNumFat = ceilDiv(self.__numberOfSectors + numDifat, self.__linksPerSector - 1)
365
+
366
+ return (numFat, numDifat, self.__numberOfSectors + numDifat + numFat)
367
+
368
+ def _treeSort(self, startingSector: int) -> List[DirectoryEntry]:
369
+ """
370
+ Uses red-black trees to sort the internal data in preparation for
371
+ writing the file, returning a list, in order, of the entries to write.
372
+ """
373
+ # First, create the root entry.
374
+ root = copy.copy(self.__rootEntry)
375
+
376
+ # Add the location of the start of the mini stream.
377
+ root.startingSectorLocation = (startingSector + ceilDiv(self.__dirEntryCount, 4) + ceilDiv(self.__numMinifatSectors, self.__linksPerSector)) if self.__numMinifat > 0 else 0xFFFFFFFE
378
+ root.streamSize = self.__numMinifatSectors * 64
379
+ root.childTreeRoot = None
380
+ root.childID = 0xFFFFFFFF
381
+ entries = [root]
382
+
383
+ toProcess = [(root, self.__dirEntries)]
384
+ # Continue looping while there is more to process.
385
+ while toProcess:
386
+ entry, currentItem = toProcess.pop()
387
+ if not currentItem:
388
+ continue
389
+ # If the current item *only* has the directory's entry and no stream
390
+ # entries, we are actually done.
391
+ # Create a tree and add all the items to it. We add it with a key
392
+ # that is a tuple of the length (as shorter is *always* less than
393
+ # longer) and the uppercase name, and the value is the actual entry.
394
+ tree = RedBlackTree()
395
+ for name in currentItem:
396
+ if not name.startswith('::'):
397
+ val = currentItem[name]
398
+ # If we find a directory entry, then we need to add it to
399
+ # the processing list.
400
+ if isinstance(val, dict):
401
+ toProcess.append((val['::DirectoryEntry'], val))
402
+ val = val['::DirectoryEntry']
403
+
404
+ entries.append(val)
405
+
406
+ # Add the data to the tree.
407
+ tree.add((len(name), name.upper()), val)
408
+
409
+ # Now that everything is added, we need to take our root and add it
410
+ # as the child of the current entry.
411
+ entry.childTreeRoot = tree.value
412
+
413
+ # Now we need to go through each node and set it's data based on
414
+ # it's sort position.
415
+ for node in tree.in_order():
416
+ item = node.value
417
+ # Set the color immediately.
418
+ item.color = Color.BLACK if node.is_black else Color.RED
419
+
420
+ if node.left:
421
+ item.leftChild = node.left.value
422
+ else:
423
+ item.leftChild = None
424
+
425
+ if node.right:
426
+ item.rightChild = node.right.value
427
+ else:
428
+ item.rightChild = None
429
+
430
+ # Now that everything is connected, we loop over the entries list a few
431
+ # times and set the data values.
432
+ for _id, entry in enumerate(entries):
433
+ entry.id = _id
434
+
435
+ for entry in entries:
436
+ entry.leftSiblingID = entry.leftChild.id if entry.leftChild else 0xFFFFFFFF
437
+ entry.childID = entry.childTreeRoot.id if entry.childTreeRoot else 0xFFFFFFFF
438
+ entry.rightSiblingID = entry.rightChild.id if entry.rightChild else 0xFFFFFFFF
439
+
440
+ # Finally, let's figure out the sector IDs to be used for the mini data.
441
+ # We only need to do this for streams with a size less than 4096.
442
+
443
+ # Use this to track where the next thing goes in the mini FAT.
444
+ miniFATLocation = 0
445
+
446
+ for entry in entries:
447
+ if len(entry.data) == 0 and entry != entries[0]:
448
+ # If there is no data, just set the starting location to none.
449
+ entry.startingSectorLocation = 0xFFFFFFFE
450
+ elif entry.type == DirectoryEntryType.STREAM and len(entry.data) < 4096:
451
+ entry.startingSectorLocation = miniFATLocation
452
+ miniFATLocation += ceilDiv(len(entry.data), 64)
453
+
454
+ return entries
455
+
456
+ def _writeBeginning(self, f) -> int:
457
+ """
458
+ Writes the beginning to the file :param f:.
459
+
460
+ This includes the header, DIFAT, and FAT blocks.
461
+
462
+ :returns: The current sector number after all the data is written.
463
+
464
+ :raises TooMuchDataError: The number of sectors required for the file is
465
+ too large.
466
+ """
467
+ # Recalculate some things needed for saving.
468
+ self.__recalculateSectors()
469
+ # Since we are going to need these multiple times, get them now.
470
+ numFat, numDifat, totalSectors = self._getFatSectors()
471
+
472
+ # Check to make sure there isn't too much data to write.
473
+ if totalSectors > 0xFFFFFFFB:
474
+ raise TooManySectorsError('Data in OleWriter requires too many sectors to write to a version 3 file.')
475
+
476
+ # The ministream *cannot* be greater than 2 GB, so check that before
477
+ # writing anything. A minifat sector is 64 bytes, so the maximum amount
478
+ # of them is 0x2000000.
479
+ if self.__numMinifatSectors > 0x2000000:
480
+ raise TooManySectorsError('Data is OleWriter requires too many MiniFAT sectors.')
481
+
482
+ # Header signature.
483
+ f.write(b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1')
484
+ # Header CLSID.
485
+ f.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
486
+ # Minor version.
487
+ f.write(b'\x3E\x00')
488
+ # Major version. For now, we only support version 3, but support for
489
+ # version 4 is planned.
490
+ f.write(b'\x03\x00' if self.__version == 3 else b'\x04\x00')
491
+ # Byte order. Specifies that it is little endian.
492
+ f.write(b'\xFE\xFF')
493
+ # Sector shift.
494
+ f.write(b'\x09\x00' if self.__version == 3 else b'\x0C\x00')
495
+ # Mini sector shift.
496
+ f.write(b'\x06\x00')
497
+ # Reserved.
498
+ f.write(b'\x00\x00\x00\x00\x00\x00')
499
+ # Number of directory sectors. Version 3 says this *must* be 0.
500
+ f.write(constants.st.ST_LE_UI32.pack(0))
501
+ # Number of FAT sectors.
502
+ f.write(constants.st.ST_LE_UI32.pack(numFat))
503
+ # First directory sector location (Sector for the directory stream).
504
+ # We place that right after the DIFAT and FAT.
505
+ f.write(constants.st.ST_LE_UI32.pack(numFat + numDifat))
506
+ # Transation signature number.
507
+ f.write(b'\x00\x00\x00\x00')
508
+ # Mini stream cutoff size.
509
+ f.write(b'\x00\x10\x00\x00')
510
+ # First mini FAT sector location.
511
+ f.write(constants.st.ST_LE_UI32.pack((numFat + numDifat + ceilDiv(self.__dirEntryCount, 4)) if self.__numMinifat > 0 else 0xFFFFFFFE))
512
+ # Number of mini FAT sectors.
513
+ f.write(constants.st.ST_LE_UI32.pack(ceilDiv(self.__numMinifatSectors, self.__linksPerSector)))
514
+ # First DIFAT sector location. If there are none, set to 0xFFFFFFFE (End
515
+ # of chain).
516
+ f.write(constants.st.ST_LE_UI32.pack(0 if numDifat else 0xFFFFFFFE))
517
+ # Number of DIFAT sectors.
518
+ f.write(constants.st.ST_LE_UI32.pack(numDifat))
519
+
520
+ # To make life easier on me, I'm having the code start with the DIFAT
521
+ # followed by the FAT sectors, as I can write them all at once before
522
+ # writing the actual contents of the file.
523
+
524
+ # Write the DIFAT sectors.
525
+ for x in range(numFat):
526
+ # Quickly check if we have hit 109. If we have, and we are writing
527
+ # a version 4 file, we need to pad a bunch of null bytes.
528
+ if x == 109 and self.__version == 4:
529
+ f.write(b'\x00' * 3584)
530
+ # This kind of sucks to code, ngl.
531
+ if x > 109 and (x - 109) % (self.__linksPerSector - 1) == 0:
532
+ # If we are at the end of a DIFAT sector, write the jump.
533
+ f.write(constants.st.ST_LE_UI32.pack((x - 109) // (self.__linksPerSector - 1)))
534
+ # Write the next FAT sector location.
535
+ f.write(constants.st.ST_LE_UI32.pack(x + numDifat))
536
+
537
+ # Finally, fill out the last DIFAT sector with null entries.
538
+ if numFat > 109:
539
+ f.write(b'\xFF\xFF\xFF\xFF' * ((self.__linksPerSector - 1) - ((numFat - 109) % (self.__linksPerSector - 1))))
540
+ # Finally, make sure to write the end of chain marker for the DIFAT.
541
+ f.write(b'\xFE\xFF\xFF\xFF')
542
+ else:
543
+ f.write(b'\xFF\xFF\xFF\xFF' * (109 - numFat))
544
+
545
+ ### FAT.
546
+
547
+ # First, if we had any DIFAT sectors, write that the previous sectors
548
+ # were all a part of it.
549
+ f.write(b'\xFC\xFF\xFF\xFF' * numDifat)
550
+ # Second write that the next x sectors are all FAT sectors.
551
+ f.write(b'\xFD\xFF\xFF\xFF' * numFat)
552
+
553
+ offset = numDifat + numFat
554
+
555
+ # Fill in the values for the directory stream.
556
+ for x in range(offset + 1, offset + ceilDiv(self.__dirEntryCount, self.__dirEntsPerSector)):
557
+ f.write(constants.st.ST_LE_UI32.pack(x))
558
+
559
+ # Write the end of chain marker.
560
+ f.write(b'\xFE\xFF\xFF\xFF')
561
+
562
+ offset += ceilDiv(self.__dirEntryCount, self.__dirEntsPerSector)
563
+
564
+ # Check if we have minifat *at all* first.
565
+ if self.__numMinifatSectors > 0:
566
+ # Mini FAT chain.
567
+ for x in range(offset + 1, offset + ceilDiv(self.__numMinifat, 16)):
568
+ f.write(constants.st.ST_LE_UI32.pack(x))
569
+
570
+ # Write the end of chain marker.
571
+ f.write(b'\xFE\xFF\xFF\xFF')
572
+
573
+ offset += ceilDiv(self.__numMinifat, 16)
574
+
575
+ # The mini stream sectors.
576
+ for x in range(offset + 1, offset + self.__numMinifat):
577
+ f.write(constants.st.ST_LE_UI32.pack(x))
578
+
579
+ # Write the end of chain marker.
580
+ f.write(b'\xFE\xFF\xFF\xFF')
581
+
582
+ offset += self.__numMinifat
583
+
584
+ # Regular stream chains. These are the most complex to handle. We handle
585
+ # them by checking a list that was make of entries which were only added
586
+ # to that list if the size was more than 4096. The order in the list is
587
+ # how they will eventually be stored into the file correctly.
588
+ for entry in self.__largeEntries:
589
+ size = ceilDiv(len(entry.data), self.__sectorSize)
590
+ entry.startingSectorLocation = offset
591
+ for x in range(offset + 1, offset + size):
592
+ f.write(constants.st.ST_LE_UI32.pack(x))
593
+
594
+ # Write the end of chain marker.
595
+ f.write(b'\xFE\xFF\xFF\xFF')
596
+
597
+ offset += size
598
+
599
+ # Finally, fill fat with markers to specify no block exists.
600
+ freeSectors = totalSectors & (self.__linksPerSector - 1)
601
+ if freeSectors:
602
+ f.write(b'\xFF\xFF\xFF\xFF' * (self.__linksPerSector - freeSectors))
603
+
604
+ # Finally, return the current sector index for use in other places.
605
+ return numDifat + numFat
606
+
607
+ def _writeDirectoryEntries(self, f, startingSector: int) -> List[DirectoryEntry]:
608
+ """
609
+ Writes out all the directory entries. Returns the list generated.
610
+ """
611
+ entries = self._treeSort(startingSector)
612
+ for x in entries:
613
+ self._writeDirectoryEntry(f, x)
614
+ if len(entries) & 3:
615
+ f.write(((b'\x00\x00' * 34) + (b'\xFF\xFF' * 6) + (b'\x00\x00' * 24)) * (4 - (len(entries) & 3)))
616
+
617
+ return entries
618
+
619
+ def _writeDirectoryEntry(self, f, entry: DirectoryEntry) -> None:
620
+ """
621
+ Writes the directory entry to the file f.
622
+ """
623
+ f.write(bytes(entry))
624
+
625
+ def _writeFinal(self, f) -> None:
626
+ """
627
+ Writes the final sectors of the file, consisting of the streams too
628
+ large for the mini FAT.
629
+ """
630
+ for x in self.__largeEntries:
631
+ f.write(x.data)
632
+ if len(x.data) & (self.__sectorSize - 1):
633
+ f.write(b'\x00' * (self.__sectorSize - (len(x.data) & (self.__sectorSize - 1))))
634
+
635
+ def _writeMini(self, f, entries: List[DirectoryEntry]) -> None:
636
+ """
637
+ Writes the mini FAT followed by the full mini stream.
638
+ """
639
+ # For each of the entires that are streams and less than 4096.
640
+ currentSector = 0
641
+ for x in entries:
642
+ if x.type == DirectoryEntryType.STREAM and len(x.data) < 4096:
643
+ size = ceilDiv(len(x.data), 64)
644
+ for x in range(currentSector + 1, currentSector + size):
645
+ f.write(constants.st.ST_LE_UI32.pack(x))
646
+ if size > 0:
647
+ f.write(b'\xFE\xFF\xFF\xFF')
648
+ currentSector += size
649
+
650
+ # Finally, write the remaining slots.
651
+ if currentSector & (self.__linksPerSector - 1):
652
+ f.write(b'\xFF\xFF\xFF\xFF' * (self.__linksPerSector - (currentSector & (self.__linksPerSector - 1))))
653
+
654
+ # Write the mini stream.
655
+ for x in entries:
656
+ if len(x.data) > 0 and len(x.data) < 4096:
657
+ f.write(x.data)
658
+ if len(x.data) & 63:
659
+ f.write(b'\x00' * (64 - (len(x.data) & 63)))
660
+
661
+ # Pad the final mini stream block.
662
+ if self.__numMinifatSectors & (self.__miniSectorsPerSector - 1):
663
+ f.write((b'\x00' * 64) * (self.__miniSectorsPerSector - (self.__numMinifatSectors & (self.__miniSectorsPerSector - 1))))
664
+
665
+ def addEntry(self, path: MSG_PATH, data: Optional[Union[bytes, SupportsBytes]] = None, storage: bool = False, **kwargs) -> None:
666
+ """
667
+ Adds an entry to the OleWriter instance at the path specified, adding
668
+ storages with default settings where necessary. If the entry is not a
669
+ storage, :param data: *must* be set.
670
+
671
+ :param path: The path to add the entry at. Must not contain a path part
672
+ that is an already added stream.
673
+ :param data: The bytes for a stream or an object with a ``__bytes__``
674
+ method.
675
+ :param storage: If ``True``, the entry to add is a storage. Otherwise,
676
+ the entry is a stream.
677
+ :param clsid: The CLSID for the stream/storage. Must a a bytes instance
678
+ that is 16 bytes long.
679
+ :param creationTime: An 8 byte filetime int. Sets the creation time of
680
+ the entry. Not applicable to streams.
681
+ :param modifiedTime: An 8 byte filetime int. Sets the modification time
682
+ of the entry. Not applicable to streams.
683
+ :param stateBits: A 4 byte int. Sets the state bits, user-defined flags,
684
+ of the entry. For a stream, this *SHOULD* be unset.
685
+
686
+ :raises OSError: A stream was found on the path before the end or an
687
+ entry with the same name already exists.
688
+ :raises ValueError: Attempts to access an internal item.
689
+ :raises ValueError: The data provided is too large.
690
+ """
691
+ path = inputToMsgPath(path)
692
+ # First, find the current place in our dict to add the item.
693
+ _dir = self.__getContainingStorage(path, False, True)
694
+ # Now, check that the item *is not* already in our dict, as that would
695
+ # cause problems.
696
+ if path[-1].lower() in map(str.lower, _dir.keys()):
697
+ raise OSError('Cannot add an entry that already exists.')
698
+
699
+ # Create a new entry with basic data and insert it.
700
+ entry = DirectoryEntry()
701
+ entry.type = DirectoryEntryType.STORAGE if storage else DirectoryEntryType.STREAM
702
+ entry.name = path[-1]
703
+ self.__modifyEntry(entry, data = data, **kwargs)
704
+ if storage:
705
+ _dir[path[-1]] = {'::DirectoryEntry': entry}
706
+ else:
707
+ _dir[path[-1]] = entry
708
+
709
+ def addOleEntry(self, path: MSG_PATH, entry: OleDirectoryEntry, data: Optional[Union[bytes, SupportsBytes]] = None) -> None:
710
+ """
711
+ Uses the entry provided to add the data to the writer.
712
+
713
+ :raises OSError: Tried to add an entry to a path that has not yet
714
+ been added, tried to add as a child of a stream, or tried to add an
715
+ entry where one already exists under the same name.
716
+ :raises ValueError: The data provided is too large.
717
+ """
718
+ path = inputToMsgPath(path)
719
+ # First, find the current place in our dict to add the item.
720
+ _dir = self.__getContainingStorage(path, False)
721
+ # Now, check that the item *is not* already in our dict, as that would
722
+ # cause problems.
723
+ if path[-1].lower() in map(str.lower, _dir.keys()):
724
+ raise OSError('Cannot add an entry that already exists.')
725
+
726
+ # Now that we are in the right place, add our data.
727
+ newEntry = DirectoryEntry()
728
+ if entry.entry_type == DirectoryEntryType.STORAGE:
729
+ # Handle a storage entry.
730
+ # First, setup the values for the storage.
731
+ newEntry.name = entry.name
732
+ newEntry.type = DirectoryEntryType.STORAGE
733
+ newEntry.clsid = _unClsid(entry.clsid)
734
+ newEntry.stateBits = entry.dwUserFlags
735
+ newEntry.creationTime = entry.createTime
736
+ newEntry.modifiedTime = entry.modifyTime
737
+
738
+ # Finally add the dict to our tree of items.
739
+ _dir[path[-1]] = {'::DirectoryEntry': newEntry}
740
+ else:
741
+ # Handle a stream entry.
742
+ # First, setup the values for the stream.
743
+ newEntry.name = entry.name
744
+ newEntry.type = DirectoryEntryType.STREAM
745
+ newEntry.clsid = _unClsid(entry.clsid)
746
+ newEntry.stateBits = entry.dwUserFlags
747
+
748
+ # Next, handle the data.
749
+ data = data or b''
750
+ newEntry.data = bytes(data)
751
+ if len(newEntry.data) > 0x80000000:
752
+ raise ValueError('Current version of extract_msg does not support streams greater than 2 GB in OLE files.')
753
+
754
+ # Finally add the entry to out dict of entries.
755
+ _dir[path[-1]] = newEntry
756
+
757
+ self.__dirEntryCount += 1
758
+
759
+ def deleteEntry(self, path) -> None:
760
+ """
761
+ Deletes the entry specified by :param path:, including all children.
762
+
763
+ :raises OSError: If the entry does not exist or a part of the path that
764
+ is not the last was a stream.
765
+ :raises ValueError: Attempted to delete an internal data stream.
766
+ """
767
+ path = inputToMsgPath(path)
768
+ # Get the containing storage for the entry.
769
+ _dir = self.__getContainingStorage(path)
770
+
771
+ # The garbage collector will take care of all the loose items, so just
772
+ # remove the entry. Also, once again we deal with the case insensitive
773
+ # nature of the path. Even though comparisons are case insensitive, the
774
+ # path does remember the case used.
775
+ del _dir[dictGetCasedKey(_dir, path[-1])]
776
+
777
+ def editEntry(self, path: MSG_PATH, **kwargs) -> None:
778
+ """
779
+ Used to edit values of an entry by setting the specific kwargs. Set a
780
+ value to something other than None to set it.
781
+
782
+ :param data: The data of a stream. Will error if used for something
783
+ other than a stream. Must be bytes or convertable to bytes.
784
+ :param clsid: The CLSID for the stream/storage. Must a a bytes instance
785
+ that is 16 bytes long.
786
+ :param creationTime: An 8 byte filetime int. Sets the creation time of
787
+ the entry. Not applicable to streams.
788
+ :param modifiedTime: An 8 byte filetime int. Sets the modification time
789
+ of the entry. Not applicable to streams.
790
+ :param stateBits: A 4 byte int. Sets the state bits, user-defined flags,
791
+ of the entry. For a stream, this *SHOULD* be unset.
792
+
793
+ To convert a 32 character hexadecial CLSID into the bytes for this
794
+ function, the _unClsid function in the ole_writer submodule can be used.
795
+
796
+ :raises OSError: The entry does not exist in the file.
797
+ :raises TypeError: Attempted to modify the bytes of a storage.
798
+ :raises ValueError: The type of a parameter was wrong, or the data of a
799
+ parameter was invalid.
800
+ """
801
+ # First, find our entry to edit.
802
+ entry = self.__getEntry(inputToMsgPath(path))
803
+
804
+ # Send it to be modified using the arguments given.
805
+ self.__modifyEntry(entry, **kwargs)
806
+
807
+ def fromMsg(self, msg: MSGFile) -> None:
808
+ """
809
+ Copies the streams and stream information necessary from the MSG file.
810
+ """
811
+ # Get the root OLE entry's CLSID.
812
+ self.__rootEntry.clsid = _unClsid(msg._getOleEntry('/').clsid)
813
+
814
+ # List both storages and directories, but sort them by shortest length
815
+ # first to prevent errors.
816
+ entries = msg.listDir(True, True, False)
817
+ entries.sort(key = len)
818
+
819
+ for x in entries:
820
+ entry = msg._getOleEntry(x)
821
+ data = msg.getStream(x) if entry.entry_type == DirectoryEntryType.STREAM else None
822
+ # THe properties stream on embedded messages actualy needs to be
823
+ # transformed a little (*why* it is like that is a mystery to me).
824
+ # Basically we just need to add a "reserved" section to it in a
825
+ # specific place. So let's check if we are doing the properties
826
+ # stream and then if we are embedded.
827
+ if x[0] == '__properties_version1.0' and msg.prefixLen > 0:
828
+ data = data[:24] + b'\x00\x00\x00\x00\x00\x00\x00\x00' + data[24:]
829
+ self.addOleEntry(x, entry, data)
830
+
831
+ # Now check if it is an embedded file. If so, we need to copy the named
832
+ # properties streams (the metadata, not the values).
833
+ if msg.prefixLen > 0:
834
+ # Get the entry for the named properties directory and add it
835
+ # immediately if it exists. If it doesn't exist, this whole
836
+ # section will be skipped.
837
+ self.addOleEntry('__nameid_version1.0', msg._getOleEntry('__nameid_version1.0', False), None)
838
+
839
+ # Now that we know it exists, grab all the file inside and copy
840
+ # them to our root.
841
+ # Create our generator.
842
+ gen = (x for x in msg._oleListDir() if len(x) > 1 and x[0] == '__nameid_version1.0')
843
+ for x in gen:
844
+ self.addOleEntry(x, msg._getOleEntry(x, prefix = False), msg.getStream(x, prefix = False))
845
+
846
+ def fromOleFile(self, ole: OleFileIO, rootPath: MSG_PATH = []) -> None:
847
+ """
848
+ Copies all the streams from the proided OLE file into this writer.
849
+
850
+ NOTE: This method does *not* handle any special rule that may be
851
+ required by a format that uses the compound binary file format as a base
852
+ when extracting an embedded directory. For example, MSG files require
853
+ modification of an embedded properties stream when extracting an
854
+ embedded MSG file.
855
+
856
+ :param rootPath: A path (accepted by ``olefile.OleFileIO``) to the
857
+ directory to use as the root of the file. If not provided, the file
858
+ root will be used.
859
+
860
+ :raises OSError: If :param rootPath: does not exist in the file.
861
+ """
862
+ rootPath = inputToMsgPath(rootPath)
863
+
864
+ # Check if the root path is simply the top of the file.
865
+ if rootPath == []:
866
+ # Copy the clsid of the root entry.
867
+ self.__rootEntry.clsid = _unClsid(ole.direntries[0].clsid)
868
+ paths = {tuple(x): (x, ole.direntries[ole._find(x)]) for x in ole.listdir(True, True)}
869
+ else:
870
+ # If it is not the top of the file, we need to do some filtering.
871
+ # First get the CLSID from the entry the path points to.
872
+ try:
873
+ entry = ole.direntries[ole._find(rootPath)]
874
+ self.__rootEntry.clsid = _unClsid(entry.clsid)
875
+
876
+ except OSError as e:
877
+ if str(e) == 'file not found':
878
+ # Get the cause/context for the original exception and use
879
+ # it for the new exception. This hides the exception from
880
+ # OleFileIO.
881
+ context = e.__cause__ or e.__context__
882
+ raise OSError('Root path was not found in the OLE file.') from context
883
+ else:
884
+ raise
885
+
886
+ paths = {tuple(x[len(rootPath):]): (x, ole.direntries[ole._find(x)])
887
+ for x in ole.listdir(True, True) if len(x) > len(rootPath)}
888
+
889
+
890
+ # Copy all of the other entries. Ensure that directories come before
891
+ # their streams by sorting the paths.
892
+ for x in sorted(paths.keys()):
893
+ fullPath, entry = paths[x]
894
+
895
+ if entry.entry_type == DirectoryEntryType.STREAM:
896
+ with ole.openstream(fullPath) as f:
897
+ data = f.read()
898
+ else:
899
+ data = None
900
+
901
+ self.addOleEntry(x, entry, data)
902
+
903
+ def getEntry(self, path: MSG_PATH) -> DirectoryEntry:
904
+ """
905
+ Finds and returns a copy of an existing `DirectoryEntry` instance in the
906
+ writer. Use this method to check the internal status of an entry.
907
+
908
+ :raises OSError: If the entry does not exist.
909
+ :raises ValueError: If access to an internal item is attempted.
910
+ """
911
+ return copy.copy(self.__getEntry(inputToMsgPath(path)))
912
+
913
+ def listItems(self, streams: bool = True, storages: bool = False) -> List[List[str]]:
914
+ """
915
+ Returns a list of the specified items currently in the writter.
916
+
917
+ :param streams: If ``True``, includes the path for each stream in the
918
+ list.
919
+ :param storages: If ``True``, includes the path for each storage in the
920
+ list.
921
+ """
922
+ # We are actually abusing the walk function a bit here to life much
923
+ # easier. The way we do this is to look at the current directory that
924
+ # the walk function is giving information about and then deciding what
925
+ # parts of it we want to use. Once we have all the paths created, we
926
+ # will then sort and return it to give an output similar, if not
927
+ # identical, to OleFileIO.listdir. The mentioned method sorts keeping
928
+ # case in mind.
929
+ if not streams and not storages:
930
+ return []
931
+
932
+ paths = []
933
+ for currentDir, stor, stre in self.walk():
934
+ if storages:
935
+ for name in stor:
936
+ paths.append(currentDir + [name])
937
+ if streams:
938
+ for name in stre:
939
+ paths.append(currentDir + [name])
940
+
941
+ paths.sort()
942
+ return paths
943
+
944
+ def renameEntry(self, path: MSG_PATH, newName: str) -> None:
945
+ """
946
+ Changes the name of an entry, leaving it in it's current position.
947
+
948
+ :raises OSError: If the entry does not exist or an entry with the new
949
+ name already exists,
950
+ :raises ValueError: If access to an internal item is attempted or the
951
+ new name provided is invalid.
952
+ """
953
+ # First, validate the new name.
954
+ if not isinstance(newName, str):
955
+ raise ValueError('New name must be a string.')
956
+ if constants.re.INVALID_OLE_PATH.search(newName):
957
+ raise ValueError('Invalid character(s) in new name. Must not contain the following characters: \\//!:')
958
+ if len(newName) > 31:
959
+ raise ValueError('New name must be less than 32 characters.')
960
+
961
+ # Get the storage for our entry. Entry *must* exist.
962
+ _dir = self.__getContainingStorage(inputToMsgPath(path))
963
+
964
+ # See if an item in the storage already has that new name.
965
+ if newName.lower() in map(str.lower, _dir.keys()):
966
+ raise OSError('An entry with the new name already exists.')
967
+
968
+ # Get the original name.
969
+ originalName = dictGetCasedKey(_dir, path[-1])
970
+
971
+ # Get the entry to change.
972
+ entry = _dir[originalName]
973
+ if isinstance(entry, dict):
974
+ dirData = entry
975
+ entry = entry['::DirectoryEntry']
976
+ else:
977
+ dirData = None
978
+
979
+ # Change the name on the entry first.
980
+ entry.name = newName
981
+
982
+ # Now, we need to remove the item from the current storage and add it
983
+ # back with the new name.
984
+ del _dir[originalName]
985
+
986
+ if dirData is None:
987
+ _dir[newName] = entry
988
+ else:
989
+ _dir[newName] = dirData
990
+
991
+ def walk(self) -> Iterator[Tuple[List[str], List[str], List[str]]]:
992
+ """
993
+ Functional equivelent to ``os.walk``, but for going over the file
994
+ structure of the OLE file to be written. Unlike ``os.walk``, it takes
995
+ no arguments.
996
+
997
+ :returns: A tuple of three lists. The first is the path, as a list of
998
+ strings, for the directory (or an empty list for the root), the
999
+ second is a list of the storages in the current directory, and the
1000
+ last is a list of the streams. Streams and storages are sorted
1001
+ caselessly.
1002
+ """
1003
+ toProcess = [([], self.__dirEntries)]
1004
+
1005
+ # Go through the toProcess list, removing the last item every time to
1006
+ # mimic the behavior of os.walk.
1007
+ while toProcess:
1008
+ currentDir, dirDict = toProcess.pop()
1009
+ storages = []
1010
+ streams = []
1011
+ for name in sorted(dirDict.keys(), key = str.lower):
1012
+ if not name.startswith('::'):
1013
+ if isinstance(dirDict[name], dict):
1014
+ storages.append(name)
1015
+ toProcess.append((currentDir + [name], dirDict[name]))
1016
+ else:
1017
+ streams.append(name)
1018
+
1019
+ yield (currentDir, storages, streams)
1020
+
1021
+ def write(self, path) -> None:
1022
+ """
1023
+ Writes the data to the path specified.
1024
+
1025
+ If :param path: has a ``write`` method, the object will be used
1026
+ directly.
1027
+
1028
+ If a failure occurs, the file or IO device may have been modified.
1029
+
1030
+ :raises TooManySectorsError: The number of sectors requires for a part
1031
+ of writing is too large.
1032
+ """
1033
+ opened = False
1034
+
1035
+ # First, let's open the file if it is not a writable object.
1036
+ if hasattr(path, 'write') and hasattr(path.write, '__call__'):
1037
+ f = path
1038
+ else:
1039
+ f = open(path, 'wb')
1040
+ opened = True
1041
+
1042
+ # Make sure we close the file after everything, especially if there is
1043
+ # an error.
1044
+ try:
1045
+ # Write each section, transferring data between functions where
1046
+ # necessary.
1047
+ offset = self._writeBeginning(f)
1048
+ entries = self._writeDirectoryEntries(f, offset)
1049
+ self._writeMini(f, entries)
1050
+ self._writeFinal(f)
1051
+ finally:
1052
+ self._cleanupEntries()
1053
+
1054
+ if opened:
1055
+ f.close()
1056
+
1057
+
1058
+
1059
+ def _unClsid(clsid: str) -> bytes:
1060
+ """
1061
+ Converts the clsid from ``olefile.olefile._clsid`` back to bytes.
1062
+ """
1063
+ if not clsid:
1064
+ return b''
1065
+ clsid = clsid.replace('-', '')
1066
+ try:
1067
+ return bytes((
1068
+ int(clsid[6:8], 16),
1069
+ int(clsid[4:6], 16),
1070
+ int(clsid[2:4], 16),
1071
+ int(clsid[0:2], 16),
1072
+ int(clsid[10:12], 16),
1073
+ int(clsid[8:10], 16),
1074
+ int(clsid[14:16], 16),
1075
+ int(clsid[12:14], 16),
1076
+ int(clsid[16:18], 16),
1077
+ int(clsid[18:20], 16),
1078
+ int(clsid[20:22], 16),
1079
+ int(clsid[22:24], 16),
1080
+ int(clsid[24:26], 16),
1081
+ int(clsid[26:28], 16),
1082
+ int(clsid[28:30], 16),
1083
+ int(clsid[30:32], 16),
1084
+ ))
1085
+ except Exception:
1086
+ raise