Skip to content

Commit 6817e92

Browse files
committed
gh-155419: honor the file object's current position in `hashlib
.file_digest`
1 parent 998b890 commit 6817e92

3 files changed

Lines changed: 49 additions & 29 deletions

File tree

Lib/hashlib.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,8 @@ def file_digest(fileobj, digest, /, *, _bufsize=2**18):
233233

234234
if hasattr(fileobj, "getbuffer"):
235235
# io.BytesIO object, use zero-copy buffer
236-
digestobj.update(fileobj.getbuffer())
236+
with fileobj.getbuffer() as buf:
237+
digestobj.update(buf[fileobj.tell():])
237238
return digestobj
238239

239240
# Only binary files implement readinto().

Lib/test/test_hashlib.py

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import itertools
1313
import logging
1414
import os
15+
import random
1516
import re
1617
import sys
1718
import sysconfig
@@ -102,6 +103,20 @@ def read_vectors(hash_name):
102103
)
103104

104105

106+
def make_hash_objects(*digestmods, buf=b"", **kwargs):
107+
objects = []
108+
for digestmod in digestmods:
109+
try:
110+
if callable(digestmod):
111+
obj = digestmod(buf)
112+
else:
113+
obj = hashlib.new(digestmod, buf, **kwargs)
114+
except ValueError:
115+
continue
116+
objects.append((digestmod, obj))
117+
return objects
118+
119+
105120
class HashLibTestCase(unittest.TestCase):
106121
supported_hash_names = ( 'md5', 'MD5', 'sha1', 'SHA1',
107122
'sha224', 'SHA224', 'sha256', 'SHA256',
@@ -524,7 +539,8 @@ def test_blake2_update_over_4gb(self):
524539
self.assertEqual(h.hexdigest(), "8a268e83dd30528bc0907fa2008c91de8f090a0b6e0e60a5ff0d999d8485526f")
525540

526541
def check(self, name, data, hexdigest, shake=False, **kwargs):
527-
length = len(hexdigest)//2
542+
n = len(data)
543+
length = len(hexdigest) // 2
528544
hexdigest = hexdigest.lower()
529545
constructors = self.constructors_to_test[name]
530546
# 2 is for hashlib.name(...) and hashlib.new(name, ...)
@@ -533,47 +549,47 @@ def check(self, name, data, hexdigest, shake=False, **kwargs):
533549
m = hash_object_constructor(data, **kwargs)
534550
computed = m.hexdigest() if not shake else m.hexdigest(length)
535551
self.assertEqual(
536-
computed, hexdigest,
537-
"Hash algorithm %s constructed using %s returned hexdigest"
538-
" %r for %d byte input data that should have hashed to %r."
539-
% (name, hash_object_constructor,
540-
computed, len(data), hexdigest))
552+
computed, hexdigest,
553+
"Hash algorithm %s constructed using %s returned hexdigest"
554+
" %r for %d byte input data that should have hashed to %r."
555+
% (name, hash_object_constructor, computed, n, hexdigest)
556+
)
541557
computed = m.digest() if not shake else m.digest(length)
542558
digest = bytes.fromhex(hexdigest)
543559
self.assertEqual(computed, digest)
544560
if not shake:
545561
self.assertEqual(len(digest), m.digest_size)
546562

563+
def generate_sub_hexdigest(pos=-1):
564+
if pos < 0:
565+
pos = 0 if n == 0 else random.randrange(0, n)
566+
pos += 1 # ensure pos is at least 1 (allowed to be out of range)
567+
buf = data[pos:]
568+
objects = make_hash_objects(name, *constructors, buf=buf, **kwargs)
569+
hexdigests = {obj.hexdigest() for _, obj in objects}
570+
self.assertEqual(len(hexdigests), 1, f"bad digests: {objects}")
571+
return pos, hexdigests.pop()
572+
547573
if not shake and kwargs.get("key") is None:
548574
# skip shake and blake2 extended parameter tests
549575
self.check_file_digest(name, data, hexdigest)
576+
for pos in sorted({-1, 1, n - 1, n, n + 1}):
577+
with self.subTest(pos=pos):
578+
pos, hexdigest2 = generate_sub_hexdigest(pos=pos)
579+
self.check_file_digest(name, data, hexdigest2, pos)
550580

551-
def check_file_digest(self, name, data, hexdigest):
581+
def check_file_digest(self, name, data, hexdigest, pos=0):
552582
hexdigest = hexdigest.lower()
553-
digests = []
554-
for digest in [name, *self.constructors_to_test[name]]:
555-
try:
556-
if callable(digest):
557-
digest(b"")
558-
else:
559-
hashlib.new(digest)
560-
except ValueError:
561-
# skip, algorithm is blocked by security policy.
562-
continue
563-
digests.append(digest)
564-
583+
digests = make_hash_objects(name, *self.constructors_to_test[name])
565584
with tempfile.TemporaryFile() as f:
566585
f.write(data)
586+
buf = io.BytesIO(data)
567587

568-
for digest in digests:
569-
buf = io.BytesIO(data)
570-
buf.seek(0)
571-
self.assertEqual(
572-
hashlib.file_digest(buf, digest).hexdigest(), hexdigest
573-
)
574-
f.seek(0)
575-
digestobj = hashlib.file_digest(f, digest)
576-
self.assertEqual(digestobj.hexdigest(), hexdigest)
588+
for digest, _ in digests:
589+
for fobj in [buf, f]:
590+
fobj.seek(pos)
591+
digestobj = hashlib.file_digest(fobj, digest)
592+
self.assertEqual(digestobj.hexdigest(), hexdigest)
577593

578594
def check_no_unicode(self, algorithm_name):
579595
# Unicode objects are not allowed as input.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Ensure that :func:`hashlib.file_digest` honors the current position of the
2+
file object when given an :class:`io.BytesIO` object. Patch by Bénédikt
3+
Tran.

0 commit comments

Comments
 (0)