From a16d979af6fce99176a436c80ef1483fa51978f6 Mon Sep 17 00:00:00 2001 From: Johannes Schauer Marin Rodrigues Date: Fri, 1 May 2026 20:25:25 +0200 Subject: [PATCH 1/6] src/bmaptool/CLI.py: add __iter__ to NamedFile() Otherwise, when attempting to read an invalid bmapfile, the exception handler throws an exception itself: Traceback (most recent call last): File "/usr/lib/python3/dist-packages/bmaptool/BmapCopy.py", line 400, in _parse_bmap self._xml = ElementTree.parse(self._f_bmap) ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^ File "/usr/lib/python3.13/xml/etree/ElementTree.py", line 1210, in parse tree.parse(source, parser) ~~~~~~~~~~^^^^^^^^^^^^^^^^ File "/usr/lib/python3.13/xml/etree/ElementTree.py", line 573, in parse self._root = parser._parse_whole(source) ~~~~~~~~~~~~~~~~~~~^^^^^^^^ xml.etree.ElementTree.ParseError: mismatched tag: line 106, column 2 During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/usr/bin/bmaptool", line 8, in sys.exit(main()) ~~~~^^ File "/usr/lib/python3/dist-packages/bmaptool/CLI.py", line 947, in main args.func(args) ~~~~~~~~~^^^^^^ File "/usr/lib/python3/dist-packages/bmaptool/CLI.py", line 637, in copy_command writer = BmapCopy.BmapCopy(image_obj, dest_obj, bmap_obj, image_size) File "/usr/lib/python3/dist-packages/bmaptool/BmapCopy.py", line 286, in __init__ self._parse_bmap() ~~~~~~~~~~~~~~~~^^ File "/usr/lib/python3/dist-packages/bmaptool/BmapCopy.py", line 405, in _parse_bmap for num, line in enumerate(self._f_bmap): ~~~~~~~~~^^^^^^^^^^^^^^ TypeError: 'NamedFile' object is not iterable --- src/bmaptool/CLI.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/bmaptool/CLI.py b/src/bmaptool/CLI.py index 40acf31..729a81d 100644 --- a/src/bmaptool/CLI.py +++ b/src/bmaptool/CLI.py @@ -100,6 +100,9 @@ def __init__(self, file_obj, name): self._file_obj = file_obj self.name = name + def __iter__(self): + return iter(self._file_obj) + def __getattr__(self, name): return getattr(self._file_obj, name) From 0d9fd9416d30a8be3a56beaf45567d4db3ab00f7 Mon Sep 17 00:00:00 2001 From: Johannes Schauer Marin Rodrigues Date: Fri, 1 May 2026 20:29:54 +0200 Subject: [PATCH 2/6] src/bmaptool/BmapCopy.py: stop processing after error position was reached On very large input, this stalls for a long time unnecessarily. Since this is handling input which is invalid XML it is likely something completely different, including multi gigabyte binary files. --- src/bmaptool/BmapCopy.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/bmaptool/BmapCopy.py b/src/bmaptool/BmapCopy.py index f8f0a2f..18298e4 100644 --- a/src/bmaptool/BmapCopy.py +++ b/src/bmaptool/BmapCopy.py @@ -403,8 +403,11 @@ def _parse_bmap(self): self._f_bmap.seek(0) xml_extract = "" for num, line in enumerate(self._f_bmap): - if num >= err.position[0] - 4 and num <= err.position[0] + 4: - xml_extract += "Line %d: %s" % (num, line) + if num < err.position[0] - 4: + continue + if num > err.position[0] + 4: + break + xml_extract += "Line %d: %s" % (num, line) raise Error( "cannot parse the bmap file '%s' which should be a " From 085f0b4d8b535a0be628699f4c09a5213e16864f Mon Sep 17 00:00:00 2001 From: Johannes Schauer Marin Rodrigues Date: Fri, 1 May 2026 20:35:28 +0200 Subject: [PATCH 3/6] src/bmaptool/BmapCopy.py: decode binary string Without decoding, the line remains binary type which means that it is printed to the user as b"..." and newlines appear as \n instead of as real line breaks. --- src/bmaptool/BmapCopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bmaptool/BmapCopy.py b/src/bmaptool/BmapCopy.py index 18298e4..fedad84 100644 --- a/src/bmaptool/BmapCopy.py +++ b/src/bmaptool/BmapCopy.py @@ -407,7 +407,7 @@ def _parse_bmap(self): continue if num > err.position[0] + 4: break - xml_extract += "Line %d: %s" % (num, line) + xml_extract += "Line %d: %s" % (num, line.decode(errors="backslashreplace")) raise Error( "cannot parse the bmap file '%s' which should be a " From c90a30806dfba3c0b8d28ad1b518ba75c73cf69e Mon Sep 17 00:00:00 2001 From: Johannes Schauer Marin Rodrigues Date: Fri, 1 May 2026 23:30:12 +0200 Subject: [PATCH 4/6] src/bmaptool/TransRead.py: error out if subprocess exited with non-zero Otherwise, this will finish successfully even though it wrote out nothing: $ echo foobar > test.gz $ bmaptool copy --nobmap test.gz out.img bmaptool: info: no bmap given, copy entire image to 'out.img' gzip: stdin: not in gzip format bmaptool: info: synchronizing 'out.img' bmaptool: info: copying time: 0.0s, copying speed 0 bytes/sec $ stat -c %s out.img 0 --- src/bmaptool/TransRead.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/bmaptool/TransRead.py b/src/bmaptool/TransRead.py index c35e033..b32e3cd 100644 --- a/src/bmaptool/TransRead.py +++ b/src/bmaptool/TransRead.py @@ -222,9 +222,13 @@ def __del__(self): if getattr(self, "_child_processes"): for child in self._child_processes: - if child.poll() is None: + returncode = child.poll() + if returncode is None: + # process is still running child.kill() - child.wait() + returncode = child.wait() + if returncode != 0: + raise Error(f"non-zero exit of {child.args}: {returncode}") self._child_processes = [] def _read_thread(self, f_from, f_to): From 746e41d2572b8e2321279097aef637e86bad3766 Mon Sep 17 00:00:00 2001 From: Johannes Schauer Marin Rodrigues Date: Sat, 2 May 2026 00:37:50 +0200 Subject: [PATCH 5/6] src/bmaptool/CLI.py: manually extract plaintext if gpg is not available Otherwise, even with --no-sig-verify, the user will get: bmaptool: ERROR: Cannot verify GPG signature without GPG --- src/bmaptool/CLI.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/bmaptool/CLI.py b/src/bmaptool/CLI.py index 729a81d..d0a163b 100644 --- a/src/bmaptool/CLI.py +++ b/src/bmaptool/CLI.py @@ -365,6 +365,35 @@ def _add_ext(p, ext): have_method.add("gpgv") if not have_method: + # if no method is available: that is okay i --no-sig-verify was passed + if args.no_sig_verify: + # if signature was detached, there is no cleartext to return + if detached_sig: + return None + # Since no gpg is available and no signature verification is + # required, strip off the signature from the plaintext manually. + # Since mmap is used later, we cannot use io.BytesIO() but need a + # real file. + try: + tmp_obj = tempfile.TemporaryFile("w+b") + except IOError as err: + error_out("cannot create a temporary file for bmap:\n%s", err) + + header_done = False + for line in bmap_obj.readlines(): + if not header_done: + # header is done after the first empty line + if line == b"\n": + header_done = True + continue + if line == b"-----BEGIN PGP SIGNATURE-----\n": + break + if line.endswith(b"-----BEGIN PGP SIGNATURE-----\n"): + line.removeprefix(b"- ") + tmp_obj.write(line) + + tmp_obj.seek(0) + return tmp_obj error_out("Cannot verify GPG signature without GPG") for method in ["gpgme", "gpgv", "gpg"]: From 67695cfe04ea1414faf7cb3f74dae2b6a694217a Mon Sep 17 00:00:00 2001 From: Johannes Schauer Marin Rodrigues Date: Sat, 2 May 2026 01:10:27 +0200 Subject: [PATCH 6/6] black --- src/bmaptool/BmapCopy.py | 7 +++++-- src/bmaptool/BmapHelpers.py | 4 ++-- src/bmaptool/CLI.py | 4 ++-- tests/oldcodebase/BmapCopy1_0.py | 2 +- tests/oldcodebase/BmapCopy2_0.py | 2 +- tests/oldcodebase/BmapCopy2_1.py | 2 +- tests/oldcodebase/BmapCopy2_2.py | 2 +- tests/oldcodebase/BmapCopy2_3.py | 2 +- tests/oldcodebase/BmapCopy2_4.py | 2 +- tests/oldcodebase/BmapCopy2_5.py | 2 +- tests/oldcodebase/BmapCopy2_6.py | 2 +- tests/oldcodebase/BmapCopy3_0.py | 2 +- tests/test_bmap_helpers.py | 1 - 13 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/bmaptool/BmapCopy.py b/src/bmaptool/BmapCopy.py index fedad84..4beba3f 100644 --- a/src/bmaptool/BmapCopy.py +++ b/src/bmaptool/BmapCopy.py @@ -407,7 +407,10 @@ def _parse_bmap(self): continue if num > err.position[0] + 4: break - xml_extract += "Line %d: %s" % (num, line.decode(errors="backslashreplace")) + xml_extract += "Line %d: %s" % ( + num, + line.decode(errors="backslashreplace"), + ) raise Error( "cannot parse the bmap file '%s' which should be a " @@ -718,7 +721,7 @@ def copy(self, sync=True, verify=True): exc_info = batch[1] raise exc_info[1] - (start, end, buf) = batch[1:4] + start, end, buf = batch[1:4] assert len(buf) <= (end - start + 1) * self.block_size assert len(buf) > (end - start) * self.block_size diff --git a/src/bmaptool/BmapHelpers.py b/src/bmaptool/BmapHelpers.py index 8a3ccb4..76c76ce 100644 --- a/src/bmaptool/BmapHelpers.py +++ b/src/bmaptool/BmapHelpers.py @@ -52,8 +52,8 @@ def human_size(size): def human_time(seconds): """Transform time in seconds to the HH:MM:SS format.""" - (minutes, seconds) = divmod(seconds, 60) - (hours, minutes) = divmod(minutes, 60) + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) result = "" if hours: diff --git a/src/bmaptool/CLI.py b/src/bmaptool/CLI.py index d0a163b..525192e 100644 --- a/src/bmaptool/CLI.py +++ b/src/bmaptool/CLI.py @@ -218,7 +218,7 @@ def verify_bmap_signature_gpgbin(bmap_obj, detached_sig, gpgargv, keyring): stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) - (output, error) = sp.communicate() + output, error = sp.communicate() if sp.returncode > 0: if error.find(b"[GNUPG:] NO_PUBKEY "): error_out("No matching key found") @@ -542,7 +542,7 @@ def open_files(args): # Open the bmap file. Try to discover the bmap file automatically if it # was not specified. - (bmap_obj, bmap_path) = find_and_open_bmap(args, image_obj.is_url) + bmap_obj, bmap_path = find_and_open_bmap(args, image_obj.is_url) if bmap_path == args.image: # Most probably the user specified the bmap file instead of the image diff --git a/tests/oldcodebase/BmapCopy1_0.py b/tests/oldcodebase/BmapCopy1_0.py index d381128..632f39a 100644 --- a/tests/oldcodebase/BmapCopy1_0.py +++ b/tests/oldcodebase/BmapCopy1_0.py @@ -501,7 +501,7 @@ def copy(self, sync=True, verify=True): exc_info = batch[1] raise exc_info[1].with_traceback(exc_info[2]) - (start, end, buf) = batch[1:4] + start, end, buf = batch[1:4] assert len(buf) <= (end - start + 1) * self.block_size assert len(buf) > (end - start) * self.block_size diff --git a/tests/oldcodebase/BmapCopy2_0.py b/tests/oldcodebase/BmapCopy2_0.py index bfd085e..0c3aac4 100644 --- a/tests/oldcodebase/BmapCopy2_0.py +++ b/tests/oldcodebase/BmapCopy2_0.py @@ -450,7 +450,7 @@ def copy(self, sync=True, verify=True): exc_info = batch[1] raise exc_info[1].with_traceback(exc_info[2]) - (start, end, buf) = batch[1:4] + start, end, buf = batch[1:4] assert len(buf) <= (end - start + 1) * self.block_size assert len(buf) > (end - start) * self.block_size diff --git a/tests/oldcodebase/BmapCopy2_1.py b/tests/oldcodebase/BmapCopy2_1.py index 77c6a2e..e274ffb 100644 --- a/tests/oldcodebase/BmapCopy2_1.py +++ b/tests/oldcodebase/BmapCopy2_1.py @@ -449,7 +449,7 @@ def copy(self, sync=True, verify=True): exc_info = batch[1] raise exc_info[1].with_traceback(exc_info[2]) - (start, end, buf) = batch[1:4] + start, end, buf = batch[1:4] assert len(buf) <= (end - start + 1) * self.block_size assert len(buf) > (end - start) * self.block_size diff --git a/tests/oldcodebase/BmapCopy2_2.py b/tests/oldcodebase/BmapCopy2_2.py index 89235f2..7449e94 100644 --- a/tests/oldcodebase/BmapCopy2_2.py +++ b/tests/oldcodebase/BmapCopy2_2.py @@ -450,7 +450,7 @@ def copy(self, sync=True, verify=True): exc_info = batch[1] raise exc_info[1].with_traceback(exc_info[2]) - (start, end, buf) = batch[1:4] + start, end, buf = batch[1:4] assert len(buf) <= (end - start + 1) * self.block_size assert len(buf) > (end - start) * self.block_size diff --git a/tests/oldcodebase/BmapCopy2_3.py b/tests/oldcodebase/BmapCopy2_3.py index 21097e2..faa31f2 100644 --- a/tests/oldcodebase/BmapCopy2_3.py +++ b/tests/oldcodebase/BmapCopy2_3.py @@ -486,7 +486,7 @@ def copy(self, sync=True, verify=True): exc_info = batch[1] raise exc_info[1].with_traceback(exc_info[2]) - (start, end, buf) = batch[1:4] + start, end, buf = batch[1:4] assert len(buf) <= (end - start + 1) * self.block_size assert len(buf) > (end - start) * self.block_size diff --git a/tests/oldcodebase/BmapCopy2_4.py b/tests/oldcodebase/BmapCopy2_4.py index 21097e2..faa31f2 100644 --- a/tests/oldcodebase/BmapCopy2_4.py +++ b/tests/oldcodebase/BmapCopy2_4.py @@ -486,7 +486,7 @@ def copy(self, sync=True, verify=True): exc_info = batch[1] raise exc_info[1].with_traceback(exc_info[2]) - (start, end, buf) = batch[1:4] + start, end, buf = batch[1:4] assert len(buf) <= (end - start + 1) * self.block_size assert len(buf) > (end - start) * self.block_size diff --git a/tests/oldcodebase/BmapCopy2_5.py b/tests/oldcodebase/BmapCopy2_5.py index 37b5f48..ab6f154 100644 --- a/tests/oldcodebase/BmapCopy2_5.py +++ b/tests/oldcodebase/BmapCopy2_5.py @@ -526,7 +526,7 @@ def copy(self, sync=True, verify=True): exc_info = batch[1] raise exc_info[1].with_traceback(exc_info[2]) - (start, end, buf) = batch[1:4] + start, end, buf = batch[1:4] assert len(buf) <= (end - start + 1) * self.block_size assert len(buf) > (end - start) * self.block_size diff --git a/tests/oldcodebase/BmapCopy2_6.py b/tests/oldcodebase/BmapCopy2_6.py index 9bbdc4d..cdd4e2c 100644 --- a/tests/oldcodebase/BmapCopy2_6.py +++ b/tests/oldcodebase/BmapCopy2_6.py @@ -526,7 +526,7 @@ def copy(self, sync=True, verify=True): exc_info = batch[1] raise exc_info[1].with_traceback(exc_info[2]) - (start, end, buf) = batch[1:4] + start, end, buf = batch[1:4] assert len(buf) <= (end - start + 1) * self.block_size assert len(buf) > (end - start) * self.block_size diff --git a/tests/oldcodebase/BmapCopy3_0.py b/tests/oldcodebase/BmapCopy3_0.py index 81eed59..898e0d3 100644 --- a/tests/oldcodebase/BmapCopy3_0.py +++ b/tests/oldcodebase/BmapCopy3_0.py @@ -575,7 +575,7 @@ def copy(self, sync=True, verify=True): exc_info = batch[1] raise exc_info[1].with_traceback(exc_info[2]) - (start, end, buf) = batch[1:4] + start, end, buf = batch[1:4] assert len(buf) <= (end - start + 1) * self.block_size assert len(buf) > (end - start) * self.block_size diff --git a/tests/test_bmap_helpers.py b/tests/test_bmap_helpers.py index 2cefad6..1ec90ea 100644 --- a/tests/test_bmap_helpers.py +++ b/tests/test_bmap_helpers.py @@ -32,7 +32,6 @@ from backports.tempfile import TemporaryDirectory from bmaptool import BmapHelpers - # This is a work-around for Centos 6 try: import unittest2 as unittest # pylint: disable=F0401