cfbpy 0.0.1__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.
cfbpy/__init__.py ADDED
File without changes
cfbpy/cfb.py ADDED
@@ -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)
cfbpy/directory.py ADDED
@@ -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)
@@ -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)
@@ -0,0 +1,7 @@
1
+ cfbpy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ cfbpy/cfb.py,sha256=sPLLVT_KKz8_o_opPlM9VSj1e0j6G1A7TyHr6dVjSAw,11207
3
+ cfbpy/directory.py,sha256=DNJcON-8OgE7N8td3KhOD0PNY48xCLpn67IPFOtil6c,8188
4
+ cfbpy-0.0.1.dist-info/METADATA,sha256=mY1KFR3jtR5hySOeUWn91urbN_LUkhyjGIsWkZdpMys,636
5
+ cfbpy-0.0.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
6
+ cfbpy-0.0.1.dist-info/licenses/LICENSE,sha256=_aHvI8YeCiMD3rpnw3eBRgQqmlWfvWt63e_LtLq7c38,1068
7
+ cfbpy-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.25.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.