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 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""