pysap 0.2.0__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.
- pysap/SAPCAR.py +846 -0
- pysap/SAPCredv2.py +411 -0
- pysap/SAPDiag.py +661 -0
- pysap/SAPDiagClient.py +248 -0
- pysap/SAPDiagItems.py +663 -0
- pysap/SAPEnqueue.py +262 -0
- pysap/SAPHDB.py +1508 -0
- pysap/SAPIGS.py +218 -0
- pysap/SAPLPS.py +173 -0
- pysap/SAPMS.py +1203 -0
- pysap/SAPNI.py +491 -0
- pysap/SAPNWRFC.py +446 -0
- pysap/SAPPSE.py +350 -0
- pysap/SAPRFC.py +921 -0
- pysap/SAPRouter.py +991 -0
- pysap/SAPSNC.py +144 -0
- pysap/SAPSSFS.py +266 -0
- pysap/__init__.py +37 -0
- pysap/utils/__init__.py +78 -0
- pysap/utils/console.py +148 -0
- pysap/utils/crypto/__init__.py +413 -0
- pysap/utils/crypto/rfc.py +166 -0
- pysap/utils/crypto/rsec.py +409 -0
- pysap/utils/fields.py +449 -0
- pysap-0.2.0.data/scripts/pysapcar +326 -0
- pysap-0.2.0.data/scripts/pysapgenpse +300 -0
- pysap-0.2.0.data/scripts/pysaphdbuserstore +214 -0
- pysap-0.2.0.dist-info/METADATA +236 -0
- pysap-0.2.0.dist-info/RECORD +33 -0
- pysap-0.2.0.dist-info/WHEEL +5 -0
- pysap-0.2.0.dist-info/licenses/COPYING +340 -0
- pysap-0.2.0.dist-info/top_level.txt +2 -0
- pysapcompress.py +1654 -0
pysap/SAPCAR.py
ADDED
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# pysap - Python library for crafting SAP's network protocols packets
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU General Public License
|
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
|
7
|
+
# of the License, or (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# Author:
|
|
15
|
+
# Martin Gallo (@martingalloar)
|
|
16
|
+
# Code contributed by SecureAuth to the OWASP CBAS project
|
|
17
|
+
#
|
|
18
|
+
|
|
19
|
+
# Standard imports
|
|
20
|
+
import stat
|
|
21
|
+
from zlib import crc32
|
|
22
|
+
from struct import pack
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from os import stat as os_stat
|
|
25
|
+
from io import BytesIO
|
|
26
|
+
# External imports
|
|
27
|
+
from scapy.packet import Packet
|
|
28
|
+
from scapy.fields import (ByteField, ByteEnumField, LEIntField, FieldLenField,
|
|
29
|
+
PacketField, StrFixedLenField, PacketListField,
|
|
30
|
+
ConditionalField, LESignedIntField, StrField, LELongField,
|
|
31
|
+
MultipleTypeField)
|
|
32
|
+
# Custom imports
|
|
33
|
+
from pysap.utils.fields import (PacketNoPadded, StrNullFixedLenField, PacketListStopField)
|
|
34
|
+
from pysapcompress import (decompress, compress, ALG_LZH, CompressError,
|
|
35
|
+
DecompressError)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Filemode code obtained from Python 3 stat.py
|
|
39
|
+
_filemode_table = (
|
|
40
|
+
((stat.S_IFLNK, "l"),
|
|
41
|
+
(stat.S_IFREG, "-"),
|
|
42
|
+
(stat.S_IFBLK, "b"),
|
|
43
|
+
(stat.S_IFDIR, "d"),
|
|
44
|
+
(stat.S_IFCHR, "c"),
|
|
45
|
+
(stat.S_IFIFO, "p")),
|
|
46
|
+
|
|
47
|
+
((stat.S_IRUSR, "r"),),
|
|
48
|
+
((stat.S_IWUSR, "w"),),
|
|
49
|
+
((stat.S_IXUSR | stat.S_ISUID, "s"),
|
|
50
|
+
(stat.S_ISUID, "S"),
|
|
51
|
+
(stat.S_IXUSR, "x")),
|
|
52
|
+
|
|
53
|
+
((stat.S_IRGRP, "r"),),
|
|
54
|
+
((stat.S_IWGRP, "w"),),
|
|
55
|
+
((stat.S_IXGRP | stat.S_ISGID, "s"),
|
|
56
|
+
(stat.S_ISGID, "S"),
|
|
57
|
+
(stat.S_IXGRP, "x")),
|
|
58
|
+
|
|
59
|
+
((stat.S_IROTH, "r"),),
|
|
60
|
+
((stat.S_IWOTH, "w"),),
|
|
61
|
+
((stat.S_IXOTH | stat.S_ISVTX, "t"),
|
|
62
|
+
(stat.S_ISVTX, "T"),
|
|
63
|
+
(stat.S_IXOTH, "x"))
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
SIZE_FOUR_GB = 0xffffffff + 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def filemode(mode):
|
|
71
|
+
"""Convert a file's mode to a string of the form '-rwxrwxrwx'."""
|
|
72
|
+
perm = []
|
|
73
|
+
for table in _filemode_table:
|
|
74
|
+
for bit, char in table:
|
|
75
|
+
if mode & bit == bit:
|
|
76
|
+
perm.append(char)
|
|
77
|
+
break
|
|
78
|
+
else:
|
|
79
|
+
perm.append("-")
|
|
80
|
+
return "".join(perm)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SAPCARInvalidFileException(Exception):
|
|
84
|
+
"""Exception to denote an invalid SAP CAR file"""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class SAPCARInvalidChecksumException(Exception):
|
|
88
|
+
"""Exception to denote a syntactically valid SAP CAR file with an invalid checksum"""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class SAPCARCompressedBlobFormat(PacketNoPadded):
|
|
92
|
+
"""SAP CAR compressed blob
|
|
93
|
+
|
|
94
|
+
This is used for decompressing blobs inside the compressed block.
|
|
95
|
+
"""
|
|
96
|
+
name = "SAP CAR Archive Compressed blob"
|
|
97
|
+
|
|
98
|
+
fields_desc = [
|
|
99
|
+
LEIntField("compressed_length", 8),
|
|
100
|
+
LEIntField("uncompress_length", 0),
|
|
101
|
+
ByteEnumField("algorithm", 0x12, {0x12: "LZH", 0x10: "LZC"}),
|
|
102
|
+
StrFixedLenField("magic_bytes", b"\x1f\x9d", 2),
|
|
103
|
+
ByteField("special", 2),
|
|
104
|
+
MultipleTypeField(
|
|
105
|
+
[(StrField("blob", b"", remain=4), lambda x: x.compressed_length <= 8),
|
|
106
|
+
(StrFixedLenField("blob", b"", length_from=lambda x: x.compressed_length - 8),
|
|
107
|
+
lambda x: x.compressed_length > 8)],
|
|
108
|
+
StrField("blob", b"", remain=4)
|
|
109
|
+
),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
SAPCAR_BLOCK_TYPE_COMPRESSED_LAST = b"ED"
|
|
114
|
+
"""SAP CAR compressed end of data block"""
|
|
115
|
+
|
|
116
|
+
SAPCAR_BLOCK_TYPE_COMPRESSED = b"DA"
|
|
117
|
+
"""SAP CAR compressed block"""
|
|
118
|
+
|
|
119
|
+
SAPCAR_BLOCK_TYPE_UNCOMPRESSED_LAST = b"UE"
|
|
120
|
+
"""SAP CAR uncompressed end of data block"""
|
|
121
|
+
|
|
122
|
+
SAPCAR_BLOCK_TYPE_UNCOMPRESSED = b"UD"
|
|
123
|
+
"""SAP CAR uncompressed block"""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class SAPCARCompressedBlockFormat(PacketNoPadded):
|
|
127
|
+
"""SAP CAR compressed block
|
|
128
|
+
|
|
129
|
+
This is used for decompressing blocks inside the file info format.
|
|
130
|
+
"""
|
|
131
|
+
name = "SAP CAR Archive Compressed block"
|
|
132
|
+
|
|
133
|
+
fields_desc = [
|
|
134
|
+
StrFixedLenField("type", SAPCAR_BLOCK_TYPE_COMPRESSED_LAST, 2),
|
|
135
|
+
ConditionalField(PacketField("compressed", SAPCARCompressedBlobFormat(), SAPCARCompressedBlobFormat),
|
|
136
|
+
lambda x: x.type in [SAPCAR_BLOCK_TYPE_COMPRESSED_LAST, SAPCAR_BLOCK_TYPE_COMPRESSED]),
|
|
137
|
+
ConditionalField(LESignedIntField("checksum", 0),
|
|
138
|
+
lambda x: x.type == SAPCAR_BLOCK_TYPE_COMPRESSED_LAST),
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def sapcar_is_last_block(packet):
|
|
143
|
+
"""Helper function that evaluates if a block packet is the end of data one or not.
|
|
144
|
+
|
|
145
|
+
:param packet: packet to check
|
|
146
|
+
:type packet: Packet
|
|
147
|
+
|
|
148
|
+
:return: if the block packet is the end of data one
|
|
149
|
+
:rtype: bool
|
|
150
|
+
"""
|
|
151
|
+
return packet.type in [SAPCAR_BLOCK_TYPE_COMPRESSED_LAST, SAPCAR_BLOCK_TYPE_UNCOMPRESSED_LAST]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
SAPCAR_TYPE_FILE = b"RG"
|
|
155
|
+
"""SAP CAR regular file string"""
|
|
156
|
+
|
|
157
|
+
SAPCAR_TYPE_DIR = b"DR"
|
|
158
|
+
"""SAP CAR directory string"""
|
|
159
|
+
|
|
160
|
+
SAPCAR_TYPE_SHORTCUT = b"SC"
|
|
161
|
+
"""SAP CAR Windows short cut string"""
|
|
162
|
+
|
|
163
|
+
SAPCAR_TYPE_LINK = b"LK"
|
|
164
|
+
"""SAP CAR Unix soft link string"""
|
|
165
|
+
|
|
166
|
+
SAPCAR_TYPE_AS400 = b"SV"
|
|
167
|
+
"""SAP CAR AS400 save file string"""
|
|
168
|
+
|
|
169
|
+
SAPCAR_TYPE_SIGNATURE = b"SM"
|
|
170
|
+
"""SAP CAR SIGNATURE.SMF file string"""
|
|
171
|
+
# XXX: Unsure if this file has any particular treatment in latest versions of SAPCAR
|
|
172
|
+
|
|
173
|
+
SAPCAR_VERSION_200 = b"2.00"
|
|
174
|
+
"""SAP CAR file format version 2.00 string"""
|
|
175
|
+
|
|
176
|
+
SAPCAR_VERSION_201 = b"2.01"
|
|
177
|
+
"""SAP CAR file format version 2.01 string"""
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SAPCARArchiveFilev200Format(PacketNoPadded):
|
|
181
|
+
"""SAP CAR file information format
|
|
182
|
+
|
|
183
|
+
This is ued to parse files inside a SAP CAR archive.
|
|
184
|
+
"""
|
|
185
|
+
name = "SAP CAR Archive File 2.00"
|
|
186
|
+
|
|
187
|
+
version = SAPCAR_VERSION_200
|
|
188
|
+
is_filename_null_terminated = False
|
|
189
|
+
|
|
190
|
+
fields_desc = [
|
|
191
|
+
StrFixedLenField("type", SAPCAR_TYPE_FILE, 2),
|
|
192
|
+
LEIntField("perm_mode", 0),
|
|
193
|
+
LELongField("file_length_low", 0),
|
|
194
|
+
LEIntField("file_length_high", 0),
|
|
195
|
+
LELongField("timestamp", 0),
|
|
196
|
+
LEIntField("code_page", 0),
|
|
197
|
+
FieldLenField("user_info_length", 0, length_of="user_info", fmt="<H"),
|
|
198
|
+
FieldLenField("filename_length", 0, length_of="filename", fmt="<H"),
|
|
199
|
+
StrNullFixedLenField("filename", None, length_from=lambda x: x.filename_length,
|
|
200
|
+
null_terminated=lambda x: x.is_filename_null_terminated),
|
|
201
|
+
StrFixedLenField("user_info", None, length_from=lambda x: x.user_info_length),
|
|
202
|
+
ConditionalField(PacketListStopField("blocks", None, SAPCARCompressedBlockFormat, stop=sapcar_is_last_block),
|
|
203
|
+
lambda x: x.type == SAPCAR_TYPE_FILE and x.file_length > 0),
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def file_length(self):
|
|
208
|
+
"""Getter for the file length fields. It converts the two length fields (low and high) as provided in the
|
|
209
|
+
archive file into a long long integer.
|
|
210
|
+
"""
|
|
211
|
+
return (self.file_length_high * SIZE_FOUR_GB) + self.file_length_low
|
|
212
|
+
|
|
213
|
+
@file_length.setter
|
|
214
|
+
def file_length(self, file_length):
|
|
215
|
+
"""Setter for the file length fields. It splits the long long integer int on the two length fields (low and
|
|
216
|
+
high) as required by the archive file.
|
|
217
|
+
"""
|
|
218
|
+
self.file_length_low = file_length & 0xffffffff
|
|
219
|
+
self.file_length_high = file_length >> 32
|
|
220
|
+
|
|
221
|
+
def extract(self, fd):
|
|
222
|
+
"""Extracts the archive file and writes the extracted file to the provided file object. Returns the checksum
|
|
223
|
+
obtained from the archive. If blocks are uncompressed, the file is directly extracted. If the blocks are
|
|
224
|
+
compressed, each block is added to a buffer, skipping the length field, and decompression is performed after
|
|
225
|
+
the block marked as end of data. Expected length and compression header is obtained from the first block and
|
|
226
|
+
checksum from the end of data block.
|
|
227
|
+
|
|
228
|
+
:param fd: file-like object to write the extracted file to
|
|
229
|
+
:type fd: file
|
|
230
|
+
|
|
231
|
+
:return: checksum
|
|
232
|
+
:rtype: int
|
|
233
|
+
|
|
234
|
+
:raise DecompressError: If there's a decompression error
|
|
235
|
+
:raise SAPCARInvalidFileException: If the file is invalid
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
if self.file_length == 0:
|
|
239
|
+
return 0
|
|
240
|
+
|
|
241
|
+
compressed = b""
|
|
242
|
+
checksum = 0
|
|
243
|
+
exp_length = None
|
|
244
|
+
|
|
245
|
+
remaining_length = self.file_length
|
|
246
|
+
for block in self.blocks:
|
|
247
|
+
# Process uncompressed block types
|
|
248
|
+
if block.type in [SAPCAR_BLOCK_TYPE_UNCOMPRESSED, SAPCAR_BLOCK_TYPE_UNCOMPRESSED_LAST]:
|
|
249
|
+
fd.write(block.compressed)
|
|
250
|
+
remaining_length -= len(block.compressed)
|
|
251
|
+
# Store compressed block types for later decompression
|
|
252
|
+
elif block.type in [SAPCAR_BLOCK_TYPE_COMPRESSED, SAPCAR_BLOCK_TYPE_COMPRESSED_LAST]:
|
|
253
|
+
# Add compressed block to a buffer, skipping the first 4 bytes of each block (uncompressed length)
|
|
254
|
+
compressed += bytes(block.compressed)[4:]
|
|
255
|
+
# If the expected length wasn't already set, do it
|
|
256
|
+
if not exp_length:
|
|
257
|
+
exp_length = block.compressed.uncompress_length
|
|
258
|
+
else:
|
|
259
|
+
raise SAPCARInvalidFileException("Invalid block type found")
|
|
260
|
+
|
|
261
|
+
# Check end of data block, performing decompression if needed
|
|
262
|
+
if sapcar_is_last_block(block):
|
|
263
|
+
checksum = block.checksum
|
|
264
|
+
# If there was at least one compressed block that set the expected length, decompress it
|
|
265
|
+
if exp_length:
|
|
266
|
+
(_, block_length, block_buffer) = decompress(bytes(compressed), exp_length)
|
|
267
|
+
if block_length != exp_length or not block_buffer:
|
|
268
|
+
raise DecompressError("Error decompressing block")
|
|
269
|
+
fd.write(block_buffer)
|
|
270
|
+
break
|
|
271
|
+
|
|
272
|
+
return checksum
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class SAPCARArchiveFilev201Format(SAPCARArchiveFilev200Format):
|
|
276
|
+
"""SAP CAR file information format
|
|
277
|
+
|
|
278
|
+
This is used to parse files inside a SAP CAR archive.
|
|
279
|
+
"""
|
|
280
|
+
name = "SAP CAR Archive File 2.01"
|
|
281
|
+
|
|
282
|
+
version = SAPCAR_VERSION_201
|
|
283
|
+
is_filename_null_terminated = True
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
SAPCAR_HEADER_MAGIC_STRING_STANDARD = b"CAR\x20"
|
|
287
|
+
"""SAP CAR archive header magic string standard"""
|
|
288
|
+
|
|
289
|
+
SAPCAR_HEADER_MAGIC_STRING_BACKUP = b"CAR\x00"
|
|
290
|
+
"""SAP CAR archive header magic string backup file"""
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
sapcar_archive_file_versions = {
|
|
294
|
+
SAPCAR_VERSION_200: SAPCARArchiveFilev200Format,
|
|
295
|
+
SAPCAR_VERSION_201: SAPCARArchiveFilev201Format,
|
|
296
|
+
}
|
|
297
|
+
"""SAP CAR file format versions"""
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class SAPCARArchiveFormat(Packet):
|
|
301
|
+
"""SAP CAR file format
|
|
302
|
+
|
|
303
|
+
This is used to parse SAP CAR archive files.
|
|
304
|
+
"""
|
|
305
|
+
name = "SAP CAR Archive"
|
|
306
|
+
|
|
307
|
+
fields_desc = [
|
|
308
|
+
StrFixedLenField("magic_string", SAPCAR_HEADER_MAGIC_STRING_STANDARD, 4),
|
|
309
|
+
StrFixedLenField("version", SAPCAR_VERSION_201, 4),
|
|
310
|
+
ConditionalField(PacketListField("files0", None, SAPCARArchiveFilev200Format),
|
|
311
|
+
lambda x: x.version == SAPCAR_VERSION_200),
|
|
312
|
+
ConditionalField(PacketListField("files1", None, SAPCARArchiveFilev201Format),
|
|
313
|
+
lambda x: x.version == SAPCAR_VERSION_201),
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class SAPCARArchiveFile(object):
|
|
318
|
+
"""Proxy class that can be used to access a file inside a SAP CAR
|
|
319
|
+
archive and obtain its properties.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
# Instance attributes
|
|
323
|
+
_file_format = None
|
|
324
|
+
|
|
325
|
+
def __init__(self, file_format=None):
|
|
326
|
+
"""Construct the file proxy object from a L{SAPCARArchiveFilev200Format}
|
|
327
|
+
or L{SAPCARArchiveFilev201Format} object.
|
|
328
|
+
|
|
329
|
+
:param file_format: file format object
|
|
330
|
+
:type file_format: Packet
|
|
331
|
+
"""
|
|
332
|
+
self._file_format = file_format
|
|
333
|
+
|
|
334
|
+
def is_file(self):
|
|
335
|
+
"""Determines if the file is a regular file.
|
|
336
|
+
|
|
337
|
+
:return: if the file is a regular file
|
|
338
|
+
:rtype: bool
|
|
339
|
+
"""
|
|
340
|
+
return self._file_format.type == SAPCAR_TYPE_FILE
|
|
341
|
+
|
|
342
|
+
def is_directory(self):
|
|
343
|
+
"""Determines if the file is a directory.
|
|
344
|
+
|
|
345
|
+
:return: if the file is a directory
|
|
346
|
+
:rtype: bool
|
|
347
|
+
"""
|
|
348
|
+
return self._file_format.type == SAPCAR_TYPE_DIR
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def version(self):
|
|
352
|
+
"""The version of the file.
|
|
353
|
+
|
|
354
|
+
:return: version of the file
|
|
355
|
+
:rtype: string
|
|
356
|
+
"""
|
|
357
|
+
return self._file_format.version
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def type(self):
|
|
361
|
+
"""The type of the file.
|
|
362
|
+
|
|
363
|
+
:return: type of the file
|
|
364
|
+
:rtype: string
|
|
365
|
+
"""
|
|
366
|
+
return self._file_format.type
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def filename(self):
|
|
370
|
+
"""The name of the file.
|
|
371
|
+
|
|
372
|
+
:return: name of the file
|
|
373
|
+
:rtype: string
|
|
374
|
+
"""
|
|
375
|
+
return self._file_format.filename
|
|
376
|
+
|
|
377
|
+
@filename.setter
|
|
378
|
+
def filename(self, filename):
|
|
379
|
+
"""Sets the name of the file.
|
|
380
|
+
|
|
381
|
+
:param filename: the name of the file
|
|
382
|
+
:type filename: string
|
|
383
|
+
"""
|
|
384
|
+
self._file_format.filename = filename
|
|
385
|
+
self._file_format.filename_length = len(filename)
|
|
386
|
+
if self._file_format.version == SAPCAR_VERSION_201:
|
|
387
|
+
self._file_format.filename_length += 1
|
|
388
|
+
|
|
389
|
+
@property
|
|
390
|
+
def size(self):
|
|
391
|
+
"""The size of the file.
|
|
392
|
+
|
|
393
|
+
:return: size of the file
|
|
394
|
+
:rtype: int
|
|
395
|
+
"""
|
|
396
|
+
return self._file_format.file_length
|
|
397
|
+
|
|
398
|
+
@size.setter
|
|
399
|
+
def size(self, file_length):
|
|
400
|
+
"""Sets the size of the file.
|
|
401
|
+
|
|
402
|
+
:param file_length: the size of the file
|
|
403
|
+
:type file_length: int
|
|
404
|
+
"""
|
|
405
|
+
self._file_format.file_length = file_length
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
def permissions(self):
|
|
409
|
+
"""The permissions of the file.
|
|
410
|
+
|
|
411
|
+
:return: permissions in human-readable format
|
|
412
|
+
:rtype: string
|
|
413
|
+
"""
|
|
414
|
+
return filemode(self._file_format.perm_mode)
|
|
415
|
+
|
|
416
|
+
@permissions.setter
|
|
417
|
+
def permissions(self, perm_mode):
|
|
418
|
+
"""Sets the permissions on the file.
|
|
419
|
+
|
|
420
|
+
:param perm_mode: the permissions to set
|
|
421
|
+
:type perm_mode: int
|
|
422
|
+
"""
|
|
423
|
+
self._file_format.perm_mode = perm_mode
|
|
424
|
+
|
|
425
|
+
@property
|
|
426
|
+
def perm_mode(self):
|
|
427
|
+
"""The permissions mode of the file.
|
|
428
|
+
|
|
429
|
+
:return: permissions in numeric format
|
|
430
|
+
:rtype: int
|
|
431
|
+
"""
|
|
432
|
+
return self._file_format.perm_mode
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def timestamp(self):
|
|
436
|
+
"""The timestamp of the file.
|
|
437
|
+
|
|
438
|
+
:return: timestamp in human-readable format
|
|
439
|
+
:rtype: string
|
|
440
|
+
"""
|
|
441
|
+
return datetime.utcfromtimestamp(self._file_format.timestamp).strftime('%d %b %Y %H:%M')
|
|
442
|
+
|
|
443
|
+
@timestamp.setter
|
|
444
|
+
def timestamp(self, timestamp):
|
|
445
|
+
"""Sets the file timestamp.
|
|
446
|
+
|
|
447
|
+
:param timestamp: the timestamp to set
|
|
448
|
+
:type timestamp: int
|
|
449
|
+
"""
|
|
450
|
+
self._file_format.timestamp = timestamp
|
|
451
|
+
|
|
452
|
+
@property
|
|
453
|
+
def timestamp_raw(self):
|
|
454
|
+
"""The timestamp of the file.
|
|
455
|
+
|
|
456
|
+
:return: timestamp in numeric format
|
|
457
|
+
:rtype: int
|
|
458
|
+
"""
|
|
459
|
+
return self._file_format.timestamp
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def checksum(self):
|
|
463
|
+
"""The checksum of the file.
|
|
464
|
+
|
|
465
|
+
:return: checksum
|
|
466
|
+
:rtype: int
|
|
467
|
+
|
|
468
|
+
:raise SAPCARInvalidFileException: if the file is invalid and contains more than one end of data block
|
|
469
|
+
"""
|
|
470
|
+
checksum = None
|
|
471
|
+
if self._file_format.blocks:
|
|
472
|
+
for block in self._file_format.blocks:
|
|
473
|
+
if block.type == SAPCAR_BLOCK_TYPE_COMPRESSED_LAST:
|
|
474
|
+
if checksum is not None:
|
|
475
|
+
raise SAPCARInvalidFileException("More than one end of data block found for the file")
|
|
476
|
+
checksum = block.checksum
|
|
477
|
+
return checksum
|
|
478
|
+
|
|
479
|
+
@checksum.setter
|
|
480
|
+
def checksum(self, checksum):
|
|
481
|
+
"""Sets the file checksum.
|
|
482
|
+
|
|
483
|
+
:param checksum: checksum to set
|
|
484
|
+
:rtype checksum: int
|
|
485
|
+
|
|
486
|
+
:raise SAPCARInvalidFileException: if the file is invalid and contains more than one end of data block
|
|
487
|
+
"""
|
|
488
|
+
checksum_set = False
|
|
489
|
+
for block in self._file_format.blocks:
|
|
490
|
+
if block.type == SAPCAR_BLOCK_TYPE_COMPRESSED_LAST:
|
|
491
|
+
if checksum_set:
|
|
492
|
+
raise SAPCARInvalidFileException("More than one end of data block found for the file")
|
|
493
|
+
block.checksum = checksum
|
|
494
|
+
checksum_set = True
|
|
495
|
+
if not checksum_set:
|
|
496
|
+
raise SAPCARInvalidFileException("No end of data block found for the file")
|
|
497
|
+
|
|
498
|
+
@staticmethod
|
|
499
|
+
def calculate_checksum(data):
|
|
500
|
+
"""Calculates the CRC32 checksum of a given data string.
|
|
501
|
+
|
|
502
|
+
:param data: data to calculate the checksum over
|
|
503
|
+
:type data: str
|
|
504
|
+
|
|
505
|
+
:return: the CRC32 checksum
|
|
506
|
+
:rtype: int
|
|
507
|
+
"""
|
|
508
|
+
crc = crc32(data, -1)
|
|
509
|
+
# zlib.crc32 always returns an unsigned 32-bit value in Python 3, but the checksum
|
|
510
|
+
# field is a signed 32-bit integer, so convert to signed before negating
|
|
511
|
+
if crc >= 0x80000000:
|
|
512
|
+
crc -= 0x100000000
|
|
513
|
+
return -crc - 1
|
|
514
|
+
|
|
515
|
+
@classmethod
|
|
516
|
+
def from_file(cls, filename, version=SAPCAR_VERSION_201, archive_filename=None):
|
|
517
|
+
"""Populates the file format object from an actual file on the
|
|
518
|
+
local file system.
|
|
519
|
+
|
|
520
|
+
:param filename: filename to build the file format object from
|
|
521
|
+
:type filename: string
|
|
522
|
+
|
|
523
|
+
:param version: version of the file to construct
|
|
524
|
+
:type version: string
|
|
525
|
+
|
|
526
|
+
:param archive_filename: filename to use inside the archive file
|
|
527
|
+
:type archive_filename: string
|
|
528
|
+
|
|
529
|
+
:raise ValueError: if the version requested is invalid
|
|
530
|
+
"""
|
|
531
|
+
|
|
532
|
+
# Read the file properties and its content
|
|
533
|
+
filename_str = filename.decode() if isinstance(filename, bytes) else filename
|
|
534
|
+
stat = os_stat(filename_str)
|
|
535
|
+
with open(filename_str, "rb") as fd:
|
|
536
|
+
data = fd.read()
|
|
537
|
+
|
|
538
|
+
# Compress the file content and build the compressed string
|
|
539
|
+
try:
|
|
540
|
+
(_, out_length, out_buffer) = compress(data, ALG_LZH)
|
|
541
|
+
except CompressError:
|
|
542
|
+
return None
|
|
543
|
+
out_buffer = pack("<I", out_length) + out_buffer
|
|
544
|
+
|
|
545
|
+
# Check the version and grab the file format class
|
|
546
|
+
if version not in sapcar_archive_file_versions:
|
|
547
|
+
raise ValueError("Invalid version")
|
|
548
|
+
ff = sapcar_archive_file_versions[version]
|
|
549
|
+
|
|
550
|
+
# If an archive filename was not provided, use the actual filename
|
|
551
|
+
if archive_filename is None:
|
|
552
|
+
archive_filename = filename
|
|
553
|
+
|
|
554
|
+
# Build the object and fill the fields
|
|
555
|
+
archive_file = cls()
|
|
556
|
+
archive_file._file_format = ff()
|
|
557
|
+
archive_file._file_format.perm_mode = stat.st_mode
|
|
558
|
+
archive_file._file_format.timestamp = int(stat.st_atime)
|
|
559
|
+
archive_file._file_format.file_length = stat.st_size
|
|
560
|
+
archive_file._file_format.filename = archive_filename
|
|
561
|
+
archive_file._file_format.filename_length = len(archive_filename)
|
|
562
|
+
if archive_file._file_format.version == SAPCAR_VERSION_201:
|
|
563
|
+
archive_file._file_format.filename_length += 1
|
|
564
|
+
# Put the compressed blob inside a end of data block and add it to the object
|
|
565
|
+
block = SAPCARCompressedBlockFormat()
|
|
566
|
+
block.type = SAPCAR_BLOCK_TYPE_COMPRESSED_LAST
|
|
567
|
+
block.compressed = SAPCARCompressedBlobFormat(out_buffer)
|
|
568
|
+
block.checksum = cls.calculate_checksum(data)
|
|
569
|
+
archive_file._file_format.blocks.append(block)
|
|
570
|
+
|
|
571
|
+
return archive_file
|
|
572
|
+
|
|
573
|
+
@classmethod
|
|
574
|
+
def from_archive_file(cls, archive_file, version=SAPCAR_VERSION_201):
|
|
575
|
+
"""Populates the file format object from another archive file object.
|
|
576
|
+
|
|
577
|
+
:param archive_file: archive file object to build the file format object from
|
|
578
|
+
:type archive_file: L{SAPCARArchiveFile}
|
|
579
|
+
|
|
580
|
+
:param version: version of the file to construct
|
|
581
|
+
:type version: string
|
|
582
|
+
|
|
583
|
+
:raise ValueError: if the version requested is invalid
|
|
584
|
+
"""
|
|
585
|
+
|
|
586
|
+
if version not in sapcar_archive_file_versions:
|
|
587
|
+
raise ValueError("Invalid version")
|
|
588
|
+
ff = sapcar_archive_file_versions[version]
|
|
589
|
+
|
|
590
|
+
new_archive_file = cls()
|
|
591
|
+
new_archive_file._file_format = ff()
|
|
592
|
+
new_archive_file._file_format.type = archive_file._file_format.type
|
|
593
|
+
new_archive_file._file_format.perm_mode = archive_file._file_format.perm_mode
|
|
594
|
+
new_archive_file._file_format.timestamp = archive_file._file_format.timestamp
|
|
595
|
+
new_archive_file._file_format.file_length = archive_file._file_format.file_length
|
|
596
|
+
new_archive_file._file_format.filename = archive_file._file_format.filename
|
|
597
|
+
new_archive_file._file_format.filename_length = archive_file._file_format.filename_length
|
|
598
|
+
|
|
599
|
+
for block in archive_file._file_format.blocks:
|
|
600
|
+
new_block = SAPCARCompressedBlockFormat()
|
|
601
|
+
new_block.type = block.type
|
|
602
|
+
new_block.compressed = SAPCARCompressedBlobFormat(bytes(block.compressed))
|
|
603
|
+
new_block.checksum = block.checksum
|
|
604
|
+
new_archive_file._file_format.blocks.append(new_block)
|
|
605
|
+
|
|
606
|
+
return new_archive_file
|
|
607
|
+
|
|
608
|
+
def open(self, enforce_checksum=False):
|
|
609
|
+
"""Opens the compressed file and returns a file-like object that
|
|
610
|
+
can be used to access its uncompressed content.
|
|
611
|
+
|
|
612
|
+
:param enforce_checksum: If the checksum validation should be enforce
|
|
613
|
+
:type enforce_checksum: bool
|
|
614
|
+
|
|
615
|
+
:return: file-like object with the uncompressed file content
|
|
616
|
+
:rtype: file
|
|
617
|
+
|
|
618
|
+
:raise Exception: If the file to open is a directory
|
|
619
|
+
:raise DecompressError: If there's a decompression error
|
|
620
|
+
:raise SAPCARInvalidFileException: If the file is invalid
|
|
621
|
+
:raise SAPCARInvalidChecksumException: If the checksum is invalid
|
|
622
|
+
"""
|
|
623
|
+
# Check that the type is file, so we don't try to extract from a directory
|
|
624
|
+
if self.is_directory():
|
|
625
|
+
raise Exception("Invalid file type")
|
|
626
|
+
|
|
627
|
+
# Extract the file to a file-like object
|
|
628
|
+
out_file = BytesIO()
|
|
629
|
+
checksum = self._file_format.extract(out_file)
|
|
630
|
+
out_file.seek(0)
|
|
631
|
+
|
|
632
|
+
# Validate the checksum if required
|
|
633
|
+
if enforce_checksum:
|
|
634
|
+
if checksum != self.calculate_checksum(out_file.getvalue()):
|
|
635
|
+
raise SAPCARInvalidChecksumException("Invalid checksum found")
|
|
636
|
+
out_file.seek(0)
|
|
637
|
+
|
|
638
|
+
# Return the extracted file
|
|
639
|
+
return out_file
|
|
640
|
+
|
|
641
|
+
def check_checksum(self):
|
|
642
|
+
"""Checks if the checksum of the file is valid.
|
|
643
|
+
|
|
644
|
+
:return: if the checksum matches
|
|
645
|
+
:rtype: bool
|
|
646
|
+
"""
|
|
647
|
+
if self.size == 0:
|
|
648
|
+
return True
|
|
649
|
+
crc = self.calculate_checksum(self.open().read())
|
|
650
|
+
return crc == self.checksum
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
class SAPCARArchive(object):
|
|
654
|
+
"""Proxy class that can be used to read SAP CAR archive files.
|
|
655
|
+
"""
|
|
656
|
+
|
|
657
|
+
# Instance attributes
|
|
658
|
+
filename = None
|
|
659
|
+
fd = None
|
|
660
|
+
_sapcar = None
|
|
661
|
+
|
|
662
|
+
def __init__(self, fil, mode="rb+", version=SAPCAR_VERSION_201):
|
|
663
|
+
"""Opens an archive file and allow access to it.
|
|
664
|
+
|
|
665
|
+
:param fil: filename or file descriptor to open
|
|
666
|
+
:type fil: string or file
|
|
667
|
+
|
|
668
|
+
:param mode: mode to open the file
|
|
669
|
+
:type mode: string
|
|
670
|
+
|
|
671
|
+
:param version: archive file version to use when creating
|
|
672
|
+
:type version: string
|
|
673
|
+
"""
|
|
674
|
+
|
|
675
|
+
# Ensure version is withing supported versions
|
|
676
|
+
if version not in sapcar_archive_file_versions:
|
|
677
|
+
raise ValueError("Invalid version")
|
|
678
|
+
|
|
679
|
+
# Ensure mode is within supported modes
|
|
680
|
+
if mode not in ["r", "r+", "w", "w+", "rb", "rb+", "wb", "wb+"]:
|
|
681
|
+
raise ValueError("Invalid mode")
|
|
682
|
+
|
|
683
|
+
# Ensure file is open in binary mode
|
|
684
|
+
if "b" not in mode:
|
|
685
|
+
mode += "b"
|
|
686
|
+
|
|
687
|
+
if isinstance(fil, str):
|
|
688
|
+
self.filename = fil
|
|
689
|
+
self.fd = open(fil, mode)
|
|
690
|
+
else:
|
|
691
|
+
self.filename = getattr(fil, "name", None)
|
|
692
|
+
self.fd = fil
|
|
693
|
+
|
|
694
|
+
if "r" in mode:
|
|
695
|
+
self.read()
|
|
696
|
+
else:
|
|
697
|
+
self.create()
|
|
698
|
+
self.version = version
|
|
699
|
+
|
|
700
|
+
@property
|
|
701
|
+
def files(self):
|
|
702
|
+
"""The list of file objects inside this archive file.
|
|
703
|
+
|
|
704
|
+
:return: list of file objects
|
|
705
|
+
:rtype: L{dict} of L{SAPCARArchiveFile}
|
|
706
|
+
"""
|
|
707
|
+
fils = {}
|
|
708
|
+
if self._files:
|
|
709
|
+
for fil in self._files:
|
|
710
|
+
fils[fil.filename] = SAPCARArchiveFile(fil)
|
|
711
|
+
return fils
|
|
712
|
+
|
|
713
|
+
@property
|
|
714
|
+
def files_names(self):
|
|
715
|
+
"""The list of file names inside this archive file.
|
|
716
|
+
|
|
717
|
+
:return: list of file names
|
|
718
|
+
:rtype: L{list} of L{string}
|
|
719
|
+
"""
|
|
720
|
+
return self.files.keys()
|
|
721
|
+
|
|
722
|
+
@property
|
|
723
|
+
def version(self):
|
|
724
|
+
"""The version of the archive file.
|
|
725
|
+
|
|
726
|
+
:return: version
|
|
727
|
+
:rtype: string
|
|
728
|
+
"""
|
|
729
|
+
return self._sapcar.version
|
|
730
|
+
|
|
731
|
+
@version.setter
|
|
732
|
+
def version(self, version):
|
|
733
|
+
"""Sets the version of the file. If the version is different to the current one, it
|
|
734
|
+
converts the archive file.
|
|
735
|
+
|
|
736
|
+
:param version: version to set
|
|
737
|
+
:type version: string
|
|
738
|
+
"""
|
|
739
|
+
if version not in sapcar_archive_file_versions:
|
|
740
|
+
raise ValueError("Invalid version")
|
|
741
|
+
# If version is different, we should convert each file
|
|
742
|
+
if version != self._sapcar.version:
|
|
743
|
+
fils = []
|
|
744
|
+
for fil in self.files.values():
|
|
745
|
+
new_file = SAPCARArchiveFile.from_archive_file(fil, version=version)
|
|
746
|
+
fils.append(new_file._file_format)
|
|
747
|
+
self._files.remove(fil._file_format)
|
|
748
|
+
self._sapcar.version = version
|
|
749
|
+
if self._files is None:
|
|
750
|
+
self._files = []
|
|
751
|
+
self._files.extend(fils)
|
|
752
|
+
|
|
753
|
+
def read(self):
|
|
754
|
+
"""Reads the SAP CAR archive file and populates the files list.
|
|
755
|
+
|
|
756
|
+
:raise Exception: if the file is invalid or unsupported
|
|
757
|
+
"""
|
|
758
|
+
self.fd.seek(0)
|
|
759
|
+
self._sapcar = SAPCARArchiveFormat(self.fd.read())
|
|
760
|
+
if self._sapcar.magic_string not in [SAPCAR_HEADER_MAGIC_STRING_STANDARD, SAPCAR_HEADER_MAGIC_STRING_BACKUP]:
|
|
761
|
+
raise Exception("Invalid or unsupported magic string in file")
|
|
762
|
+
if self._sapcar.version not in sapcar_archive_file_versions:
|
|
763
|
+
raise Exception("Invalid or unsupported version in file")
|
|
764
|
+
|
|
765
|
+
@property
|
|
766
|
+
def _files(self):
|
|
767
|
+
"""The file format objects according to the version.
|
|
768
|
+
|
|
769
|
+
:return: files format objects according to the version
|
|
770
|
+
"""
|
|
771
|
+
if self.version == SAPCAR_VERSION_200:
|
|
772
|
+
return self._sapcar.files0
|
|
773
|
+
else:
|
|
774
|
+
return self._sapcar.files1
|
|
775
|
+
|
|
776
|
+
@_files.setter
|
|
777
|
+
def _files(self, files):
|
|
778
|
+
if self.version == SAPCAR_VERSION_200:
|
|
779
|
+
self._sapcar.files0 = files
|
|
780
|
+
else:
|
|
781
|
+
self._sapcar.files1 = files
|
|
782
|
+
|
|
783
|
+
def create(self):
|
|
784
|
+
"""Creates the structure for holding a new SAP CAR archive file.
|
|
785
|
+
"""
|
|
786
|
+
self._sapcar = SAPCARArchiveFormat()
|
|
787
|
+
|
|
788
|
+
def write(self):
|
|
789
|
+
"""Writes the SAP CAR archive file to the file descriptor.
|
|
790
|
+
"""
|
|
791
|
+
self.fd.seek(0)
|
|
792
|
+
self.fd.write(bytes(self._sapcar))
|
|
793
|
+
self.fd.flush()
|
|
794
|
+
|
|
795
|
+
def write_as(self, filename=None):
|
|
796
|
+
"""Writes the SAP CAR archive file to another file.
|
|
797
|
+
|
|
798
|
+
:param filename: name of the file to write to
|
|
799
|
+
:type filename: string
|
|
800
|
+
"""
|
|
801
|
+
if not filename:
|
|
802
|
+
self.write()
|
|
803
|
+
else:
|
|
804
|
+
with open(filename, "wb") as fd:
|
|
805
|
+
fd.write(bytes(self._sapcar))
|
|
806
|
+
|
|
807
|
+
def add_file(self, filename, archive_filename=None):
|
|
808
|
+
"""Adds a new file to the SAP CAR archive file.
|
|
809
|
+
|
|
810
|
+
:param filename: name of the file to add
|
|
811
|
+
:type filename: string
|
|
812
|
+
|
|
813
|
+
:param archive_filename: name of the file to use in the archive
|
|
814
|
+
:type archive_filename: string
|
|
815
|
+
"""
|
|
816
|
+
fil = SAPCARArchiveFile.from_file(filename, self.version, archive_filename)
|
|
817
|
+
self._files.append(fil._file_format)
|
|
818
|
+
|
|
819
|
+
def open(self, filename):
|
|
820
|
+
"""Returns a file-like object that can be used to access a file
|
|
821
|
+
inside the SAP CAR archive.
|
|
822
|
+
|
|
823
|
+
:param filename: name of the file to open
|
|
824
|
+
:type filename: string
|
|
825
|
+
|
|
826
|
+
:return: a file-like object that can be used to access the decompressed file.
|
|
827
|
+
:rtype: file
|
|
828
|
+
"""
|
|
829
|
+
if filename not in self.files:
|
|
830
|
+
raise Exception("Invalid filename")
|
|
831
|
+
return self.files[filename].open()
|
|
832
|
+
|
|
833
|
+
def close(self):
|
|
834
|
+
"""Close the file descriptor object associated to the archive file.
|
|
835
|
+
"""
|
|
836
|
+
self.fd.close()
|
|
837
|
+
|
|
838
|
+
def raw(self):
|
|
839
|
+
"""Returns the raw data of the archive file.
|
|
840
|
+
|
|
841
|
+
:return: raw data
|
|
842
|
+
:rtype: string
|
|
843
|
+
"""
|
|
844
|
+
if self._sapcar:
|
|
845
|
+
return bytes(self._sapcar)
|
|
846
|
+
return b""
|