cfbpy 0.0.1__tar.gz
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.
- cfbpy-0.0.1/.gitignore +8 -0
- cfbpy-0.0.1/LICENSE +21 -0
- cfbpy-0.0.1/PKG-INFO +16 -0
- cfbpy-0.0.1/README.md +2 -0
- cfbpy-0.0.1/pyproject.toml +22 -0
- cfbpy-0.0.1/src/cfbpy/__init__.py +0 -0
- cfbpy-0.0.1/src/cfbpy/cfb.py +367 -0
- cfbpy-0.0.1/src/cfbpy/directory.py +298 -0
cfbpy-0.0.1/.gitignore
ADDED
cfbpy-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Chaemin Lim
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cfbpy-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: cfbpy
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python module for compressing/decompressing compound file binary format (CFB)
|
|
5
|
+
Project-URL: Homepage, https://github.com/killerwhalee/cfbpy
|
|
6
|
+
Project-URL: Issues, https://github.com/killerwhalee/cfbpy/issues
|
|
7
|
+
Author-email: killerwhalee <killerwhalee@kaist.ac.kr>
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# cfbpy
|
|
16
|
+
Python module for compressing/decompressing compound file binary format (CFB)
|
cfbpy-0.0.1/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cfbpy"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="killerwhalee", email="killerwhalee@kaist.ac.kr" },
|
|
10
|
+
]
|
|
11
|
+
description = "Python module for compressing/decompressing compound file binary format (CFB)"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.12"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/killerwhalee/cfbpy"
|
|
22
|
+
Issues = "https://github.com/killerwhalee/cfbpy/issues"
|
|
File without changes
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
from olefile import OleFileIO
|
|
2
|
+
import os, struct
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import directory
|
|
6
|
+
|
|
7
|
+
# Header constants
|
|
8
|
+
HEADER_SIGNATURE = b"\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1"
|
|
9
|
+
MINOR_VERSION = 0x003E
|
|
10
|
+
MAJOR_VERSION = 0x0003
|
|
11
|
+
BYTE_ORDER = 0xFFFE
|
|
12
|
+
MINI_SECTOR_SHIFT = 0x0006
|
|
13
|
+
MINI_STREAM_CUTOFF_SIZE = 0x00001000
|
|
14
|
+
|
|
15
|
+
# Sector constants
|
|
16
|
+
MAXREGSECT = 0xFFFFFFFA #: (-6) maximum SECT
|
|
17
|
+
DIFSECT = 0xFFFFFFFC #: (-4) denotes a DIFAT sector in a FAT
|
|
18
|
+
FATSECT = 0xFFFFFFFD #: (-3) denotes a FAT sector in a FAT
|
|
19
|
+
ENDOFCHAIN = 0xFFFFFFFE #: (-2) end of a virtual stream chain
|
|
20
|
+
FREESECT = 0xFFFFFFFF #: (-1) unallocated sector
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CompoundFile:
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
# File metadata
|
|
26
|
+
self.file_name = ""
|
|
27
|
+
|
|
28
|
+
# Header data
|
|
29
|
+
self.header_signature = HEADER_SIGNATURE
|
|
30
|
+
self.header_clsid = 0
|
|
31
|
+
self.minor_version = MINOR_VERSION
|
|
32
|
+
self.major_version = MAJOR_VERSION
|
|
33
|
+
self.byte_order = BYTE_ORDER
|
|
34
|
+
self.sector_shift = 0x0009 if self.major_version == 0x0003 else 0x000C
|
|
35
|
+
self.mini_sector_shift = MINI_SECTOR_SHIFT
|
|
36
|
+
self.num_dir_sectors = 0
|
|
37
|
+
self.num_fat_sectors = 0
|
|
38
|
+
self.first_dir_sector = 0
|
|
39
|
+
self.transaction_signature_number = 0
|
|
40
|
+
self.mini_stream_cutoff_size = MINI_STREAM_CUTOFF_SIZE
|
|
41
|
+
self.first_mini_fat_sector = 0
|
|
42
|
+
self.num_mini_fat_sectors = 0
|
|
43
|
+
self.first_difat_sector = 0
|
|
44
|
+
self.num_difat_sectors = 0
|
|
45
|
+
|
|
46
|
+
# File Allocation Table (FAT)
|
|
47
|
+
self.fat = []
|
|
48
|
+
self.mini_fat = []
|
|
49
|
+
self.difat = []
|
|
50
|
+
|
|
51
|
+
# Sector data
|
|
52
|
+
self.sectors = []
|
|
53
|
+
self.mini_sectors = []
|
|
54
|
+
|
|
55
|
+
# Directories
|
|
56
|
+
self.root_directory = directory.Directory()
|
|
57
|
+
self.num_dir = 1 # Starts from 1 due to root entry
|
|
58
|
+
|
|
59
|
+
# Insert root entry to root directory
|
|
60
|
+
self.root_directory.insert(
|
|
61
|
+
directory.EntryData(
|
|
62
|
+
"Root Entry",
|
|
63
|
+
stream_id=0,
|
|
64
|
+
obj_type=directory.OBJTY_ROOT,
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def search_directory(self, path):
|
|
69
|
+
"""
|
|
70
|
+
Search path in directory
|
|
71
|
+
|
|
72
|
+
Returns entry with given path if found. Return `None` if there is no such path in directory.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
current_entry = self.root_directory.root
|
|
77
|
+
|
|
78
|
+
for name in path.split("/"):
|
|
79
|
+
# Skip if name is blank (double slash or root directory)
|
|
80
|
+
if name == "":
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# Search name for current entry
|
|
84
|
+
current_entry = current_entry.data.child.search_name(name=name)
|
|
85
|
+
|
|
86
|
+
# Return `None` if current_entry is NIL
|
|
87
|
+
if current_entry.data is None:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
# Return search result
|
|
91
|
+
return current_entry
|
|
92
|
+
|
|
93
|
+
def insert_directory(self, path, data: directory.EntryData):
|
|
94
|
+
"""
|
|
95
|
+
Insert new entry as child of other entry with given path.
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
if (entry := self.search_directory(path)) is None:
|
|
100
|
+
raise Exception(f"Given path not found: {path}")
|
|
101
|
+
|
|
102
|
+
# Update entry data
|
|
103
|
+
data.stream_id = self.num_dir
|
|
104
|
+
data.parent = entry
|
|
105
|
+
|
|
106
|
+
# Insert new entry as child of parent entry
|
|
107
|
+
entry.data.child.insert(data)
|
|
108
|
+
self.num_dir += 1
|
|
109
|
+
|
|
110
|
+
def write_sector(self, data: bytes):
|
|
111
|
+
"""
|
|
112
|
+
Write data stream to the sector.
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
# Get sector index for data
|
|
117
|
+
sector_index = len(self.fat)
|
|
118
|
+
|
|
119
|
+
# Get sector size
|
|
120
|
+
sector_size = 1 << self.sector_shift
|
|
121
|
+
|
|
122
|
+
# Separate data into head/tail
|
|
123
|
+
head, tail = data[:sector_size], data[sector_size:]
|
|
124
|
+
|
|
125
|
+
# Write head to sector right before last head
|
|
126
|
+
while tail:
|
|
127
|
+
self.sectors.append(head)
|
|
128
|
+
self.fat.append(len(self.sectors))
|
|
129
|
+
|
|
130
|
+
head, tail = tail[:sector_size], tail[sector_size:]
|
|
131
|
+
|
|
132
|
+
# Write last head to sector and end FAT chain
|
|
133
|
+
self.sectors.append(struct.pack(f"<{sector_size}s", head))
|
|
134
|
+
self.fat.append(ENDOFCHAIN)
|
|
135
|
+
|
|
136
|
+
# Return first sector number of stream
|
|
137
|
+
return sector_index
|
|
138
|
+
|
|
139
|
+
def write_mini_sector(self, data: bytes):
|
|
140
|
+
"""
|
|
141
|
+
Write mini data stream to the sector.
|
|
142
|
+
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
# Get sector index for data
|
|
146
|
+
sector_index = len(self.mini_fat)
|
|
147
|
+
|
|
148
|
+
# Get mini sector size
|
|
149
|
+
mini_sector_size = 1 << self.mini_sector_shift
|
|
150
|
+
|
|
151
|
+
# Separate data into head/tail
|
|
152
|
+
head, tail = data[:mini_sector_size], data[mini_sector_size:]
|
|
153
|
+
|
|
154
|
+
# Write head to sector right before last head
|
|
155
|
+
while tail:
|
|
156
|
+
self.mini_sectors.append(head)
|
|
157
|
+
self.mini_fat.append(len(self.mini_sectors))
|
|
158
|
+
|
|
159
|
+
head, tail = tail[:mini_sector_size], tail[mini_sector_size:]
|
|
160
|
+
|
|
161
|
+
# Write last head to sector and end MINIFAT chain
|
|
162
|
+
self.mini_sectors.append(
|
|
163
|
+
struct.pack(f"<{mini_sector_size}s", head),
|
|
164
|
+
)
|
|
165
|
+
self.mini_fat.append(ENDOFCHAIN)
|
|
166
|
+
|
|
167
|
+
# Return first sector number of stream
|
|
168
|
+
return sector_index
|
|
169
|
+
|
|
170
|
+
def write_fat(self, data: bytes):
|
|
171
|
+
"""
|
|
172
|
+
Write fat to the sector and write difat array.
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
# Get sector size
|
|
177
|
+
sector_size = 1 << self.sector_shift
|
|
178
|
+
|
|
179
|
+
# Separate data into head/tail
|
|
180
|
+
head, tail = data[:sector_size], data[sector_size:]
|
|
181
|
+
|
|
182
|
+
# Write head to sector until there is no head left
|
|
183
|
+
while head:
|
|
184
|
+
self.difat.append(len(self.sectors))
|
|
185
|
+
self.sectors.append(struct.pack(f"<{sector_size}s", head))
|
|
186
|
+
|
|
187
|
+
head, tail = tail[:sector_size], tail[sector_size:]
|
|
188
|
+
|
|
189
|
+
def open(self, src):
|
|
190
|
+
"""
|
|
191
|
+
Open compound file and create CompoundFile instance.
|
|
192
|
+
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
raise NotImplementedError
|
|
196
|
+
|
|
197
|
+
def save(self, dest):
|
|
198
|
+
"""
|
|
199
|
+
Save compound file from CompoundFile instance.
|
|
200
|
+
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
# Write header sector
|
|
204
|
+
header_data = struct.pack(
|
|
205
|
+
"<8s16xHHHHH6xIIIIIIIII",
|
|
206
|
+
self.header_signature,
|
|
207
|
+
self.minor_version,
|
|
208
|
+
self.major_version,
|
|
209
|
+
self.byte_order,
|
|
210
|
+
self.sector_shift,
|
|
211
|
+
self.mini_sector_shift,
|
|
212
|
+
self.num_dir_sectors,
|
|
213
|
+
self.num_fat_sectors,
|
|
214
|
+
self.first_dir_sector,
|
|
215
|
+
self.transaction_signature_number,
|
|
216
|
+
self.mini_stream_cutoff_size,
|
|
217
|
+
self.first_mini_fat_sector,
|
|
218
|
+
self.num_mini_fat_sectors,
|
|
219
|
+
self.first_difat_sector,
|
|
220
|
+
self.num_difat_sectors,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Open file path to save data
|
|
224
|
+
with open(dest, "wb") as fp:
|
|
225
|
+
# Write header
|
|
226
|
+
fp.write(header_data)
|
|
227
|
+
|
|
228
|
+
# Write difat
|
|
229
|
+
for entry in self.difat:
|
|
230
|
+
fp.write(struct.pack("<I", entry))
|
|
231
|
+
|
|
232
|
+
# Patch rest of difat entry
|
|
233
|
+
for _ in range(109 - len(self.difat)):
|
|
234
|
+
fp.write(struct.pack("<I", 0xFFFFFFFF))
|
|
235
|
+
|
|
236
|
+
# Write sectors
|
|
237
|
+
fp.write(b"".join(self.sectors))
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def decompress(src, dest=None):
|
|
241
|
+
"""
|
|
242
|
+
Decompress compound file and create directory at destination.
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
# Set path same as `src` if dest is not set
|
|
247
|
+
if not dest:
|
|
248
|
+
dest, _ = os.path.splitext(src)
|
|
249
|
+
|
|
250
|
+
os.makedirs(dest, exist_ok=True)
|
|
251
|
+
|
|
252
|
+
# Open compound file
|
|
253
|
+
olefile = OleFileIO(src)
|
|
254
|
+
|
|
255
|
+
# Iterate every stream in compound file
|
|
256
|
+
for *storage_path, stream_path in olefile.listdir():
|
|
257
|
+
# Create storge as directory
|
|
258
|
+
full_path = dest + "/" + "/".join(storage_path)
|
|
259
|
+
os.makedirs(full_path, exist_ok=True)
|
|
260
|
+
|
|
261
|
+
# Read stream and export as file
|
|
262
|
+
real_path = full_path + "/" + stream_path
|
|
263
|
+
|
|
264
|
+
with open(real_path, "wb") as f:
|
|
265
|
+
data_path = "/".join(storage_path + [stream_path])
|
|
266
|
+
data = olefile.openstream(data_path).read()
|
|
267
|
+
|
|
268
|
+
f.write(data)
|
|
269
|
+
|
|
270
|
+
# Return True if decompressed successfully
|
|
271
|
+
return True
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def compress(src, dest=None):
|
|
275
|
+
"""
|
|
276
|
+
Create compound file from given directory tree.
|
|
277
|
+
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
cfb = CompoundFile()
|
|
281
|
+
|
|
282
|
+
if not dest:
|
|
283
|
+
dest = f"{src}.cfb"
|
|
284
|
+
|
|
285
|
+
for root, _, streams in os.walk(src):
|
|
286
|
+
storage = os.path.relpath(root, src).lstrip(".")
|
|
287
|
+
|
|
288
|
+
# Add storage as directory entry is storage is not a root
|
|
289
|
+
if storage:
|
|
290
|
+
*path, storage_name = storage.split("/")
|
|
291
|
+
|
|
292
|
+
# Insert storage into directory
|
|
293
|
+
cfb.insert_directory(
|
|
294
|
+
"/".join(path),
|
|
295
|
+
directory.EntryData(
|
|
296
|
+
name=storage_name,
|
|
297
|
+
obj_type=directory.OBJTY_STORAGE,
|
|
298
|
+
),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
for stream in streams:
|
|
302
|
+
# Read data from file
|
|
303
|
+
with open(f"{root}/{stream}", "rb") as f:
|
|
304
|
+
stream_data = f.read()
|
|
305
|
+
stream_size = len(stream_data)
|
|
306
|
+
|
|
307
|
+
# Write data into sector or mini-sector
|
|
308
|
+
if stream_size > MINI_STREAM_CUTOFF_SIZE:
|
|
309
|
+
stream_sector_index = cfb.write_sector(stream_data)
|
|
310
|
+
|
|
311
|
+
else:
|
|
312
|
+
stream_sector_index = cfb.write_mini_sector(stream_data)
|
|
313
|
+
|
|
314
|
+
# Insert stream into directory
|
|
315
|
+
cfb.insert_directory(
|
|
316
|
+
storage,
|
|
317
|
+
directory.EntryData(
|
|
318
|
+
name=stream,
|
|
319
|
+
obj_type=directory.OBJTY_STREAM,
|
|
320
|
+
sector=stream_sector_index,
|
|
321
|
+
size=stream_size,
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Write mini-sector into sector
|
|
326
|
+
mini_sector_data = b"".join(cfb.mini_sectors)
|
|
327
|
+
mini_sector_index = cfb.write_sector(mini_sector_data)
|
|
328
|
+
|
|
329
|
+
cfb.root_directory.root.data.size = len(mini_sector_data)
|
|
330
|
+
cfb.root_directory.root.data.sector = mini_sector_index
|
|
331
|
+
|
|
332
|
+
# Get sector size
|
|
333
|
+
sector_size = 1 << cfb.sector_shift
|
|
334
|
+
|
|
335
|
+
# Write mini-fat into sector
|
|
336
|
+
mini_fat_data = b"".join(
|
|
337
|
+
[struct.pack("<I", entry) for entry in cfb.mini_fat],
|
|
338
|
+
)
|
|
339
|
+
cfb.num_mini_fat_sectors = math.ceil(
|
|
340
|
+
len(mini_fat_data) / sector_size,
|
|
341
|
+
)
|
|
342
|
+
cfb.first_mini_fat_sector = cfb.write_sector(mini_fat_data)
|
|
343
|
+
|
|
344
|
+
# Round up to nearest multiple of 4
|
|
345
|
+
directory_list = [b"\xff\xff\xff\xff"] * ((cfb.num_dir + 3) // 4) * 4
|
|
346
|
+
|
|
347
|
+
# Insert directory entry into directory list
|
|
348
|
+
for entry in cfb.root_directory.traverse():
|
|
349
|
+
directory_list[entry.stream_id()] = bytes(entry)
|
|
350
|
+
|
|
351
|
+
# Write directory entry into sector
|
|
352
|
+
directory_data = b"".join(directory_list)
|
|
353
|
+
cfb.num_dir_sectors = math.ceil(len(directory_data) / sector_size)
|
|
354
|
+
cfb.first_dir_sector = cfb.write_sector(directory_data)
|
|
355
|
+
|
|
356
|
+
# Write fat into sector
|
|
357
|
+
fat_data = b"".join([struct.pack("<I", entry) for entry in cfb.fat])
|
|
358
|
+
cfb.write_fat(fat_data)
|
|
359
|
+
cfb.num_fat_sectors = math.ceil(len(fat_data) / sector_size)
|
|
360
|
+
|
|
361
|
+
# Hotfixed instance variables
|
|
362
|
+
# TODO: You have to remove this line after problem is fully recognized.
|
|
363
|
+
cfb.first_difat_sector = 0xFFFFFFFE
|
|
364
|
+
cfb.num_dir_sectors = 0
|
|
365
|
+
|
|
366
|
+
# Export results as file
|
|
367
|
+
cfb.save(dest)
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Self
|
|
4
|
+
import struct
|
|
5
|
+
|
|
6
|
+
# Directory entry constants
|
|
7
|
+
MAXREGSID = 0xFFFFFFFA #: (-6) maximum directory entry ID
|
|
8
|
+
NOSTREAM = 0xFFFFFFFF #: (-1) unallocated directory entry
|
|
9
|
+
|
|
10
|
+
# Object type constants
|
|
11
|
+
OBJTY_EMPTY = 0x00 #: empty directory entry
|
|
12
|
+
OBJTY_STORAGE = 0x01 #: element is a storage object
|
|
13
|
+
OBJTY_STREAM = 0x02 #: element is a stream object
|
|
14
|
+
OBJTY_LOCKBYTES = 0x03 #: element is an ILockBytes object
|
|
15
|
+
OBJTY_PROPERTY = 0x04 #: element is an IPropertyStorage object
|
|
16
|
+
OBJTY_ROOT = 0x05 #: element is a root storage
|
|
17
|
+
|
|
18
|
+
# Color flags
|
|
19
|
+
RED = 0x00
|
|
20
|
+
BLACK = 0x01
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EntryData:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
name: str,
|
|
27
|
+
stream_id=NOSTREAM,
|
|
28
|
+
obj_type=OBJTY_EMPTY,
|
|
29
|
+
sector=0x00000000,
|
|
30
|
+
size=0,
|
|
31
|
+
parent: Entry = None,
|
|
32
|
+
):
|
|
33
|
+
|
|
34
|
+
# Entry information
|
|
35
|
+
self.name = name[:31].encode("utf-16-le")
|
|
36
|
+
self.stream_id = stream_id
|
|
37
|
+
self.obj_type = obj_type
|
|
38
|
+
self.sector = sector
|
|
39
|
+
self.size = size
|
|
40
|
+
|
|
41
|
+
# Directory hierarchy
|
|
42
|
+
self.parent = parent # Entry of parent directory
|
|
43
|
+
self.child = Directory() # RBTree of child directory
|
|
44
|
+
|
|
45
|
+
def __eq__(self, other: Self) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Regard two `EntryData` instance is the same if the name is identical.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
return self.name == other.name
|
|
52
|
+
|
|
53
|
+
def __gt__(self, other: Self) -> bool:
|
|
54
|
+
"""
|
|
55
|
+
Define comparison operator as described on MS-CFB documentation.
|
|
56
|
+
|
|
57
|
+
Currently, uppercase conversion is achieved by using `upper()` method provided by python. As `upper()` is implemented regarding unicode uppercase conversion rule, it works fine with most of the case.
|
|
58
|
+
|
|
59
|
+
However, as MS-CFB has some exception cases for several unicode characters. But this exceptions are negligible. Who will use "LATIN SMALL LETTER A WITH STROKE" for directory name?
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
self_name = self.name.decode("utf-16-le").upper()
|
|
64
|
+
other_name = other.name.decode("utf-16-le").upper()
|
|
65
|
+
|
|
66
|
+
if len(self_name) != len(other_name):
|
|
67
|
+
return len(self_name) > len(other_name)
|
|
68
|
+
|
|
69
|
+
return self_name > other_name
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Entry:
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
data: EntryData = None,
|
|
76
|
+
color=RED,
|
|
77
|
+
left: Self = None,
|
|
78
|
+
right: Self = None,
|
|
79
|
+
parent: Self = None,
|
|
80
|
+
):
|
|
81
|
+
# Entry data
|
|
82
|
+
self.data = data
|
|
83
|
+
|
|
84
|
+
# RBTree information
|
|
85
|
+
self.color = color
|
|
86
|
+
self.left = left
|
|
87
|
+
self.right = right
|
|
88
|
+
self.parent = parent
|
|
89
|
+
|
|
90
|
+
def __bytes__(self) -> bytes:
|
|
91
|
+
"""
|
|
92
|
+
Represents directory entry objects as 128-byte bytes string.
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
directory_data = struct.pack(
|
|
96
|
+
"<64sHBBIII16sIQQIQ",
|
|
97
|
+
self.data.name,
|
|
98
|
+
len(self.data.name) + 2,
|
|
99
|
+
self.data.obj_type,
|
|
100
|
+
self.color,
|
|
101
|
+
self.left.stream_id(),
|
|
102
|
+
self.right.stream_id(),
|
|
103
|
+
self.data.child.root.stream_id(),
|
|
104
|
+
bytes(16),
|
|
105
|
+
0x00000000,
|
|
106
|
+
0x0000000000000000,
|
|
107
|
+
0x0000000000000000,
|
|
108
|
+
self.data.sector,
|
|
109
|
+
self.data.size,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return directory_data
|
|
113
|
+
|
|
114
|
+
def stream_id(self):
|
|
115
|
+
"""
|
|
116
|
+
Get `stream_id` from entry data
|
|
117
|
+
|
|
118
|
+
This shortcut is provided since `stream_id` is often used and there are some cases where entry data is `None`.
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
if self.data is None:
|
|
123
|
+
return NOSTREAM
|
|
124
|
+
|
|
125
|
+
return self.data.stream_id
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Directory:
|
|
129
|
+
def __init__(self):
|
|
130
|
+
"""
|
|
131
|
+
Initialize Directory
|
|
132
|
+
|
|
133
|
+
Create NIL entry for indicating there's no entry available.
|
|
134
|
+
Set root as NIL.
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
self.NIL = Entry(color=BLACK)
|
|
139
|
+
self.root = self.NIL
|
|
140
|
+
|
|
141
|
+
def insert(self, data: EntryData):
|
|
142
|
+
new_entry = Entry(data=data, left=self.NIL, right=self.NIL)
|
|
143
|
+
parent = None
|
|
144
|
+
current = self.root
|
|
145
|
+
|
|
146
|
+
while current != self.NIL:
|
|
147
|
+
parent = current
|
|
148
|
+
|
|
149
|
+
if new_entry.data < current.data:
|
|
150
|
+
current = current.left
|
|
151
|
+
|
|
152
|
+
else:
|
|
153
|
+
current = current.right
|
|
154
|
+
|
|
155
|
+
new_entry.parent = parent
|
|
156
|
+
|
|
157
|
+
# Tree is empty
|
|
158
|
+
if parent is None:
|
|
159
|
+
self.root = new_entry
|
|
160
|
+
|
|
161
|
+
elif new_entry.data < parent.data:
|
|
162
|
+
parent.left = new_entry
|
|
163
|
+
|
|
164
|
+
else:
|
|
165
|
+
parent.right = new_entry
|
|
166
|
+
|
|
167
|
+
new_entry.left = self.NIL
|
|
168
|
+
new_entry.right = self.NIL
|
|
169
|
+
new_entry.color = RED
|
|
170
|
+
|
|
171
|
+
self.fix_insert(new_entry)
|
|
172
|
+
|
|
173
|
+
def fix_insert(self, entry):
|
|
174
|
+
while entry.parent and entry.parent.color == RED:
|
|
175
|
+
if entry.parent == entry.parent.parent.left:
|
|
176
|
+
uncle = entry.parent.parent.right
|
|
177
|
+
|
|
178
|
+
# Case 1: Uncle is red
|
|
179
|
+
if uncle.color == RED:
|
|
180
|
+
entry.parent.color = BLACK
|
|
181
|
+
uncle.color = BLACK
|
|
182
|
+
entry.parent.parent.color = RED
|
|
183
|
+
entry = entry.parent.parent
|
|
184
|
+
|
|
185
|
+
else:
|
|
186
|
+
# Case 2: entry is right child
|
|
187
|
+
if entry == entry.parent.right:
|
|
188
|
+
entry = entry.parent
|
|
189
|
+
self.left_rotate(entry)
|
|
190
|
+
|
|
191
|
+
# Case 3: entry is left child
|
|
192
|
+
entry.parent.color = BLACK
|
|
193
|
+
entry.parent.parent.color = RED
|
|
194
|
+
self.right_rotate(entry.parent.parent)
|
|
195
|
+
|
|
196
|
+
else:
|
|
197
|
+
uncle = entry.parent.parent.left
|
|
198
|
+
|
|
199
|
+
# Case 1: Uncle is red
|
|
200
|
+
if uncle.color == RED:
|
|
201
|
+
entry.parent.color = BLACK
|
|
202
|
+
uncle.color = BLACK
|
|
203
|
+
entry.parent.parent.color = RED
|
|
204
|
+
entry = entry.parent.parent
|
|
205
|
+
|
|
206
|
+
else:
|
|
207
|
+
# Case 2: entry is left child
|
|
208
|
+
if entry == entry.parent.left:
|
|
209
|
+
entry = entry.parent
|
|
210
|
+
self.right_rotate(entry)
|
|
211
|
+
|
|
212
|
+
# Case 3: entry is right child
|
|
213
|
+
entry.parent.color = BLACK
|
|
214
|
+
entry.parent.parent.color = RED
|
|
215
|
+
self.left_rotate(entry.parent.parent)
|
|
216
|
+
|
|
217
|
+
self.root.color = BLACK
|
|
218
|
+
|
|
219
|
+
def left_rotate(self, entry):
|
|
220
|
+
right_child = entry.right
|
|
221
|
+
entry.right = right_child.left
|
|
222
|
+
|
|
223
|
+
if right_child.left != self.NIL:
|
|
224
|
+
right_child.left.parent = entry
|
|
225
|
+
|
|
226
|
+
right_child.parent = entry.parent
|
|
227
|
+
|
|
228
|
+
# entry is root
|
|
229
|
+
if entry.parent is None:
|
|
230
|
+
self.root = right_child
|
|
231
|
+
|
|
232
|
+
elif entry == entry.parent.left:
|
|
233
|
+
entry.parent.left = right_child
|
|
234
|
+
|
|
235
|
+
else:
|
|
236
|
+
entry.parent.right = right_child
|
|
237
|
+
|
|
238
|
+
right_child.left = entry
|
|
239
|
+
entry.parent = right_child
|
|
240
|
+
|
|
241
|
+
def right_rotate(self, entry):
|
|
242
|
+
left_child = entry.left
|
|
243
|
+
entry.left = left_child.right
|
|
244
|
+
|
|
245
|
+
if left_child.right != self.NIL:
|
|
246
|
+
left_child.right.parent = entry
|
|
247
|
+
|
|
248
|
+
left_child.parent = entry.parent
|
|
249
|
+
|
|
250
|
+
# entry is root
|
|
251
|
+
if entry.parent is None:
|
|
252
|
+
self.root = left_child
|
|
253
|
+
|
|
254
|
+
elif entry == entry.parent.right:
|
|
255
|
+
entry.parent.right = left_child
|
|
256
|
+
|
|
257
|
+
else:
|
|
258
|
+
entry.parent.left = left_child
|
|
259
|
+
|
|
260
|
+
left_child.right = entry
|
|
261
|
+
entry.parent = left_child
|
|
262
|
+
|
|
263
|
+
def search_name(self, name):
|
|
264
|
+
"""
|
|
265
|
+
Search entry by name of entry data.
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
# Create dummy entry data for search
|
|
270
|
+
name_entry_data = EntryData(name=name, stream_id=NOSTREAM)
|
|
271
|
+
|
|
272
|
+
return self._search_tree_helper(self.root, name_entry_data)
|
|
273
|
+
|
|
274
|
+
def _search_tree_helper(self, entry, data):
|
|
275
|
+
if entry == self.NIL or data == entry.data:
|
|
276
|
+
return entry
|
|
277
|
+
|
|
278
|
+
if data < entry.data:
|
|
279
|
+
return self._search_tree_helper(entry.left, data)
|
|
280
|
+
|
|
281
|
+
return self._search_tree_helper(entry.right, data)
|
|
282
|
+
|
|
283
|
+
def traverse(self, entry=None):
|
|
284
|
+
"""
|
|
285
|
+
Traverse directory tree
|
|
286
|
+
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
if entry == None:
|
|
290
|
+
entry = self.root
|
|
291
|
+
|
|
292
|
+
if entry != self.NIL:
|
|
293
|
+
yield from self.traverse(entry.left)
|
|
294
|
+
|
|
295
|
+
yield entry
|
|
296
|
+
yield from entry.data.child.traverse()
|
|
297
|
+
|
|
298
|
+
yield from self.traverse(entry.right)
|