#include "hpa101saptype.h"
#include "hpa104CsObject.h"
@@ -32,6 +36,13 @@
#include "hpa107cslzh.h"
#include "hpa105CsObjInt.h"
+/* compress and decompress take and return bytes on Py3, unfortunately there's no way to enforce it on Py2 */
+#if PY_MAJOR_VERSION >= 3
+ #define BYTES_FORMAT "y#"
+#else
+ #define BYTES_FORMAT "s#"
+#endif
+
/* Memory allocation factor constant */
#define MEMORY_ALLOC_FACTOR 10
@@ -364,7 +375,7 @@ static char pysapcompress_compress_doc[] = "Compress a buffer using SAP's compre
":param str in: input buffer to compress\n\n"
":param int algorithm: algorithm to use\n\n"
":return: tuple with return code, output length and output buffer\n"
- ":rtype: tuple of int, int, string\n\n"
+ ":rtype: tuple of int, int, bytes\n\n"
":raises CompressError: if an error occurred during compression\n";
static PyObject *
@@ -373,6 +384,7 @@ pysapcompress_compress(PyObject *self, PyObject *args, PyObject *keywds)
const unsigned char *in = NULL;
unsigned char *out = NULL;
int status = 0, in_length = 0, out_length = 0, algorithm = ALG_LZC;
+ Py_ssize_t in_length_arg = 0;
/* Define the keyword list */
static char kwin[] = "in";
@@ -380,9 +392,16 @@ pysapcompress_compress(PyObject *self, PyObject *args, PyObject *keywds)
static char* kwlist[] = {kwin, kwalgorithm, NULL};
/* Parse the parameters. We are also interested in the length of the input buffer. */
- if (!PyArg_ParseTupleAndKeywords(args, keywds, "s#|i", kwlist, &in, &in_length, &algorithm))
+ if (!PyArg_ParseTupleAndKeywords(args, keywds, BYTES_FORMAT"|i", kwlist, &in, &in_length_arg, &algorithm))
return (NULL);
+ /* Check the size of length args and convert from Py_ssize_t to int */
+ if (in_length_arg > INT_MAX) {
+ return (PyErr_Format(compression_exception, "Compression error (Input length is larger than INT_MAX)"));
+ }
+
+ in_length = Py_SAFE_DOWNCAST(in_length_arg, Py_ssize_t, int);
+
/* Call the compression function */
status = compress_packet(in, in_length, &out, &out_length, algorithm);
@@ -398,7 +417,7 @@ pysapcompress_compress(PyObject *self, PyObject *args, PyObject *keywds)
}
/* It no error was raised, return the compressed buffer and the length */
- return (Py_BuildValue("iis#", status, out_length, out, out_length));
+ return (Py_BuildValue("ii"BYTES_FORMAT, status, out_length, out, out_length));
}
@@ -407,7 +426,7 @@ static char pysapcompress_decompress_doc[] = "Decompress a buffer using SAP's co
":param str in: input buffer to decompress\n"
":param int out_length: length of the output to decompress\n"
":return: tuple of return code, output length and output buffer\n"
- ":rtype: tuple of int, int, string\n\n"
+ ":rtype: tuple of int, int, bytes\n\n"
":raises DecompressError: if an error occurred during decompression\n";
static PyObject *
@@ -416,11 +435,21 @@ pysapcompress_decompress(PyObject *self, PyObject *args)
const unsigned char *in = NULL;
unsigned char *out = NULL;
int status = 0, in_length = 0, out_length = 0;
+ Py_ssize_t in_length_arg = 0, out_length_arg = 0;
/* Parse the parameters. We are also interested in the length of the input buffer. */
- if (!PyArg_ParseTuple(args, "s#i", &in, &in_length, &out_length))
+ if (!PyArg_ParseTuple(args, BYTES_FORMAT"i", &in, &in_length_arg, &out_length_arg))
return (NULL);
+ /* Check the size of length args and convert from Py_ssize_t to int */
+ if (in_length_arg > INT_MAX)
+ return (PyErr_Format(decompression_exception, "Decompression error (Input length is larger than INT_MAX)"));
+ if (out_length_arg > INT_MAX)
+ return (PyErr_Format(decompression_exception, "Decompression error (Output length is larger than INT_MAX)"));
+
+ in_length = Py_SAFE_DOWNCAST(in_length_arg, Py_ssize_t, int);
+ out_length = Py_SAFE_DOWNCAST(out_length_arg, Py_ssize_t, int);
+
/* Call the compression function */
status = decompress_packet(in, in_length, &out, &out_length);
@@ -434,7 +463,7 @@ pysapcompress_decompress(PyObject *self, PyObject *args)
return (PyErr_Format(decompression_exception, "Decompression error (%s)", error_string(status)));
}
/* It no error was raised, return the uncompressed buffer and the length */
- return (Py_BuildValue("iis#", status, out_length, out, out_length));
+ return (Py_BuildValue("ii"BYTES_FORMAT, status, out_length, out, out_length));
}
@@ -450,12 +479,31 @@ static PyMethodDef pysapcompressMethods[] = {
static char pysapcompress_module_doc[] = "Library implementing SAP's LZH and LZC compression algorithms.";
/* Module initialization */
-PyMODINIT_FUNC
-initpysapcompress(void)
+/* Python 2 and 3 compatiblitily shenanigans, from http://python3porting.com/cextensions.html */
+#if PY_MAJOR_VERSION >= 3
+ #define MOD_ERROR_VAL NULL
+ #define MOD_SUCCESS_VAL(val) val
+ #define MOD_INIT(name) PyMODINIT_FUNC PyInit_##name(void)
+ #define MOD_DEF(ob, name, doc, methods) \
+ static struct PyModuleDef moduledef = { \
+ PyModuleDef_HEAD_INIT, name, doc, -1, methods, }; \
+ ob = PyModule_Create(&moduledef);
+#else
+ #define MOD_ERROR_VAL
+ #define MOD_SUCCESS_VAL(val)
+ #define MOD_INIT(name) PyMODINIT_FUNC init##name(void)
+ #define MOD_DEF(ob, name, doc, methods) \
+ ob = Py_InitModule3(name, methods, doc);
+#endif
+
+MOD_INIT(pysapcompress)
{
PyObject *module = NULL;
/* Create the module and define the methods */
- module = Py_InitModule3("pysapcompress", pysapcompressMethods, pysapcompress_module_doc);
+ MOD_DEF(module, "pysapcompress", pysapcompress_module_doc, pysapcompressMethods)
+
+ if (module == NULL)
+ return MOD_ERROR_VAL;
/* Add the algorithm constants */
PyModule_AddIntConstant(module, "ALG_LZC", ALG_LZC);
@@ -468,4 +516,5 @@ initpysapcompress(void)
decompression_exception = PyErr_NewException(decompression_exception_name, NULL, NULL);
PyModule_AddObject(module, decompression_exception_short, decompression_exception);
+ return MOD_SUCCESS_VAL(module);
}
diff --git a/requirements-docs.txt b/requirements-docs.txt
index da2a1fc..52dccfa 100644
--- a/requirements-docs.txt
+++ b/requirements-docs.txt
@@ -1,6 +1,7 @@
Sphinx==1.8.5
ipykernel
-nbsphinx==0.5.1
-pyx==0.12.1
+nbsphinx
+pyx==0.12.1; python_version < '3.2'
+pyx==0.14.1; python_version >= '3.2'
ipython<6.0
m2r==0.2.1
diff --git a/requirements-tests.txt b/requirements-tests.txt
new file mode 100644
index 0000000..6860787
--- /dev/null
+++ b/requirements-tests.txt
@@ -0,0 +1,4 @@
+testfixtures==6.17.1
+mock==3.0.5; python_version < '3'
+tox==3.23.1
+pytest
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index d049cba..6faa008 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
+six==1.15
scapy==2.4.4
cryptography==2.9.2
diff --git a/setup.py b/setup.py
index d773cd9..873f103 100755
--- a/setup.py
+++ b/setup.py
@@ -104,6 +104,7 @@ def run(self):
url=pysap.__url__,
download_url=pysap.__url__,
license=pysap.__license__,
+ python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4',
classifiers=['Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
@@ -122,9 +123,6 @@ def run(self):
# Script files
scripts=['bin/pysapcar', 'bin/pysapgenpse'],
- # Tests command
- test_suite='tests.test_suite',
-
# Documentation commands
cmdclass={'doc': DocumentationCommand,
'notebooks': PreExecuteNotebooksCommand},
diff --git a/tests/__init__.py b/tests/__init__.py
old mode 100755
new mode 100644
index 763d2ce..ccbe1b2
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -21,7 +21,7 @@
import unittest
-test_suite = unittest.defaultTestLoader.discover('.', '*_test.py')
+test_suite = unittest.defaultTestLoader.discover('.', 'test_*.py')
if __name__ == '__main__':
diff --git a/tests/crypto_test.py b/tests/test_crypto.py
old mode 100644
new mode 100755
similarity index 100%
rename from tests/crypto_test.py
rename to tests/test_crypto.py
diff --git a/tests/pysapcompress_test.py b/tests/test_pysapcompress.py
similarity index 87%
rename from tests/pysapcompress_test.py
rename to tests/test_pysapcompress.py
index ff25e4f..c2ace8a 100755
--- a/tests/pysapcompress_test.py
+++ b/tests/test_pysapcompress.py
@@ -17,39 +17,42 @@
#
# Standard imports
+from __future__ import unicode_literals
import sys
import unittest
+# External imports
+from six import assertRaisesRegex, text_type
# Custom imports
from tests.utils import read_data_file
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"""
try:
import pysapcompress # @UnusedImport # noqa: F401
except ImportError as e:
- self.Fail(str(e))
+ self.fail(text_type(e))
def test_compress_input(self):
"""Test compress function input"""
from pysapcompress import compress, CompressError
- self.assertRaisesRegexp(CompressError, "invalid input length", compress, "")
- self.assertRaisesRegexp(CompressError, "unknown algorithm", compress, "TestString", algorithm=999)
+ assertRaisesRegex(self, CompressError, "invalid input length", compress, b"")
+ assertRaisesRegex(self, CompressError, "unknown algorithm", compress, b"TestString", algorithm=999)
def test_decompress_input(self):
"""Test decompress function input"""
from pysapcompress import decompress, DecompressError
- self.assertRaisesRegexp(DecompressError, "invalid input length", decompress, "", 1)
- self.assertRaisesRegexp(DecompressError, "input not compressed", decompress, "AAAAAAAA", 1)
- self.assertRaisesRegexp(DecompressError, "unknown algorithm", decompress,
- "\x0f\x00\x00\x00\xff\x1f\x9d\x00\x00\x00\x00", 1)
+ assertRaisesRegex(self, DecompressError, "invalid input length", decompress, b"", 1)
+ assertRaisesRegex(self, DecompressError, "input not compressed", decompress, b"AAAAAAAA", 1)
+ assertRaisesRegex(self, DecompressError, "unknown algorithm", decompress,
+ b"\x0f\x00\x00\x00\xff\x1f\x9d\x00\x00\x00\x00", 1)
def test_lzc(self):
"""Test compression and decompression using LZC algorithm"""
diff --git a/tests/sapcar_test.py b/tests/test_sapcar.py
similarity index 92%
rename from tests/sapcar_test.py
rename to tests/test_sapcar.py
index c2173f9..766fc2a 100755
--- a/tests/sapcar_test.py
+++ b/tests/test_sapcar.py
@@ -17,10 +17,11 @@
#
# Standard imports
+from __future__ import unicode_literals
+import six
import sys
import unittest
-from os import unlink, rmdir
-from os.path import basename, exists
+from os import unlink, rmdir, path
# External imports
# Custom imports
from tests.utils import data_filename
@@ -36,7 +37,7 @@ class PySAPCARTest(unittest.TestCase):
test_timestamp = "01 Dec 2015 22:48"
test_perm_mode = 33204
test_permissions = "-rw-rw-r--"
- test_string = "The quick brown fox jumps over the lazy dog"
+ test_string = b"The quick brown fox jumps over the lazy dog"
def setUp(self):
with open(self.test_filename, "wb") as fd:
@@ -44,9 +45,11 @@ def setUp(self):
def tearDown(self):
for filename in [self.test_filename, self.test_archive_file]:
- if exists(filename):
+ if path.exists(filename):
unlink(filename)
- if exists("test"):
+ if path.exists(path.join("test", "blah")):
+ unlink(path.join("test", "blah"))
+ if path.exists("test"):
rmdir("test")
def check_sapcar_archive(self, filename, version):
@@ -55,12 +58,15 @@ def check_sapcar_archive(self, filename, version):
with open(data_filename(filename), "rb") as fd:
sapcar_archive = SAPCARArchive(fd, mode="r")
- self.assertEqual(filename, basename(sapcar_archive.filename))
+ self.assertEqual(filename, path.basename(sapcar_archive.filename))
self.assertEqual(version, sapcar_archive.version)
self.assertEqual(1, len(sapcar_archive.files))
self.assertEqual(1, len(sapcar_archive.files_names))
self.assertListEqual([self.test_filename], sapcar_archive.files_names)
- self.assertListEqual([self.test_filename], sapcar_archive.files.keys())
+ if six.PY2:
+ self.assertListEqual([self.test_filename], sapcar_archive.files.keys())
+ else:
+ self.assertListEqual([self.test_filename], list(sapcar_archive.files.keys()))
af = sapcar_archive.open(self.test_filename)
self.assertEqual(self.test_string, af.read())
@@ -111,7 +117,10 @@ def test_sapcar_archive_add_file(self):
self.assertEqual(2, len(ar.files))
self.assertEqual(2, len(ar.files_names))
self.assertListEqual([self.test_filename, self.test_filename+"two"], ar.files_names)
- self.assertListEqual([self.test_filename, self.test_filename+"two"], ar.files.keys())
+ if six.PY2:
+ self.assertListEqual([self.test_filename, self.test_filename+"two"], ar.files.keys())
+ else:
+ self.assertListEqual([self.test_filename, self.test_filename+"two"], list(ar.files.keys()))
for filename in [self.test_filename, self.test_filename+"two"]:
af = ar.open(filename)
diff --git a/tests/test_sapcarcli.py b/tests/test_sapcarcli.py
new file mode 100755
index 0000000..8e9bf9b
--- /dev/null
+++ b/tests/test_sapcarcli.py
@@ -0,0 +1,377 @@
+# ===========
+# pysap - Python library for crafting SAP's network protocols packets
+#
+# Copyright (C) 2012-2018 by Martin Gallo, Core Security
+#
+# The library was designed and developed by Martin Gallo from
+# Core Security's CoreLabs team.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+# ==============
+
+# Standard imports
+from __future__ import unicode_literals, absolute_import
+import unittest
+from os import path
+try:
+ from unittest import mock
+except ImportError:
+ import mock
+# External imports
+from testfixtures import LogCapture
+# Custom imports
+from tests.utils import data_filename
+from pysap.sapcarcli import PySAPCAR
+from pysap.SAPCAR import SAPCARArchive, SAPCARArchiveFile, SAPCARInvalidFileException, SAPCARInvalidChecksumException
+
+
+class PySAPCARCLITest(unittest.TestCase):
+
+ def setUp(self):
+ self.mock_file = mock.Mock(
+ spec=SAPCARArchiveFile,
+ filename=path.join("test", "bl\x00ah"),
+ is_directory=lambda: False,
+ is_file=lambda: True,
+ perm_mode="-rw-rw-r--",
+ timestamp_raw=7
+ )
+ self.mock_dir = mock.Mock(
+ spec=SAPCARArchiveFile,
+ filename=path.basename(self.mock_file.filename),
+ is_directory=lambda: True,
+ is_file=lambda: False,
+ perm_mode="-rwxrw-r--",
+ timestamp_raw=8
+ )
+ self.mock_archive = mock.Mock(
+ spec=SAPCARArchive,
+ files={
+ self.mock_file.filename: self.mock_file,
+ self.mock_dir.filename: self.mock_dir
+ }
+ )
+ self.cli = PySAPCAR()
+ self.cli.mode = "r"
+ self.cli.archive_fd = open(data_filename("car201_test_string.sar"), "rb")
+ self.cli.logger.handlers = [] # Mute logging while running unit tests
+ self.log = LogCapture() # Enable logger output checking
+
+ def tearDown(self):
+ try:
+ self.cli.archive_fd.close()
+ except Exception:
+ pass
+ self.log.uninstall()
+
+ def test_open_archive_fd_not_set(self):
+ temp = self.cli.archive_fd # Backup test archive file handle
+ self.cli.archive_fd = None
+ self.assertIsNone(self.cli.open_archive())
+ self.cli.archive_fd = temp
+
+ def test_open_archive_raises_exception(self):
+ # While SAPCARArchive is defined in pysap.SAPCAR, it's looked up in sapcarcli, so we need to patch it there
+ # See https://docs.python.org/3/library/unittest.mock.html#where-to-patch for details
+ with mock.patch("pysap.sapcarcli.SAPCARArchive") as mock_archive:
+ mock_archive.side_effect = Exception("unit test exception")
+ self.assertIsNone(self.cli.open_archive())
+ mock_archive.assert_called_once_with(self.cli.archive_fd, mode=self.cli.mode)
+
+ def test_open_archive_succeeds(self):
+ self.assertIsInstance(self.cli.open_archive(), SAPCARArchive)
+
+ def test_target_files_no_kwargs(self):
+ names = sorted(["list", "of", "test", "names"])
+ self.assertEqual(names, [n for n in self.cli.target_files(names)])
+
+ def test_target_files_with_kwargs(self):
+ names = sorted(["list", "of", "test", "names"])
+ targets = sorted(["list", "names", "blah"])
+ self.assertEqual(["list", "names"], [n for n in self.cli.target_files(names, targets)])
+
+ def test_list_open_fails(self):
+ with mock.patch.object(self.cli, "open_archive", return_value=None):
+ self.assertIsNone(self.cli.list(None, None))
+
+ def test_list_succeeds(self):
+ archive_attrs = {
+ "files_names": ["test_string.txt", "blah.so"],
+ "files": {
+ "test_string.txt": mock.MagicMock(**{
+ "permissions": "-rw-rw-r--",
+ "size": 43,
+ "timestamp": "01 Dec 2015 22:48",
+ "filename": "test_string.txt"
+ }),
+ "blah.so": mock.MagicMock(**{
+ "permissions": "-rwxrwxr-x",
+ "size": 1243,
+ "timestamp": "01 Dec 2017 13:37",
+ "filename": "blah.so"
+ })
+ }
+ }
+ mock_archive = mock.MagicMock(**archive_attrs)
+ with mock.patch.object(self.cli, "open_archive", return_value=mock_archive):
+ self.cli.list(None, None)
+ messages = ("{} {:>10} {} {}".format(fil.permissions, fil.size, fil.timestamp, fil.filename)
+ for fil in archive_attrs["files"].values())
+ logs = (("pysap.pysapcar", "INFO", message) for message in messages)
+ self.log.check_present(*logs, order_matters=False)
+
+ def test_append_no_args(self):
+ self.cli.append(None, [])
+ self.log.check_present(("pysap.pysapcar", "ERROR", "pysapcar: no files specified for appending"))
+
+ def test_append_open_fails(self):
+ with mock.patch.object(self.cli, "open_archive", return_value=None):
+ self.assertIsNone(self.cli.append(None, ["blah"]))
+ self.log.check() # Check that there's no log messages
+
+ def test_append_no_renaming(self):
+ names = ["list", "of", "test", "names"]
+ mock_sar = mock.Mock(spec=SAPCARArchive)
+ # Because of pass by reference and lazy string formatting in logging, we can't actually check that args are
+ # being printed correctly. Or we could if we logged a copy of args or str(args) in production code, but it would
+ # beat the point of pass by reference and lazy string formatting, so let's not. Left the generator here because
+ # it's pretty in it's obscurity. And because someone (probably future me) will probably bang one's head
+ # against the same wall I just did before coming to the same realization I just did, so here's a little
+ # something for the next poor soul. May it make your head-against-the-wall session shorter than mine was.
+ # debugs = (("pysap.pysapcar", "DEBUG", str(names[i + 1:])) for i in range(len(names)))
+ infos = (("pysap.pysapcar", "INFO", "d {}".format(name)) for name in names)
+ calls = [mock.call.add_file(name, archive_filename=name) for name in names]
+ calls.append(mock.call.write())
+ with mock.patch.object(self.cli, "open_archive", return_value=mock_sar):
+ # Pass copy of names so generator works correctly
+ self.cli.append(None, names[:])
+ # self.log.check_present(*debugs)
+ self.log.check_present(*infos)
+ # For some reason mock_sar.assert_has_calls(calls) fails, even though this passes...
+ self.assertEqual(calls, mock_sar.mock_calls)
+
+ def test_append_with_renaming(self):
+ names = ["test", "/n", "blah", "test", "test", "/n", "blah2"]
+ archive_names = [("test", "blah"), ("test", "blah2")]
+ # List instead of generator, as we need to insert one line
+ infos = [("pysap.pysapcar", "INFO", "d {} (original name {})".format(archive_name, name))
+ for name, archive_name in archive_names]
+ infos.insert(1, ("pysap.pysapcar", "INFO", "d {}".format("test")))
+ mock_sar = mock.Mock(spec=SAPCARArchive)
+ calls = [mock.call.add_file(name, archive_filename=archive_name) for name, archive_name in archive_names]
+ calls.insert(1, mock.call.add_file("test", archive_filename="test"))
+ calls.append(mock.call.write())
+ with mock.patch.object(self.cli, "open_archive", return_value=mock_sar):
+ self.cli.append(None, names[:])
+ self.log.check_present(*infos)
+ # For some reason mock_sar.assert_has_calls(calls) fails, even though this passes...
+ self.assertEqual(calls, mock_sar.mock_calls)
+
+ def test_extract_open_fails(self):
+ with mock.patch.object(self.cli, "open_archive", return_value=None):
+ self.assertIsNone(self.cli.extract(None, None))
+ self.log.check() # Check that there's no log messages
+
+ def test_extract_empty_archive(self):
+ with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive):
+ with mock.patch.object(self.cli, "target_files", return_value=[]):
+ self.cli.extract(None, None)
+ self.log.check(("pysap.pysapcar", "INFO", "pysapcar: 0 file(s) processed"))
+
+ def test_extract_invalid_file_type(self):
+ key = self.mock_file.filename
+ self.mock_file.is_file = lambda: False
+ self.mock_file.is_directory = lambda: False
+ with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive):
+ with mock.patch.object(self.cli, "target_files", return_value=[key]):
+ key = key.replace("\x00", "")
+ self.cli.extract(mock.MagicMock(outdir=False), None)
+ self.log.check(
+ ("pysap.pysapcar", "WARNING", "pysapcar: Invalid file type '{}'".format(key)),
+ ("pysap.pysapcar", "INFO", "pysapcar: 0 file(s) processed")
+ )
+
+ @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, chmod=mock.DEFAULT,
+ makedirs=mock.DEFAULT)
+ def test_extract_dir_exists(self, makedirs, chmod, utime):
+ makedirs.side_effect = OSError(17, "Unit test error")
+ key = self.mock_dir.filename
+ with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive):
+ with mock.patch.object(self.cli, "target_files", return_value=[key]):
+ key = key.replace("\x00", "")
+ self.cli.extract(mock.MagicMock(outdir=False), None)
+ makedirs.assert_called_once_with(key)
+ chmod.assert_not_called()
+ utime.assert_not_called()
+ self.log.check(
+ ("pysap.pysapcar", "INFO", "d {}".format(key)),
+ ("pysap.pysapcar", "INFO", "pysapcar: 1 file(s) processed")
+ )
+
+ @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, chmod=mock.DEFAULT,
+ makedirs=mock.DEFAULT)
+ def test_extract_dir_creation_fails(self, makedirs, chmod, utime):
+ makedirs.side_effect = OSError(13, "Unit test error")
+ key = self.mock_dir.filename
+ with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive):
+ with mock.patch.object(self.cli, "target_files", return_value=[key]):
+ key = key.replace("\x00", "")
+ self.cli.extract(mock.MagicMock(outdir=False), None)
+ makedirs.assert_called_once_with(key)
+ chmod.assert_not_called()
+ utime.assert_not_called()
+ self.log.check(
+ ("pysap.pysapcar", "ERROR", "pysapcar: Could not create directory '{}' ([Errno 13] Unit test error)"
+ .format(key)),
+ ("pysap.pysapcar", "INFO", "pysapcar: Stopping extraction"),
+ ("pysap.pysapcar", "INFO", "pysapcar: 0 file(s) processed")
+ )
+
+ @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, chmod=mock.DEFAULT,
+ makedirs=mock.DEFAULT)
+ def test_extract_dir_passes(self, makedirs, chmod, utime):
+ key = self.mock_dir.filename
+ with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive):
+ with mock.patch.object(self.cli, "target_files", return_value=[key]):
+ key = key.replace("\x00", "")
+ self.cli.extract(mock.MagicMock(outdir=False), None)
+ makedirs.assert_called_once_with(key)
+ chmod.assert_called_once_with(key, self.mock_dir.perm_mode)
+ utime.assert_called_once_with(key, (self.mock_dir.timestamp_raw, self.mock_dir.timestamp_raw))
+ self.log.check(
+ ("pysap.pysapcar", "INFO", "d {}".format(key)),
+ ("pysap.pysapcar", "INFO", "pysapcar: 1 file(s) processed")
+ )
+
+ @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, fchmod=mock.DEFAULT,
+ makedirs=mock.DEFAULT)
+ def test_extract_file_intermediate_dir_exists(self, makedirs, fchmod, utime):
+ makedirs.side_effect = OSError(17, "Unit test error")
+ key = path.join("test", "bl\x00ah")
+ mock_file = mock.Mock(spec=SAPCARArchiveFile, is_directory=lambda: False, perm_mode="-rw-rw-r--",
+ timestamp_raw=7)
+ mock_arch = mock.Mock(spec=SAPCARArchive, files={key: mock_file})
+ with mock.patch.object(self.cli, "open_archive", return_value=mock_arch):
+ with mock.patch.object(self.cli, "target_files", return_value=[key]):
+ key = key.replace("\x00", "")
+ with mock.patch("pysap.sapcarcli.open", mock.mock_open()) as mock_open:
+ mock_open.return_value.fileno.return_value = 1337 # yo dawg...
+ self.cli.extract(mock.MagicMock(outdir=False), None)
+ dirname = path.dirname(key)
+ makedirs.assert_called_once_with(dirname)
+ fchmod.assert_called_once_with(1337, "-rw-rw-r--")
+ utime.assert_called_once_with(key, (7, 7))
+ self.log.check(
+ ("pysap.pysapcar", "INFO", "d {}".format(key)),
+ ("pysap.pysapcar", "INFO", "pysapcar: 1 file(s) processed")
+ )
+
+ @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, fchmod=mock.DEFAULT,
+ makedirs=mock.DEFAULT)
+ def test_extract_file_intermediate_dir_creation_fails(self, makedirs, fchmod, utime):
+ key = self.mock_file.filename
+ makedirs.side_effect = OSError(13, "Unit test error")
+ with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive):
+ with mock.patch.object(self.cli, "target_files", return_value=[key]):
+ key = key.replace("\x00", "")
+ self.cli.extract(mock.MagicMock(outdir=False), None)
+ dirname = path.dirname(key)
+ makedirs.assert_called_once_with(dirname)
+ fchmod.assert_not_called()
+ utime.assert_not_called()
+ self.log.check(
+ ("pysap.pysapcar", "ERROR", "pysapcar: Could not create intermediate directory '{}' for '{}' "
+ "(Unit test error)".format(dirname, key)),
+ ("pysap.pysapcar", "INFO", "pysapcar: Stopping extraction"),
+ ("pysap.pysapcar", "INFO", "pysapcar: 0 file(s) processed")
+ )
+
+ @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, fchmod=mock.DEFAULT,
+ makedirs=mock.DEFAULT)
+ def test_extract_file_passes(self, makedirs, fchmod, utime):
+ key = self.mock_file.filename
+ with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive):
+ with mock.patch.object(self.cli, "target_files", return_value=[key]):
+ key = key.replace("\x00", "")
+ with mock.patch("pysap.sapcarcli.open", mock.mock_open()) as mock_open:
+ mock_open.return_value.fileno.return_value = 1337 # yo dawg...
+ self.cli.extract(mock.MagicMock(outdir=False), None)
+ dirname = path.dirname(key)
+ makedirs.assert_called_once_with(dirname)
+ fchmod.assert_called_once_with(1337, "-rw-rw-r--")
+ utime.assert_called_once_with(key, (7, 7))
+ self.log.check(
+ ("pysap.pysapcar", "INFO", "d {}".format(dirname)),
+ ("pysap.pysapcar", "INFO", "d {}".format(key)),
+ ("pysap.pysapcar", "INFO", "pysapcar: 1 file(s) processed")
+ )
+
+ @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, fchmod=mock.DEFAULT)
+ def test_extract_file_invalid_file(self, fchmod, utime):
+ key = self.mock_file.filename
+ self.mock_file.open.side_effect = SAPCARInvalidFileException("Unit test error")
+ with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive):
+ with mock.patch.object(self.cli, "target_files", return_value=[key]):
+ self.cli.extract(mock.MagicMock(outdir=False, break_on_error=False), None)
+ fchmod.assert_not_called()
+ utime.assert_not_called()
+ self.log.check(
+ ("pysap.pysapcar", "ERROR", "pysapcar: Invalid SAP CAR file '{}' (Unit test error)"
+ .format(self.cli.archive_fd.name)),
+ ("pysap.pysapcar", "INFO", "pysapcar: Skipping extraction of file '{}'".format(key)),
+ ("pysap.pysapcar", "INFO", "pysapcar: 0 file(s) processed")
+ )
+
+ @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, fchmod=mock.DEFAULT)
+ def test_extract_file_invalid_checksum(self, fchmod, utime):
+ key = self.mock_file.filename
+ self.mock_file.open.side_effect = SAPCARInvalidChecksumException("Unit test error")
+ with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive):
+ with mock.patch.object(self.cli, "target_files", return_value=[key]):
+ self.cli.extract(mock.MagicMock(outdir=False, break_on_error=False), None)
+ fchmod.assert_not_called()
+ utime.assert_not_called()
+ self.log.check(
+ ("pysap.pysapcar", "ERROR", "pysapcar: Invalid checksum found for file '{}'"
+ .format(key)),
+ ("pysap.pysapcar", "INFO", "pysapcar: Stopping extraction"),
+ ("pysap.pysapcar", "INFO", "pysapcar: 0 file(s) processed")
+ )
+
+ @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, fchmod=mock.DEFAULT)
+ def test_extract_file_extraction_fails(self, fchmod, utime):
+ key = self.mock_file.filename
+ with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive):
+ with mock.patch.object(self.cli, "target_files", return_value=[key]):
+ key = key.replace("\x00", "")
+ with mock.patch("pysap.sapcarcli.open", mock.mock_open()) as mock_open:
+ mock_open.side_effect = OSError(13, "Unit test error")
+ self.cli.extract(mock.MagicMock(outdir=False), None)
+ dirname = path.dirname(key)
+ fchmod.assert_not_called()
+ utime.assert_not_called()
+ self.log.check(
+ ("pysap.pysapcar", "INFO", "d {}".format(dirname)),
+ ("pysap.pysapcar", "ERROR", "pysapcar: Failed to extract file '{}', ([Errno 13] Unit test error)".format(key)),
+ ("pysap.pysapcar", "INFO", "pysapcar: Stopping extraction"),
+ ("pysap.pysapcar", "INFO", "pysapcar: 0 file(s) processed")
+ )
+
+
+def test_suite():
+ loader = unittest.TestLoader()
+ suite = unittest.TestSuite()
+ suite.addTest(loader.loadTestsFromTestCase(PySAPCARCLITest))
+ return suite
+
+
+if __name__ == "__main__":
+ unittest.TextTestRunner(verbosity=2).run(test_suite())
diff --git a/tests/sapcredv2_test.py b/tests/test_sapcredv2.py
old mode 100644
new mode 100755
similarity index 96%
rename from tests/sapcredv2_test.py
rename to tests/test_sapcredv2.py
index 33a8a5f..e343e5a
--- a/tests/sapcredv2_test.py
+++ b/tests/test_sapcredv2.py
@@ -31,9 +31,11 @@
class PySAPCredV2Test(unittest.TestCase):
decrypt_username = "username"
- decrypt_pin = "1234567890"
- cert_name = "CN=PSEOwner"
- common_name = "PSEOwner"
+ decrypt_pin = b"1234567890"
+ cert_name = b"CN=PSEOwner"
+ common_name = b"PSEOwner"
+ pse_path = b"/secudir/pse-v2-noreq-DSA-1024-SHA1.pse"
+ pse_path_win = b"C:\\secudir\\pse-v2-noreq-DSA-1024-SHA1.pse"
subject_str = "/CN=PSEOwner"
subject = [
X509_RDN(rdn=[
@@ -41,8 +43,6 @@ class PySAPCredV2Test(unittest.TestCase):
value=ASN1_PRINTABLE_STRING(common_name))
])
]
- pse_path = "/secudir/pse-v2-noreq-DSA-1024-SHA1.pse"
- pse_path_win = "C:\\secudir\\pse-v2-noreq-DSA-1024-SHA1.pse"
def validate_credv2_lps_off_fields(self, creds, number, lps_type, cipher_format_version,
cipher_algorithm, cert_name=None, pse_path=None):
@@ -56,9 +56,9 @@ def validate_credv2_lps_off_fields(self, creds, number, lps_type, cipher_format_
self.assertEqual(cred.cipher_algorithm, cipher_algorithm)
self.assertEqual(cred.cert_name, cert_name or self.cert_name)
- self.assertEqual(cred.unknown1, "")
+ self.assertEqual(cred.unknown1, b"")
self.assertEqual(cred.pse_path, pse_path or self.pse_path)
- self.assertEqual(cred.unknown2, "")
+ self.assertEqual(cred.unknown2, b"")
def validate_credv2_plain(self, cred, decrypt_username=None, decrypt_pin=None):
plain = cred.decrypt(decrypt_username or self.decrypt_username)
@@ -85,6 +85,7 @@ def test_credv2_lps_off_v0_dp_3des(self):
with open(data_filename("credv2_lps_off_v0_dp_3des"), "rb") as fd:
s = fd.read()
creds = SAPCredv2(s).creds
+
self.validate_credv2_lps_off_fields(creds, 1, None, 0, CIPHER_ALGORITHM_3DES,
pse_path=self.pse_path_win)
diff --git a/tests/sapdiag_test.py b/tests/test_sapdiag.py
similarity index 100%
rename from tests/sapdiag_test.py
rename to tests/test_sapdiag.py
diff --git a/tests/saphdb_test.py b/tests/test_saphdb.py
similarity index 94%
rename from tests/saphdb_test.py
rename to tests/test_saphdb.py
index 204a45d..186da48 100755
--- a/tests/saphdb_test.py
+++ b/tests/test_saphdb.py
@@ -20,7 +20,8 @@
import sys
import unittest
from threading import Thread
-from SocketServer import BaseRequestHandler, ThreadingTCPServer
+# External imports
+from six.moves.socketserver import BaseRequestHandler, ThreadingTCPServer
# Custom imports
from pysap.SAPHDB import SAPHDBConnection
@@ -30,7 +31,7 @@ class SAPHDBServerTestHandler(BaseRequestHandler):
def handle_data(self):
self.request.recv(14)
- self.request.send("\x00" * 8)
+ self.request.send(b"\x00" * 8)
class PySAPHDBConnectionTest(unittest.TestCase):
diff --git a/tests/sapni_test.py b/tests/test_sapni.py
similarity index 96%
rename from tests/sapni_test.py
rename to tests/test_sapni.py
index 1757174..f2acecb 100755
--- a/tests/sapni_test.py
+++ b/tests/test_sapni.py
@@ -22,7 +22,7 @@
import unittest
from threading import Thread
from struct import pack, unpack
-from SocketServer import BaseRequestHandler, ThreadingTCPServer
+from six.moves.socketserver import BaseRequestHandler, ThreadingTCPServer
# External imports
from scapy.fields import StrField
from scapy.packet import Packet, Raw
@@ -53,13 +53,13 @@ def stop_server(self):
class PySAPNITest(unittest.TestCase):
- test_string = "LALA" * 10
+ test_string = b"LALA" * 10
def test_sapni_building(self):
"""Test SAPNI length field building"""
sapni = SAPNI() / self.test_string
- (sapni_length, ) = unpack("!I", str(sapni)[:4])
+ (sapni_length, ) = unpack("!I", bytes(sapni)[:4])
self.assertEqual(sapni_length, len(self.test_string))
self.assertEqual(sapni.payload.load, self.test_string)
@@ -91,21 +91,21 @@ class SAPNITestHandlerKeepAlive(SAPNITestHandler):
def handle(self):
SAPNITestHandler.handle(self)
- self.request.sendall("\x00\x00\x00\x08NI_PING\x00")
+ self.request.sendall(b"\x00\x00\x00\x08NI_PING\x00")
class SAPNITestHandlerClose(SAPNITestHandler):
"""Basic SAP NI server that closes the connection"""
def handle(self):
- self.request.send("")
+ self.request.send(b"")
class PySAPNIStreamSocketTest(PySAPBaseServerTest):
test_port = 8005
test_address = "127.0.0.1"
- test_string = "TEST" * 10
+ test_string = b"TEST" * 10
def test_sapnistreamsocket(self):
"""Test SAPNIStreamSocket"""
@@ -239,7 +239,7 @@ class PySAPNIServerTest(PySAPBaseServerTest):
test_port = 8005
test_address = "127.0.0.1"
- test_string = "TEST" * 10
+ test_string = b"TEST" * 10
handler_cls = SAPNIServerTestHandler
def test_sapniserver(self):
@@ -268,7 +268,7 @@ class PySAPNIProxyTest(PySAPBaseServerTest):
test_proxyport = 8005
test_serverport = 8006
test_address = "127.0.0.1"
- test_string = "TEST" * 10
+ test_string = b"TEST" * 10
proxyhandler_cls = SAPNIProxyHandler
serverhandler_cls = SAPNIServerTestHandler
@@ -326,7 +326,7 @@ def process_server(self, packet):
sock.connect((self.test_address, self.test_proxyport))
sock.sendall(pack("!I", len(self.test_string)) + self.test_string)
- expected_reponse = self.test_string + "Client" + "Server"
+ expected_reponse = self.test_string + b"Client" + b"Server"
response = sock.recv(4)
self.assertEqual(len(response), 4)
diff --git a/tests/sappse_test.py b/tests/test_sappse.py
old mode 100644
new mode 100755
similarity index 96%
rename from tests/sappse_test.py
rename to tests/test_sappse.py
index fa40a4b..17eb87b
--- a/tests/sappse_test.py
+++ b/tests/test_sappse.py
@@ -20,6 +20,7 @@
import sys
import unittest
# External imports
+from six import b, assertRaisesRegex
# Custom imports
from tests.utils import data_filename
from pysap.SAPPSE import (SAPPSEFile, PKCS12_ALGORITHM_PBE1_SHA_3DES_CBC)
@@ -51,7 +52,7 @@ def test_pse_v2_lps_off_pbes1_3des_sha1_decrypt(self):
s = fd.read()
pse = SAPPSEFile(s)
- self.assertRaisesRegexp(ValueError, "Invalid PIN supplied", pse.decrypt, "Some Invalid PIN")
+ assertRaisesRegex(self, ValueError, "Invalid PIN supplied", pse.decrypt, "Some Invalid PIN")
pse.decrypt(self.decrypt_pin)
def test_pse_v4_lps_off_pbes1_3des_sha1(self):
diff --git a/tests/saprouter_test.py b/tests/test_saprouter.py
similarity index 99%
rename from tests/saprouter_test.py
rename to tests/test_saprouter.py
index 8b13f9b..806f02e 100755
--- a/tests/saprouter_test.py
+++ b/tests/test_saprouter.py
@@ -219,7 +219,7 @@ def test_saproutedstreamsocket_getnisocket(self):
route = [SAPRouterRouteHop(hostname=self.test_address,
port=self.test_port)]
self.client = SAPRoutedStreamSocket.get_nisocket("10.0.0.1",
- "3200",
+ 3200,
route=route,
router_version=40)
diff --git a/tests/sapssfs_test.py b/tests/test_sapssfs.py
similarity index 85%
rename from tests/sapssfs_test.py
rename to tests/test_sapssfs.py
index 878cc6c..84eff86 100755
--- a/tests/sapssfs_test.py
+++ b/tests/test_sapssfs.py
@@ -19,15 +19,16 @@
# Standard imports
import unittest
# External imports
+from six import b
# Custom imports
from tests.utils import data_filename
-from pysap.SAPSSFS import (SAPSSFSKey, SAPSSFSData, SAPSSFSLock)
+from pysap.SAPSSFS import (SAPSSFSKey, SAPSSFSData)
class PySAPSSFSKeyTest(unittest.TestCase):
- USERNAME = "SomeUser "
- HOST = "ubuntu "
+ USERNAME = b("SomeUser ")
+ HOST = b("ubuntu ")
def test_ssfs_key_parsing(self):
"""Test parsing of a SSFS Key file"""
@@ -37,7 +38,7 @@ def test_ssfs_key_parsing(self):
key = SAPSSFSKey(s)
- self.assertEqual(key.preamble, "RSecSSFsKey")
+ self.assertEqual(key.preamble, b("RSecSSFsKey"))
self.assertEqual(key.type, 1)
self.assertEqual(key.user, self.USERNAME)
self.assertEqual(key.host, self.HOST)
@@ -45,12 +46,12 @@ def test_ssfs_key_parsing(self):
class PySAPSSFSDataTest(unittest.TestCase):
- USERNAME = "SomeUser "
- HOST = "ubuntu "
+ USERNAME = b("SomeUser ")
+ HOST = b("ubuntu ")
- PLAIN_VALUES = {"HDB/KEYNAME/DB_CON_ENV": "Env",
- "HDB/KEYNAME/DB_DATABASE_NAME": "Database",
- "HDB/KEYNAME/DB_USER": "SomeUser",
+ PLAIN_VALUES = {b("HDB/KEYNAME/DB_CON_ENV"): b("Env"),
+ b("HDB/KEYNAME/DB_DATABASE_NAME"): b("Database"),
+ b("HDB/KEYNAME/DB_USER"): b("SomeUser"),
}
def test_ssfs_data_parsing(self):
@@ -63,7 +64,7 @@ def test_ssfs_data_parsing(self):
self.assertEqual(len(data.records), 4)
for record in data.records:
- self.assertEqual(record.preamble, "RSecSSFsData")
+ self.assertEqual(record.preamble, b("RSecSSFsData"))
self.assertEqual(record.length, len(record))
self.assertEqual(record.type, 1)
self.assertEqual(record.user, self.USERNAME)
@@ -101,21 +102,21 @@ def test_ssfs_data_record_hmac(self):
# Now tamper with the header
original_user = record.user
- record.user = "NewUser"
+ record.user = b("NewUser")
self.assertFalse(record.valid)
record.user = original_user
self.assertTrue(record.valid)
# Now tamper with the data
orginal_data = record.data
- record.data = orginal_data + "AddedDataBytes"
+ record.data = orginal_data + b("AddedDataBytes")
self.assertFalse(record.valid)
record.data = orginal_data
self.assertTrue(record.valid)
# Now tamper with the HMAC
orginal_hmac = record.hmac
- record.hmac = orginal_hmac[:-1] + "A"
+ record.hmac = orginal_hmac[:-1] + b("A")
self.assertFalse(record.valid)
record.hmac = orginal_hmac
self.assertTrue(record.valid)
@@ -123,7 +124,7 @@ def test_ssfs_data_record_hmac(self):
class PySAPSSFSDataDecryptTest(unittest.TestCase):
- ENCRYPTED_VALUES = {"HDB/KEYNAME/DB_PASSWORD": "SomePassword"}
+ ENCRYPTED_VALUES = {b("HDB/KEYNAME/DB_PASSWORD"): b("SomePassword")}
def test_ssfs_data_record_decrypt(self):
"""Test decrypting a record with a given key in a SSFS Data file."""
diff --git a/tests/utils.py b/tests/utils.py
index 36e97d0..ec944fa 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -18,19 +18,19 @@
# Standard imports
from binascii import unhexlify
-from os.path import join as join, dirname
+from os import path
def data_filename(filename):
- return join(dirname(__file__), 'data', filename)
+ return path.join(path.dirname(__file__), 'data', filename)
def read_data_file(filename, unhex=True):
filename = data_filename(filename)
- with open(filename, 'r') as f:
+ with open(filename, 'rb') as f:
data = f.read()
- data = data.replace('\n', ' ').replace(' ', '')
+ data = data.replace(b'\n', b' ').replace(b' ', b'')
if unhex:
data = unhexlify(data)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..d88b43c
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,15 @@
+[tox]
+envlist = py27,py36,py37,py38
+
+[testenv]
+basepython =
+ py27: python2.7
+ py36: python3.6
+ py37: python3.7
+ py38: python3.8
+ py39: python3.9
+deps=-rrequirements.txt
+ -rrequirements-tests.txt
+passenv = NO_REMOTE
+commands_pre = {envpython} -m pip check
+commands = {envpython} -m pytest {posargs}
\ No newline at end of file
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy