Skip to content

Commit

Permalink
SAPCAR: Python 3 compat for pysapcompress and SAPCAR (#59)
Browse files Browse the repository at this point in the history
  • Loading branch information
martingalloar committed Mar 21, 2023
1 parent 1a12274 commit 55a6251
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 98 deletions.
120 changes: 41 additions & 79 deletions pysap/SAPCAR.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
import stat
from zlib import crc32
from struct import pack
from stat import filemode
from datetime import datetime
from os import stat as os_stat
from io import StringIO
from io import BytesIO
# External imports
from scapy.packet import Packet
from scapy.fields import (ByteField, ByteEnumField, LEIntField, FieldLenField,
Expand All @@ -34,51 +35,9 @@
DecompressError)


# Filemode code obtained from Python 3 stat.py
_filemode_table = (
((stat.S_IFLNK, "l"),
(stat.S_IFREG, "-"),
(stat.S_IFBLK, "b"),
(stat.S_IFDIR, "d"),
(stat.S_IFCHR, "c"),
(stat.S_IFIFO, "p")),

((stat.S_IRUSR, "r"),),
((stat.S_IWUSR, "w"),),
((stat.S_IXUSR | stat.S_ISUID, "s"),
(stat.S_ISUID, "S"),
(stat.S_IXUSR, "x")),

((stat.S_IRGRP, "r"),),
((stat.S_IWGRP, "w"),),
((stat.S_IXGRP | stat.S_ISGID, "s"),
(stat.S_ISGID, "S"),
(stat.S_IXGRP, "x")),

((stat.S_IROTH, "r"),),
((stat.S_IWOTH, "w"),),
((stat.S_IXOTH | stat.S_ISVTX, "t"),
(stat.S_ISVTX, "T"),
(stat.S_IXOTH, "x"))
)


SIZE_FOUR_GB = 0xffffffff + 1


def filemode(mode):
"""Convert a file's mode to a string of the form '-rwxrwxrwx'."""
perm = []
for table in _filemode_table:
for bit, char in table:
if mode & bit == bit:
perm.append(char)
break
else:
perm.append("-")
return "".join(perm)


class SAPCARInvalidFileException(Exception):
"""Exception to denote an invalid SAP CAR file"""

Expand All @@ -98,24 +57,24 @@ class SAPCARCompressedBlobFormat(PacketNoPadded):
LEIntField("compressed_length", None),
LEIntField("uncompress_length", None),
ByteEnumField("algorithm", 0x12, {0x12: "LZH", 0x10: "LZC"}),
StrFixedLenField("magic_bytes", "\x1f\x9d", 2),
StrFixedLenField("magic_bytes", b"\x1f\x9d", 2),
ByteField("special", 2),
ConditionalField(StrField("blob", None, remain=4), lambda x: x.compressed_length <= 8),
ConditionalField(StrFixedLenField("blob", None, length_from=lambda x: x.compressed_length - 8),
lambda x: x.compressed_length > 8),
]


SAPCAR_BLOCK_TYPE_COMPRESSED_LAST = "ED"
SAPCAR_BLOCK_TYPE_COMPRESSED_LAST = b"ED"
"""SAP CAR compressed end of data block"""

SAPCAR_BLOCK_TYPE_COMPRESSED = "DA"
SAPCAR_BLOCK_TYPE_COMPRESSED = b"DA"
"""SAP CAR compressed block"""

SAPCAR_BLOCK_TYPE_UNCOMPRESSED_LAST = "UE"
SAPCAR_BLOCK_TYPE_UNCOMPRESSED_LAST = b"UE"
"""SAP CAR uncompressed end of data block"""

SAPCAR_BLOCK_TYPE_UNCOMPRESSED = "UD"
SAPCAR_BLOCK_TYPE_UNCOMPRESSED = b"UD"
"""SAP CAR uncompressed block"""


Expand Down Expand Up @@ -147,22 +106,22 @@ def sapcar_is_last_block(packet):
return packet.type in [SAPCAR_BLOCK_TYPE_COMPRESSED_LAST, SAPCAR_BLOCK_TYPE_UNCOMPRESSED_LAST]


SAPCAR_TYPE_FILE = "RG"
SAPCAR_TYPE_FILE = b"RG"
"""SAP CAR regular file string"""

SAPCAR_TYPE_DIR = "DR"
SAPCAR_TYPE_DIR = b"DR"
"""SAP CAR directory string"""

SAPCAR_TYPE_SHORTCUT = "SC"
SAPCAR_TYPE_SHORTCUT = b"SC"
"""SAP CAR Windows short cut string"""

SAPCAR_TYPE_LINK = "LK"
SAPCAR_TYPE_LINK = b"LK"
"""SAP CAR Unix soft link string"""

SAPCAR_TYPE_AS400 = "SV"
SAPCAR_TYPE_AS400 = b"SV"
"""SAP CAR AS400 save file string"""

SAPCAR_TYPE_SIGNATURE = "SM"
SAPCAR_TYPE_SIGNATURE = b"SM"
"""SAP CAR SIGNATURE.SMF file string"""
# XXX: Unsure if this file has any particular treatment in latest versions of SAPCAR

Expand Down Expand Up @@ -234,7 +193,7 @@ def extract(self, fd):
if self.file_length == 0:
return 0

compressed = ""
compressed = b""
checksum = 0
exp_length = None

Expand All @@ -247,7 +206,7 @@ def extract(self, fd):
# Store compressed block types for later decompression
elif block.type in [SAPCAR_BLOCK_TYPE_COMPRESSED, SAPCAR_BLOCK_TYPE_COMPRESSED_LAST]:
# Add compressed block to a buffer, skipping the first 4 bytes of each block (uncompressed length)
compressed += str(block.compressed)[4:]
compressed += bytes(block.compressed)[4:]
# If the expected length wasn't already set, do it
if not exp_length:
exp_length = block.compressed.uncompress_length
Expand All @@ -259,7 +218,7 @@ def extract(self, fd):
checksum = block.checksum
# If there was at least one compressed block that set the expected length, decompress it
if exp_length:
(_, block_length, block_buffer) = decompress(str(compressed), exp_length)
(_, block_length, block_buffer) = decompress(bytes(compressed), exp_length)
if block_length != exp_length or not block_buffer:
raise DecompressError("Error decompressing block")
fd.write(block_buffer)
Expand All @@ -279,10 +238,10 @@ class SAPCARArchiveFilev201Format(SAPCARArchiveFilev200Format):
is_filename_null_terminated = True


SAPCAR_HEADER_MAGIC_STRING_STANDARD = "CAR\x20"
SAPCAR_HEADER_MAGIC_STRING_STANDARD = b"CAR\x20"
"""SAP CAR archive header magic string standard"""

SAPCAR_HEADER_MAGIC_STRING_BACKUP = "CAR\x00"
SAPCAR_HEADER_MAGIC_STRING_BACKUP = b"CAR\x00"
"""SAP CAR archive header magic string backup file"""


Expand All @@ -304,9 +263,9 @@ class SAPCARArchiveFormat(Packet):
StrFixedLenField("magic_string", SAPCAR_HEADER_MAGIC_STRING_STANDARD, 4),
StrFixedLenField("version", SAPCAR_VERSION_201, 4),
ConditionalField(PacketListField("files0", None, SAPCARArchiveFilev200Format),
lambda x: x.version == SAPCAR_VERSION_200),
lambda x: x.version.decode() == SAPCAR_VERSION_200),
ConditionalField(PacketListField("files1", None, SAPCARArchiveFilev201Format),
lambda x: x.version == SAPCAR_VERSION_201),
lambda x: x.version.decode() == SAPCAR_VERSION_201),
]


Expand Down Expand Up @@ -368,7 +327,7 @@ def filename(self):
:return: name of the file
:rtype: string
"""
return self._file_format.filename
return self._file_format.filename.decode()

@filename.setter
def filename(self, filename):
Expand All @@ -377,7 +336,7 @@ def filename(self, filename):
:param filename: the name of the file
:type filename: string
"""
self._file_format.filename = filename
self._file_format.filename = str(filename)
self._file_format.filename_length = len(filename)
if self._file_format.version == SAPCAR_VERSION_201:
self._file_format.filename_length += 1
Expand Down Expand Up @@ -443,7 +402,7 @@ def timestamp(self, timestamp):
:param timestamp: the timestamp to set
:type timestamp: int
"""
self._file_format.timestamp = timestamp
self._file_format.timestamp = int(timestamp)

@property
def timestamp_raw(self):
Expand Down Expand Up @@ -533,21 +492,22 @@ def from_file(cls, filename, version=SAPCAR_VERSION_201, archive_filename=None):
out_buffer = pack("<I", out_length) + out_buffer

# Check the version and grab the file format class
if version not in sapcar_archive_file_versions:
if version not in list(sapcar_archive_file_versions.keys()):
raise ValueError("Invalid version")
ff = sapcar_archive_file_versions[version]

# If an archive filename was not provided, use the actual filename
if archive_filename is None:
archive_filename = filename
print(archive_filename)

# Build the object and fill the fields
archive_file = cls()
archive_file._file_format = ff()
archive_file._file_format.perm_mode = stat.st_mode
archive_file._file_format.timestamp = stat.st_atime
archive_file._file_format.timestamp = stat.st_atime_ns
archive_file._file_format.file_length = stat.st_size
archive_file._file_format.filename = archive_filename
archive_file._file_format.filename = archive_filename.encode('utf-8')
archive_file._file_format.filename_length = len(archive_filename)
if archive_file._file_format.version == SAPCAR_VERSION_201:
archive_file._file_format.filename_length += 1
Expand All @@ -558,6 +518,7 @@ def from_file(cls, filename, version=SAPCAR_VERSION_201, archive_filename=None):
block.checksum = cls.calculate_checksum(data)
archive_file._file_format.blocks.append(block)

archive_file._file_format.show()
return archive_file

@classmethod
Expand All @@ -573,7 +534,7 @@ def from_archive_file(cls, archive_file, version=SAPCAR_VERSION_201):
:raise ValueError: if the version requested is invalid
"""

if version not in sapcar_archive_file_versions:
if version not in list(sapcar_archive_file_versions.keys()):
raise ValueError("Invalid version")
ff = sapcar_archive_file_versions[version]

Expand All @@ -589,7 +550,7 @@ def from_archive_file(cls, archive_file, version=SAPCAR_VERSION_201):
for block in archive_file._file_format.blocks:
new_block = SAPCARCompressedBlockFormat()
new_block.type = block.type
new_block.compressed = SAPCARCompressedBlobFormat(str(block.compressed))
new_block.compressed = SAPCARCompressedBlobFormat(bytes(block.compressed))
new_block.checksum = block.checksum
new_archive_file._file_format.blocks.append(new_block)

Expand All @@ -615,7 +576,7 @@ def open(self, enforce_checksum=False):
raise Exception("Invalid file type")

# Extract the file to a file-like object
out_file = StringIO()
out_file = BytesIO()
checksum = self._file_format.extract(out_file)
out_file.seek(0)

Expand Down Expand Up @@ -663,7 +624,7 @@ def __init__(self, fil, mode="rb+", version=SAPCAR_VERSION_201):
"""

# Ensure version is withing supported versions
if version not in sapcar_archive_file_versions:
if version not in list(sapcar_archive_file_versions.keys()):
raise ValueError("Invalid version")

# Ensure mode is within supported modes
Expand Down Expand Up @@ -697,7 +658,7 @@ def files(self):
fils = {}
if self._files:
for fil in self._files:
fils[fil.filename] = SAPCARArchiveFile(fil)
fils[fil.filename.decode()] = SAPCARArchiveFile(fil)
return fils

@property
Expand All @@ -716,7 +677,7 @@ def version(self):
:return: version
:rtype: string
"""
return self._sapcar.version
return self._sapcar.version.decode()

@version.setter
def version(self, version):
Expand All @@ -726,10 +687,10 @@ def version(self, version):
:param version: version to set
:type version: string
"""
if version not in sapcar_archive_file_versions:
if version not in list(sapcar_archive_file_versions.keys()):
raise ValueError("Invalid version")
# If version is different, we should convert each file
if version != self._sapcar.version:
if version != self._sapcar.version.decode():
fils = []
for fil in list(self.files.values()):
new_file = SAPCARArchiveFile.from_archive_file(fil, version=version)
Expand All @@ -749,7 +710,8 @@ def read(self):
self._sapcar = SAPCARArchiveFormat(self.fd.read())
if self._sapcar.magic_string not in [SAPCAR_HEADER_MAGIC_STRING_STANDARD, SAPCAR_HEADER_MAGIC_STRING_BACKUP]:
raise Exception("Invalid or unsupported magic string in file")
if self._sapcar.version not in sapcar_archive_file_versions:
print(self._sapcar.version)
if self._sapcar.version.decode() not in list(sapcar_archive_file_versions.keys()):
raise Exception("Invalid or unsupported version in file")

@property
Expand Down Expand Up @@ -779,7 +741,7 @@ def write(self):
"""Writes the SAP CAR archive file to the file descriptor.
"""
self.fd.seek(0)
self.fd.write(str(self._sapcar))
self.fd.write(bytes(self._sapcar))
self.fd.flush()

def write_as(self, filename=None):
Expand All @@ -792,7 +754,7 @@ def write_as(self, filename=None):
self.write()
else:
with open(filename, "w") as fd:
fd.write(str(self._sapcar))
fd.write(bytes(self._sapcar))

def add_file(self, filename, archive_filename=None):
"""Adds a new file to the SAP CAR archive file.
Expand Down Expand Up @@ -832,5 +794,5 @@ def raw(self):
:rtype: string
"""
if self._sapcar:
return str(self._sapcar)
return bytes(self._sapcar)
return ""
2 changes: 1 addition & 1 deletion pysap/utils/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def getfield(self, pkt, s):
def addfield(self, pkt, s, val):
if self.null_terminated(pkt):
l = self.length_from(pkt) - 1
return s + struct.pack("%is" % l, self.i2m(pkt, val)) + "\x00"
return s + struct.pack("%is" % l, self.i2m(pkt, val)) + b"\x00"
return StrFixedLenField.addfield(self, pkt, s, val)

def randval(self):
Expand Down
18 changes: 9 additions & 9 deletions tests/test_pysapcompress.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@

class PySAPCompressTest(unittest.TestCase):

test_string_plain = "TEST" * 70
test_string_compr_lzc = '\x18\x01\x00\x00\x11\x1f\x9d\x8dT\x8aL\xa1\x12p`A\x82\x02\x11\x1aLx\xb0!\xc3\x87\x0b#*\x9c\xe8' \
'PbE\x8a\x101Z\xccx\xb1#\xc7\x8f\x1bCj\x1c\xe9QdI\x92 Q\x9aLy\xf2 '
test_string_compr_lzh = '\x18\x01\x00\x00\x12\x1f\x9d\x02]\x88kpH\xc8(\xc6\xc0\x00\x00'
test_string_plain = b"TEST" * 70
test_string_compr_lzc = b'\x18\x01\x00\x00\x11\x1f\x9d\x8dT\x8aL\xa1\x12p`A\x82\x02\x11\x1aLx\xb0!\xc3\x87\x0b#*\x9c\xe8' \
b'PbE\x8a\x101Z\xccx\xb1#\xc7\x8f\x1bCj\x1c\xe9QdI\x92 Q\x9aLy\xf2 '
test_string_compr_lzh = b'\x18\x01\x00\x00\x12\x1f\x9d\x02]\x88kpH\xc8(\xc6\xc0\x00\x00'

def test_import(self):
"""Test import of the pysapcompress library"""
Expand All @@ -40,16 +40,16 @@ def test_import(self):
def test_compress_input(self):
"""Test compress function input"""
from pysapcompress import compress, CompressError
self.assertRaisesRegex(CompressError, "invalid input length", compress, "")
self.assertRaisesRegex(CompressError, "unknown algorithm", compress, "TestString", algorithm=999)
self.assertRaisesRegex(CompressError, "invalid input length", compress, b"")
self.assertRaisesRegex(CompressError, "unknown algorithm", compress, b"TestString", algorithm=999)

def test_decompress_input(self):
"""Test decompress function input"""
from pysapcompress import decompress, DecompressError
self.assertRaisesRegex(DecompressError, "invalid input length", decompress, "", 1)
self.assertRaisesRegex(DecompressError, "input not compressed", decompress, "AAAAAAAA", 1)
self.assertRaisesRegex(DecompressError, "invalid input length", decompress, b"", 1)
self.assertRaisesRegex(DecompressError, "input not compressed", decompress, b"AAAAAAAA", 1)
self.assertRaisesRegex(DecompressError, "unknown algorithm", decompress,
"\x0f\x00\x00\x00\xff\x1f\x9d\x00\x00\x00\x00", 1)
b"\x0f\x00\x00\x00\xff\x1f\x9d\x00\x00\x00\x00", 1)

def test_lzc(self):
"""Test compression and decompression using LZC algorithm"""
Expand Down

0 comments on commit 55a6251

Please sign in to comment.