Bug#1130691: python3-apt: _file_is_same() always returns False, causing packages to re-download every run

Eddy Pronk <[email protected]> Sat, 14 Mar 2026 07:33:40 +0000
Newsgroups gmane.linux.debian.apt.devel
Message-ID <177347362089.5713.2200174753039841625.reportbug__49818.8360173785$1773473728$gmane$org@e3b208d7450c>
Package: python3-apt
Version: 3.0.0
Severity: important
X-Debbugs-Cc: [email protected]

Dear Maintainer,

`apt.package._file_is_same()` always returns `False` even when the cached
`.deb` file is present and correct. This causes every call to
`Version.fetch_binary()` to re-download the package from the network,
bypassing the destination-path cache entirely.

**Root cause**

Two bugs in `apt/package.py`:

Bug 1 — file opened in text mode:

```python
with open(path) as fobj:          # should be open(path, "rb")
```

Bug 2 — order-sensitive hash comparison:

```python
return apt_pkg.Hashes(fobj).hashes == hashes
```

`HashStringList.__eq__` compares element-by-element (list semantics).
`apt_pkg.Hashes(file)` computes all hash types (MD5, SHA1, SHA256, SHA512),
but the package records only store a subset. The lists differ in both order
and length, so `==` always returns `False`.

**Reproduction**

```python
import apt, apt_pkg, tempfile
from apt.package import _file_is_same

cache = apt.Cache(memonly=True)
version = cache["hello"].candidate
with tempfile.TemporaryDirectory() as tmpdir:
    path = version.fetch_binary(tmpdir)
    print(_file_is_same(path, version.size, version._records.hashes))
    # prints: False  (expected: True)
```

**Fix**

```python
def _file_is_same(path: str, size: int, hashes: apt_pkg.HashStringList) -> bool:
    """Return ``True`` if the file is the same."""
    if os.path.exists(path) and os.path.getsize(path) == size:
        with open(path, "rb") as fobj:
            file_set = {str(h) for h in apt_pkg.Hashes(fobj).hashes}
            return {str(h) for h in hashes} <= file_set
    return False
```

A patch is attached.

-- System Information:
Debian Release: 13.3
  APT prefers stable-updates
  APT policy: (500, 'stable-updates'), (500, 'stable-security'), (500, 'stable')
Architecture: amd64 (x86_64)

Kernel: Linux 6.8.0-101-generic (SMP w/8 CPU threads; PREEMPT)
Kernel taint flags: TAINT_PROPRIETARY_MODULE, TAINT_OOT_MODULE
Locale: LANG=C, LC_CTYPE=C.UTF-8 (charmap=UTF-8), LANGUAGE not set
Shell: /bin/sh linked to /usr/bin/dash
Init: unable to detect

Versions of packages python3-apt depends on:
ii  distro-info-data   0.66+deb13u1
ii  libapt-pkg7.0      3.0.3
ii  libc6              2.41-12+deb13u1
ii  libgcc-s1          14.2.0-19
ii  libstdc++6         14.2.0-19
ii  python-apt-common  3.0.0
ii  python3            3.13.5-1

Versions of packages python3-apt recommends:
ii  iso-codes    4.18.0-1
ii  lsb-release  12.1-1

Versions of packages python3-apt suggests:
ii  apt             3.0.3
pn  python-apt-doc  <none>

-- no debconf information
0001-Fix-_file_is_same-to-use-binary-mode-and-subset-hash.patch (application/mbox, 1.4 KB)
From abe8bdaec4f0a0cbcc028eb621c05054aa0e86e8 Mon Sep 17 00:00:00 2001
From: Eddy Pronk <[email protected]>
Date: Sat, 14 Mar 2026 18:09:48 +1100
Subject: [PATCH] Fix _file_is_same() to use binary mode and subset hash
 comparison

apt_pkg.Hashes(file) always computes all hash types (MD5, SHA1, SHA256,
SHA512) while package records may only store a subset. Additionally,
HashStringList.__eq__ compares by position (list semantics), so two lists
with the same hashes in different order compare as not equal.

Fix both issues:
- Open the file in binary mode ("rb") instead of text mode
- Check that the record hashes are a subset of the computed file hashes
  instead of requiring exact equality

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 apt/package.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/apt/package.py b/apt/package.py
index 50ed6d1..1e8db30 100644
--- a/apt/package.py
+++ b/apt/package.py
@@ -54,8 +54,9 @@ __all__ = (
 def _file_is_same(path: str, size: int, hashes: apt_pkg.HashStringList) -> bool:
     """Return ``True`` if the file is the same."""
     if os.path.exists(path) and os.path.getsize(path) == size:
-        with open(path) as fobj:
-            return apt_pkg.Hashes(fobj).hashes == hashes
+        with open(path, "rb") as fobj:
+            file_set = {str(h) for h in apt_pkg.Hashes(fobj).hashes}
+            return {str(h) for h in hashes} <= file_set
     return False
 
 
-- 
2.34.1