Skip to content

gh-98758: Fix shutil.unpack_archive masking real filesystem errors - #155587

Open
SomSamantray wants to merge 3 commits into
python:mainfrom
SomSamantray:gh-98758-shutil-unpack-archive-readerror
Open

gh-98758: Fix shutil.unpack_archive masking real filesystem errors#155587
SomSamantray wants to merge 3 commits into
python:mainfrom
SomSamantray:gh-98758-shutil-unpack-archive-readerror

Conversation

@SomSamantray

Copy link
Copy Markdown

Fixes gh-98758.

shutil.unpack_archive(filename) with no explicit format resolves the format purely by pattern-matching filename's extension via _find_unpack_format(), which never touches the filesystem. When no extension matches — including the common case of a nonexistent or permission-denied file — it raises a generic shutil.ReadError("Unknown archive format '<filename>'"), masking the real cause:

>>> shutil.unpack_archive("./enoent")
shutil.ReadError: Unknown archive format './enoent'

open() correctly raises FileNotFoundError/PermissionError for the same inputs; this brings unpack_archive in line with that.

Fix

When _find_unpack_format() returns None:

  1. os.stat(filename) first — surfaces FileNotFoundError (and other stat-level errors) for a missing path.
  2. Only for regular files, briefly open(filename, 'rb') and close it (contents never read) — surfaces PermissionError for a file that exists but isn't readable.
  3. If both succeed, ReadError is still raised — the extension genuinely doesn't match any registered format.

The open() probe is skipped for non-regular files (directories, FIFOs, etc.) so:

  • a directory still gets the pre-existing ReadError (not IsADirectoryError)
  • a FIFO doesn't risk hanging forever on a blocking read-open with no writer

The explicit format=... argument path is completely untouched.

Testing

  • Updated the existing check_unpack_archive_with_converter test helper: a nonexistent path now correctly expects FileNotFoundError instead of the old ReadError (this assertion previously encoded the bug).
  • Added tests for: an accessible file with a genuinely unrecognized extension (still ReadError), permission-denied (PermissionError, skipped under root/unsupported-chmod platforms), an unrecognized-extension directory (still ReadError), and an unrecognized-extension FIFO (still ReadError, and confirmed it doesn't hang).
  • Full test_shutil suite passes locally (237 tests).

unpack_archive() raised a generic ReadError("Unknown archive
format") whenever the filename's extension didn't match a
registered format -- including when the real problem was that the
file didn't exist or wasn't readable, masking FileNotFoundError/
PermissionError the way open() would correctly raise them.

When _find_unpack_format() finds no match, stat the path first
(surfaces FileNotFoundError et al.) and, only for regular files,
briefly open() it (surfaces PermissionError) before falling back to
ReadError. Skipping the open() probe for non-regular files avoids
a hang on FIFOs (a blocking read-open with no writer) and preserves
the existing ReadError behavior for directories.
SomSamantray and others added 2 commits August 12, 2026 00:32
shutil.ReadError has no documented exception entry in the docs
(only tarfile.ReadError does), so Sphinx's py:exc cross-reference
role can't resolve it, failing the Docs CI check. Use plain code
markup instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

shutil.unpack_archive returns bogus ReadError exceptions when input is not found or inaccessible

1 participant