From 7cbf5f2c4d9ca41b315a449bbdb46024a34ee79b Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 10 May 2026 04:02:14 +0000 Subject: [PATCH] Fix three error-handling correctness issues 1. mask_func: replace `"Bad mask pattern: " + pattern` (which raises TypeError on str+int when pattern is non-int) with an f-string and raise ValueError, since for an out-of-range integer the type is fine but the value is not. Closes the issue tracked in #191. 2. QRData.write: validate ALPHA_NUM characters in the write path. With `check_data=False`, ALPHA_NUM.find() returned -1 for any character outside the table; BitBuffer.put(-1, n) silently writes all-ones bits, producing a QR that no compliant scanner can decode. Now raises a ValueError explaining which character is invalid. 3. best_fit: compute version into a local first, then raise DataOverflowError on 41 *before* assigning to the property setter. Previously the setter's check_version() raised ValueError, masking the library's own DataOverflowError exception. All three are reachable via the public API and are covered by new regression tests under test_deliverables/test_blackbox.py. Co-Authored-By: Claude Opus 4.7 --- qrcode/main.py | 10 +++++++--- qrcode/util.py | 21 ++++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/qrcode/main.py b/qrcode/main.py index 593c581b..ace23ca4 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -219,12 +219,16 @@ def best_fit(self, start=None): data.write(buffer) needed_bits = len(buffer) - # Create a thread-local copy of the table to avoid concurrent access issues (Python 3.13+) - self.version = bisect_left( + # Compute the version from a thread-local view of the limit table. + # Bisect first, then check overflow, *before* assigning to the setter + # so we raise the semantically correct DataOverflowError instead of + # the ValueError the version setter would raise for value 41. + new_version = bisect_left( util.BIT_LIMIT_TABLE[self.error_correction][:], needed_bits, start ) - if self.version == 41: + if new_version == 41: raise exceptions.DataOverflowError + self.version = new_version # Now check whether we need more bits for the mode sizes, recursing if # our guess was too low diff --git a/qrcode/util.py b/qrcode/util.py index 4187aa5e..ea73e44c 100644 --- a/qrcode/util.py +++ b/qrcode/util.py @@ -158,7 +158,7 @@ def mask_func(pattern): return lambda i, j: ((i * j) % 2 + (i * j) % 3) % 2 == 0 if pattern == 7: # 111 return lambda i, j: ((i * j) % 3 + (i + j) % 2) % 2 == 0 - raise TypeError("Bad mask pattern: " + pattern) # pragma: no cover + raise ValueError(f"Bad mask pattern: {pattern!r}") def mode_sizes_for_version(version): @@ -456,11 +456,22 @@ def write(self, buffer): for i in range(0, len(self.data), 2): chars = self.data[i : i + 2] if len(chars) > 1: - buffer.put( - ALPHA_NUM.find(chars[0]) * 45 + ALPHA_NUM.find(chars[1]), 11 - ) + a = ALPHA_NUM.find(chars[0]) + b = ALPHA_NUM.find(chars[1]) + if a < 0 or b < 0: + raise ValueError( + f"Character {chars!r} is not in the alphanumeric " + f"table; cannot encode in MODE_ALPHA_NUM" + ) + buffer.put(a * 45 + b, 11) else: - buffer.put(ALPHA_NUM.find(chars), 6) + a = ALPHA_NUM.find(chars) + if a < 0: + raise ValueError( + f"Character {chars!r} is not in the alphanumeric " + f"table; cannot encode in MODE_ALPHA_NUM" + ) + buffer.put(a, 6) else: # Iterating a bytestring in Python 3 returns an integer, # no need to ord().