diff --git a/pmb/challenge/__init__.py b/pmb/challenge/__init__.py index a218f26f..92e45710 100644 --- a/pmb/challenge/__init__.py +++ b/pmb/challenge/__init__.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with pmbootstrap. If not, see . """ # Exported functions -from pmb.challenge.apk import apk +from pmb.challenge.apk_file import apk from pmb.challenge.apkindex import apkindex from pmb.challenge.build import build from pmb.challenge.frontend import frontend diff --git a/pmb/challenge/apk_file.py b/pmb/challenge/apk_file.py new file mode 100644 index 00000000..f05fa2ad --- /dev/null +++ b/pmb/challenge/apk_file.py @@ -0,0 +1,126 @@ +""" +Copyright 2017 Oliver Smith + +This file is part of pmbootstrap. + +pmbootstrap 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 3 of the License, or +(at your option) any later version. + +pmbootstrap 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. + +You should have received a copy of the GNU General Public License +along with pmbootstrap. If not, see . +""" +import logging +import os +import tarfile +import tempfile +import filecmp +import shutil + + +def contents_diff(tar_a, tar_b, member_a, member_b, name): + # Extract both files + tars = [tar_a, tar_b] + members = [member_a, member_b] + temp_files = [] + for i in range(2): + handle, path = tempfile.mkstemp("pmbootstrap") + handle = open(handle, "wb") + shutil.copyfileobj(tars[i].extractfile(members[i]), handle) + handle.close() + temp_files.append(path) + + # Compare and delete + equal = filecmp.cmp(temp_files[0], temp_files[1], shallow=False) + for temp_file in temp_files: + os.remove(temp_file) + if equal: + logging.debug("=> OK!") + else: + raise RuntimeError("File '" + name + "' is different!") + + +def contents_without_signature(tar, tar_name): + """ + The signature file name is always different. + This function raises an exception, when the number of signature + files in the archive is not 1. + :returns: a sorted list of all filenames inside the tar archive, + except for the signature file. + """ + names = tar.getnames() + found = False + ret = [] + for name in names: + if name.startswith(".SIGN.RSA."): + if found: + raise RuntimeError("More than one signature file found" + " inside " + tar_name + ": " + + str(names)) + else: + found = True + else: + ret.append(name) + + if not found: + raise RuntimeError("No signature file found inside " + + tar_name + ": " + str(names)) + return sorted(ret) + + +def apk(args, apk_a, apk_b, stop_after_first_error=False): + with tarfile.open(apk_a, "r:gz") as tar_a: + with tarfile.open(apk_b, "r:gz") as tar_b: + # List of files must be the same + list_a = contents_without_signature(tar_a, apk_a) + list_b = contents_without_signature(tar_b, apk_b) + if list_a != list_b: + logging.info("Files in " + apk_a + ":" + str(list_a)) + logging.info("Files in " + apk_b + ":" + str(list_b)) + raise RuntimeError( + "Both APKs do not contain the same file names!") + + # Iterate through the list + success = True + for name in list_a: + try: + logging.debug("Compare: " + name) + if name == ".PKGINFO": + logging.debug( + "=> Skipping: expected to be different") + continue + + # Get members + member_a = tar_a.getmember(name) + member_b = tar_b.getmember(name) + if member_a.type != member_b.type: + raise RuntimeError( + "Entry '" + name + "' has a different type!") + + if member_a.isdir(): + logging.debug("=> Skipping: directory") + elif member_a.isfile(): + contents_diff(tar_a, tar_b, member_a, member_b, name) + elif member_a.issym() or member_a.islnk(): + if member_a.linkname == member_b.linkname: + logging.debug( + "=> Both link to " + member_a.linkname) + else: + raise RuntimeError( + "Link " + name + " has a different target!") + else: + raise RuntimeError( + "Can't diff '" + name + "', unsupported type!") + except Exception as e: + logging.info("CHALLENGE FAILED for " + name + ":" + str(e)) + success = False + if stop_after_first_error: + raise + if not success: + raise RuntimeError("Challenge failed (see errors above)") diff --git a/pmb/challenge/build.py b/pmb/challenge/build.py index b9ff352f..3fa2bdf9 100644 --- a/pmb/challenge/build.py +++ b/pmb/challenge/build.py @@ -23,7 +23,7 @@ import pmb.build import pmb.parse.apkbuild import pmb.parse.other import pmb.helpers.repo -import pmb.challenge.apk +import pmb.challenge def build(args, apk_path): diff --git a/pmb/chroot/other.py b/pmb/chroot/other.py index 1f734657..b54a9dc6 100644 --- a/pmb/chroot/other.py +++ b/pmb/chroot/other.py @@ -38,3 +38,17 @@ def kernel_flavor_autodetect(args, suffix): """ pmb.chroot.apk.install(args, ["device-" + args.device], suffix) return kernel_flavors_installed(args, suffix)[0] + + +def tempfolder(args, path, suffix="native"): + """ + Create a temporary folder inside the chroot, that belongs to "user". + The folder gets deleted, if it already exists. + + :param path: of the temporary folder inside the chroot + :returns: the path + """ + if os.path.exists(args.work + "/chroot_" + suffix + path): + pmb.chroot.root(args, ["rm", "-r", path]) + pmb.chroot.user(args, ["mkdir", "-p", path]) + return path diff --git a/pmb/chroot/zap.py b/pmb/chroot/zap.py index 7c9264e1..b87dc3a8 100644 --- a/pmb/chroot/zap.py +++ b/pmb/chroot/zap.py @@ -27,7 +27,7 @@ def zap(args): pmb.chroot.shutdown(args) patterns = [ "chroot_native", - "chroot_buildroot_" + args.deviceinfo["arch"], + "chroot_buildroot_*", "chroot_rootfs_" + args.device, ] diff --git a/test/test_apk_static.py b/test/test_apk_static.py index 22215e7d..49f9a296 100644 --- a/test/test_apk_static.py +++ b/test/test_apk_static.py @@ -42,15 +42,15 @@ def args(request): def test_read_signature_info(args): # Tempfolder inside chroot for fake apk files tmp_path = "/tmp/test_read_signature_info" - tmp_path_chroot = args.work + "/chroot_native" + tmp_path - if os.path.exists(tmp_path_chroot): + tmp_path_outside = args.work + "/chroot_native" + tmp_path + if os.path.exists(tmp_path_outside): pmb.chroot.root(args, ["rm", "-r", tmp_path]) pmb.chroot.user(args, ["mkdir", "-p", tmp_path]) # No signature found pmb.chroot.user(args, ["tar", "-czf", tmp_path + "/no_sig.apk", "/etc/issue"]) - with tarfile.open(tmp_path_chroot + "/no_sig.apk", "r:gz") as tar: + with tarfile.open(tmp_path_outside + "/no_sig.apk", "r:gz") as tar: with pytest.raises(RuntimeError) as e: pmb.chroot.apk_static.read_signature_info(tar) assert "Could not find signature" in str(e.value) @@ -62,7 +62,7 @@ def test_read_signature_info(args): pmb.chroot.user(args, ["tar", "-czf", tmp_path + "/invalid_sig.apk", "sbin/apk.static.SIGN.RSA.invalid.pub"], working_dir=tmp_path) - with tarfile.open(tmp_path_chroot + "/invalid_sig.apk", "r:gz") as tar: + with tarfile.open(tmp_path_outside + "/invalid_sig.apk", "r:gz") as tar: with pytest.raises(RuntimeError) as e: pmb.chroot.apk_static.read_signature_info(tar) assert "Invalid signature key" in str(e.value) @@ -75,7 +75,7 @@ def test_read_signature_info(args): tmp_path + "/" + path_archive]) pmb.chroot.user(args, ["tar", "-czf", tmp_path + "/realistic_name_sig.apk", path_archive], working_dir=tmp_path) - with tarfile.open(tmp_path_chroot + "/realistic_name_sig.apk", "r:gz") as tar: + with tarfile.open(tmp_path_outside + "/realistic_name_sig.apk", "r:gz") as tar: sigfilename, sigkey_path = pmb.chroot.apk_static.read_signature_info( tar) assert sigfilename == path_archive diff --git a/test/test_challenge_apk.py b/test/test_challenge_apk.py new file mode 100644 index 00000000..e7792da2 --- /dev/null +++ b/test/test_challenge_apk.py @@ -0,0 +1,270 @@ +""" +Copyright 2017 Oliver Smith + +This file is part of pmbootstrap. + +pmbootstrap 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 3 of the License, or +(at your option) any later version. + +pmbootstrap 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. + +You should have received a copy of the GNU General Public License +along with pmbootstrap. If not, see . +""" +import os +import sys +import pytest +import tarfile + +# Import from parent directory +sys.path.append(os.path.abspath( + os.path.join(os.path.dirname(__file__) + "/.."))) +import pmb.challenge.apk_file +import pmb.config +import pmb.chroot.other + + +@pytest.fixture +def args(request): + import pmb.parse + sys.argv = ["pmbootstrap.py", "chroot"] + args = pmb.parse.arguments() + setattr(args, "logfd", open("/dev/null", "a+")) + request.addfinalizer(args.logfd.close) + return args + + +def test_apk_challenge_contents_diff(args): + """ + Create two tar files, which contain a file with the same name. + The content of that file is different. + """ + # Tempfolder inside chroot for fake apk files + temp_path = pmb.chroot.other.tempfolder( + args, "/tmp/test_apk_challenge_contents_diff") + temp_path_outside = args.work + "/chroot_native" + temp_path + + # First file + name = "testfile" + apk_a = temp_path_outside + "/a.apk" + pmb.chroot.user(args, ["cp", "/etc/inittab", temp_path + "/" + name]) + pmb.chroot.user(args, ["tar", "-czf", "a.apk", name], + working_dir=temp_path) + + # Second file + apk_b = temp_path_outside + "/b.apk" + pmb.chroot.user(args, ["cp", "/etc/abuild.conf", temp_path + "/" + name]) + pmb.chroot.user(args, ["tar", "-czf", "b.apk", name], + working_dir=temp_path) + + # Compare OK + with tarfile.open(apk_a, "r:gz") as tar_a: + member_a = tar_a.getmember(name) + pmb.challenge.apk_file.contents_diff( + tar_a, tar_a, member_a, member_a, name) + + # Compare NOK + with tarfile.open(apk_b, "r:gz") as tar_b: + member_b = tar_b.getmember(name) + with pytest.raises(RuntimeError) as e: + pmb.challenge.apk_file.contents_diff(tar_a, tar_b, member_a, + member_b, name) + assert str(e.value).endswith(" is different!") + + +def test_apk_challenge_contents_without_signature(args): + # Tempfolder inside chroot for fake apk files + temp_path = pmb.chroot.other.tempfolder( + args, "/tmp/test_apk_challenge_nosig") + temp_path_outside = args.work + "/chroot_native" + temp_path + + # Create three archives + contents = { + "no_sig.apk": ["other_file"], + "one_sig.apk": [".SIGN.RSA.first", "other_file"], + "two_sig.apk": [".SIGN.RSA.first", ".SIGN.RSA.second"], + } + for apk, files in contents.items(): + for file in files: + pmb.chroot.user(args, ["touch", temp_path + "/" + file]) + pmb.chroot.user(args, ["tar", "-czf", apk] + + files, working_dir=temp_path) + + # No signature + with tarfile.open(temp_path_outside + "/no_sig.apk", "r:gz") as tar: + with pytest.raises(RuntimeError) as e: + pmb.challenge.apk_file.contents_without_signature(tar, "a.apk") + assert str(e.value).startswith("No signature file found") + + # One signature + with tarfile.open(temp_path_outside + "/one_sig.apk", "r:gz") as tar: + contents = pmb.challenge.apk_file.contents_without_signature( + tar, "a.apk") + assert contents == ["other_file"] + + # More than one signature + with tarfile.open(temp_path_outside + "/two_sig.apk", "r:gz") as tar: + with pytest.raises(RuntimeError) as e: + pmb.challenge.apk_file.contents_without_signature(tar, "a.apk") + assert str(e.value).startswith("More than one signature") + + +def test_apk_challenge_different_files_inside_archive(args): + # Tempfolder inside chroot for fake apk files + temp_path = pmb.chroot.other.tempfolder(args, "/tmp/test_apk_challenge") + temp_path_outside = args.work + "/chroot_native" + temp_path + + # Create fake apks + contents = { + "a.apk": [".SIGN.RSA.first", "first_file", "second_file"], + "b.apk": [".SIGN.RSA.second", "first_file"], + } + for apk, files in contents.items(): + for file in files: + pmb.chroot.user(args, ["touch", temp_path + "/" + file]) + pmb.chroot.user(args, ["tar", "-czf", apk] + + files, working_dir=temp_path) + + # Challenge both files + with pytest.raises(RuntimeError) as e: + pmb.challenge.apk(args, temp_path_outside + "/a.apk", + temp_path_outside + "/b.apk") + assert "do not contain the same file names" in str(e.value) + + +def test_apk_challenge_entry_has_a_different_type(args): + # Tempfolder inside chroot for fake apk files + temp_path = pmb.chroot.other.tempfolder(args, "/tmp/test_apk_challenge") + temp_path_outside = args.work + "/chroot_native" + temp_path + + # Create fake apks + contents = { + "a.apk": [".SIGN.RSA.first", ".APKINDEX", "different_type"], + "b.apk": [".SIGN.RSA.second", ".APKINDEX", "different_type"], + } + for apk, files in contents.items(): + for file in files: + if file == "different_type" and apk == "b.apk": + pmb.chroot.user(args, ["rm", temp_path + "/" + file]) + pmb.chroot.user(args, ["mkdir", temp_path + "/" + file]) + else: + pmb.chroot.user(args, ["touch", temp_path + "/" + file]) + pmb.chroot.user(args, ["tar", "-czf", apk] + + files, working_dir=temp_path) + + # Exact error (with stop_after_first_error) + with pytest.raises(RuntimeError) as e: + pmb.challenge.apk(args, temp_path_outside + "/a.apk", + temp_path_outside + "/b.apk", stop_after_first_error=True) + assert "has a different type!" in str(e.value) + + # Generic error + with pytest.raises(RuntimeError) as e: + pmb.challenge.apk(args, temp_path_outside + "/a.apk", + temp_path_outside + "/b.apk") + assert "Challenge failed" + + +def test_apk_challenge_file_has_different_content(args): + # Tempfolder inside chroot for fake apk files + temp_path = pmb.chroot.other.tempfolder(args, "/tmp/test_apk_challenge") + temp_path_outside = args.work + "/chroot_native" + temp_path + + # Create fake apks + contents = { + "a.apk": [".SIGN.RSA.first", ".APKINDEX", "different_content"], + "b.apk": [".SIGN.RSA.second", ".APKINDEX", "different_content"], + } + for apk, files in contents.items(): + for file in files: + if file == "different_content" and apk == "b.apk": + pmb.chroot.user( + args, [ + "cp", "/etc/hostname", temp_path + "/" + file]) + else: + pmb.chroot.user(args, ["touch", temp_path + "/" + file]) + pmb.chroot.user(args, ["tar", "-czf", apk] + + files, working_dir=temp_path) + + # Exact error (with stop_after_first_error) + with pytest.raises(RuntimeError) as e: + pmb.challenge.apk(args, temp_path_outside + "/a.apk", + temp_path_outside + "/b.apk", stop_after_first_error=True) + assert str(e.value).endswith("is different!") + + # Generic error + with pytest.raises(RuntimeError) as e: + pmb.challenge.apk(args, temp_path_outside + "/a.apk", + temp_path_outside + "/b.apk") + assert "Challenge failed" + + +def test_apk_challenge_different_link_target(args): + # Tempfolder inside chroot for fake apk files + temp_path = pmb.chroot.other.tempfolder(args, "/tmp/test_apk_challenge") + temp_path_outside = args.work + "/chroot_native" + temp_path + + # Create fake apks + contents = { + "a.apk": [".SIGN.RSA.first", ".APKINDEX", "link_same", "link_different"], + "b.apk": [".SIGN.RSA.second", ".APKINDEX", "link_same", "link_different"], + } + for apk, files in contents.items(): + for file in files: + if file.startswith("link_"): + if file == "link_different" and apk == "b.apk": + pmb.chroot.user(args, ["ln", "-sf", "/different_target", + temp_path + "/" + file]) + else: + pmb.chroot.user(args, ["ln", "-sf", "/some_link_target", + temp_path + "/" + file]) + else: + pmb.chroot.user(args, ["touch", temp_path + "/" + file]) + pmb.chroot.user(args, ["tar", "-czf", apk] + + files, working_dir=temp_path) + + # Exact error (with stop_after_first_error) + with pytest.raises(RuntimeError) as e: + pmb.challenge.apk(args, temp_path_outside + "/a.apk", + temp_path_outside + "/b.apk", stop_after_first_error=True) + assert str(e.value).endswith("has a different target!") + + # Generic error + with pytest.raises(RuntimeError) as e: + pmb.challenge.apk(args, temp_path_outside + "/a.apk", + temp_path_outside + "/b.apk") + assert "Challenge failed" + + +def test_apk_challenge_unsupported_type(args): + # Tempfolder inside chroot for fake apk files + temp_path = pmb.chroot.other.tempfolder(args, "/tmp/test_apk_challenge") + temp_path_outside = args.work + "/chroot_native" + temp_path + + # Create fake apk with a FIFO (-> unsupported type) + apk = "test.apk" + content = [".SIGN.RSA.first", ".APKINDEX", "fifo"] + for file in content: + if file == "fifo": + pmb.chroot.user(args, ["mkfifo", temp_path + "/" + file]) + else: + pmb.chroot.user(args, ["touch", temp_path + "/" + file]) + pmb.chroot.user(args, ["tar", "-czf", apk] + + content, working_dir=temp_path) + + # Exact error (with stop_after_first_error) + with pytest.raises(RuntimeError) as e: + pmb.challenge.apk(args, temp_path_outside + "/test.apk", + temp_path_outside + "/test.apk", stop_after_first_error=True) + assert str(e.value).endswith("unsupported type!") + + # Generic error + with pytest.raises(RuntimeError) as e: + pmb.challenge.apk(args, temp_path_outside + "/test.apk", + temp_path_outside + "/test.apk") + assert "Challenge failed" diff --git a/test/test_challenge_apkindex.py b/test/test_challenge_apkindex.py new file mode 100644 index 00000000..12039352 --- /dev/null +++ b/test/test_challenge_apkindex.py @@ -0,0 +1,87 @@ +""" +Copyright 2017 Oliver Smith + +This file is part of pmbootstrap. + +pmbootstrap 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 3 of the License, or +(at your option) any later version. + +pmbootstrap 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. + +You should have received a copy of the GNU General Public License +along with pmbootstrap. If not, see . +""" +import os +import sys +import pytest + +# Import from parent directory +sys.path.append(os.path.abspath( + os.path.join(os.path.dirname(__file__) + "/.."))) +import pmb.challenge.apkindex +import pmb.config + + +@pytest.fixture +def args(request, tmpdir): + import pmb.parse + sys.argv = ["pmbootstrap.py", "chroot"] + args = pmb.parse.arguments() + setattr(args, "logfd", open("/dev/null", "a+")) + request.addfinalizer(args.logfd.close) + + # Create an empty APKINDEX.tar.gz file, so we can use its path and + # timestamp to put test information in the cache. + path_apkindex = str(tmpdir) + "/APKINDEX.tar.gz" + open(path_apkindex, "a").close() + lastmod = os.path.getmtime(path_apkindex) + args.cache["apkindex"][path_apkindex] = {"lastmod": lastmod, "ret": {}} + + return args + + +def test_challenge_apkindex_extra_file(args): + """ + Create an extra file, that is not mentioned in the APKINDEX cache. + """ + path_apkindex = list(args.cache["apkindex"].keys())[0] + tmpdir = os.path.dirname(path_apkindex) + open(tmpdir + "/invalid-extra-file.apk", "a").close() + + with pytest.raises(RuntimeError) as e: + pmb.challenge.apkindex(args, path_apkindex) + assert "Unexpected file" in str(e.value) + + +def test_challenge_apkindex_file_does_not_exist(args): + """ + Add an entry to the APKINDEX cache, that does not exist on disk. + """ + path_apkindex = list(args.cache["apkindex"].keys())[0] + args.cache["apkindex"][path_apkindex]["ret"] = { + "hello-world": {"pkgname": "hello-world", "version": "1-r2"} + } + + with pytest.raises(RuntimeError) as e: + pmb.challenge.apkindex(args, path_apkindex) + assert str(e.value).startswith("Could not find file 'hello-world") + + +def test_challenge_apkindex_ok(args): + """ + Metion one file in the APKINDEX cache, and create it on disk. The challenge + should go through without an exception. + """ + path_apkindex = list(args.cache["apkindex"].keys())[0] + args.cache["apkindex"][path_apkindex]["ret"] = { + "hello-world": {"pkgname": "hello-world", "version": "1-r2"} + } + tmpdir = os.path.dirname(path_apkindex) + open(tmpdir + "/hello-world-1-r2.apk", "a").close() + + pmb.challenge.apkindex(args, path_apkindex) diff --git a/test/test_challenge_build.py b/test/test_challenge_build.py new file mode 100644 index 00000000..c6d9c136 --- /dev/null +++ b/test/test_challenge_build.py @@ -0,0 +1,67 @@ +""" +Copyright 2017 Oliver Smith + +This file is part of pmbootstrap. + +pmbootstrap 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 3 of the License, or +(at your option) any later version. + +pmbootstrap 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. + +You should have received a copy of the GNU General Public License +along with pmbootstrap. If not, see . +""" +import os +import sys +import pytest + +# Import from parent directory +sys.path.append(os.path.abspath( + os.path.join(os.path.dirname(__file__) + "/.."))) +import pmb.build.package +import pmb.challenge.build +import pmb.parse +import pmb.config + + +@pytest.fixture +def args(request, tmpdir): + import pmb.parse + sys.argv = ["pmbootstrap.py", "chroot"] + args = pmb.parse.arguments() + setattr(args, "logfd", open("/dev/null", "a+")) + request.addfinalizer(args.logfd.close) + return args + + +def test_challenge_build(args): + # Build the "hello-world" package + pkgname = "hello-world" + pmb.build.package(args, pkgname, None, force=True, buildinfo=True) + + # Copy it to a temporary path + apkbuild = pmb.parse.apkbuild(args.aports + "/" + pkgname + "/APKBUILD") + version = apkbuild["pkgver"] + "-r" + apkbuild["pkgrel"] + temp_path = pmb.chroot.other.tempfolder(args, "/tmp/test_challenge_build/" + + args.arch_native) + apk_path = ("/home/user/packages/user/" + args.arch_native + "/" + pkgname + + "-" + version + ".apk") + pmb.chroot.user(args, ["cp", apk_path, apk_path + + ".buildinfo.json", temp_path]) + + # Challenge, output changes into a file + setattr(args, "output_repo_changes", args.work + "/chroot_native/tmp/" + "test_challenge_build_output.txt") + pmb.challenge.build(args, args.work + "/chroot_native/" + temp_path + "/" + + os.path.basename(apk_path)) + + # Verify the output textfile + with open(args.output_repo_changes, "r") as handle: + lines = handle.readlines() + assert lines == [args.arch_native + "/APKINDEX.tar.gz\n", + args.arch_native + "/" + pkgname + "-" + version + ".apk\n"]