Conversation
The FNAME field of the gzip header is meant to hold the name the file is
expected to have after decompression, but gzip and tarfile only stripped
a literal ".gz" suffix. An archive created as "spam.tgz" therefore
recorded "spam.tgz", so decompressors that honor FNAME wrote out a tar
file still named ".tgz".
Match gunzip instead: compare the suffix ignoring case, and replace
".tgz" and ".taz" with ".tar" rather than leaving them in place. This
covers both code paths, GzipFile._write_gzip_header (mode "w:gz") and
tarfile._Stream._init_write_gz (mode "w|gz").
Only the suffix is matched ignoring case; the rest of the name keeps the
case it was given. gunzip behaves the same way: get_suffix() lowercases
only the trailing bytes it compares, and make_ofname() calls strlwr() on
the suffix alone, so "SPAM.TGZ" becomes "SPAM.tar". Both branches are
tested with an upper-case stem.
Deliberately left out, being separate user-visible changes rather than
part of deriving the FNAME field: the remaining suffixes gunzip knows
(".z", "-gz", "-z" and "_z"), and the "python -m gzip -d" CLI, which
still accepts only a literal ".gz" argument. ".tz" is not added because
it is not a gunzip suffix at all, unlike ".taz".
Documentation build overview
13 files changed ·
|
| # and ".taz" into ".tar" instead of just stripping them. | ||
| if fname[-3:].lower() == b'.gz': | ||
| fname = fname[:-3] | ||
| elif fname[-4:].lower() in (b'.tgz', b'.taz'): |
There was a problem hiding this comment.
.taz is for .tar.Z, but Python's gzip module does not support the .Z compression.
The reading side is now inconsistent with what we write:
$ python -m gzip -d spam.tgz
filename doesn't end in .gz: 'spam.tgz'
main() tests arg[-3:] != ".gz", so it rejects both spam.tgz and spam.GZ, which gunzip accepts. Could you fix it here?
.taz names a .tar.Z, and this module does not do compress(1), so mapping it to .tar was wrong. Only .gz and .tgz are handled now. The suffix logic moved into a small helper that both the header writer and main() use, so python -m gzip -d accepts what gunzip accepts and writes the name gunzip would write. It rejected spam.tgz and spam.GZ before, which was the inconsistency Serhiy pointed out. The existing command-line error test asserted the old message text; it now asserts the new one and also covers .taz, which is refused.
|
Both done, thank you — the Dropped it. I had taken it from GNU The command line uses the same logic. The suffix handling moved into one small helper that both
One thing to flag rather than bury:
|
The three names differed only in case, so on a case-insensitive filesystem the second write landed on the first file. The iOS simulator is one, and the job failed there with FileExistsError.
|
The iOS job caught something my machine could not: the three names in the new command-line test differed only in case, so on a case-insensitive filesystem the second write landed on the first file and it failed with |
| """The name :program:`gunzip` would decompress *name* to, or ``None``. | ||
|
|
||
| The suffix is matched ignoring case, and ``.tgz`` becomes ``.tar`` rather | ||
| than being stripped. ``.taz`` is not handled: it means a ``.tar.Z``, and |
There was a problem hiding this comment.
Drop the mention of .taz -- it is irrelevant here.
Docstring are plain text; do not use the RST syntax.
There was a problem hiding this comment.
Done, plain text now and the .taz sentence is gone.
| # writes the name gunzip would write. | ||
| # Distinct stems: the names differ only in case on some platforms, | ||
| # and a case-insensitive filesystem would have them collide. | ||
| for name, expected in (('lower.tgz', 'lower.tar'), |
There was a problem hiding this comment.
You can use the test.support.subTests() decorator.
There was a problem hiding this comment.
Switched all three loops over to it. The metadata one lost its two explicit upper-case rows in the process, it now runs every suffix against both the plain name and the upper-cased one, which covers a bit more than before.
Plain-text docstring without the .taz aside, and support.subTests() in place of the hand-written subTest loops.
Closes #88661.
An archive created as
spam.tgzrecordsspam.tgzas the name to decompress to, sogunzipon it produces a file calledspam.tgz. Measured onmain, readingFLGand the NUL-terminatedFNAMEstraight out of the RFC 1952 header:maintarfile.open("test.tgz", "w:gz")b'test.tgz'b'test.tar'tarfile.open("s.tgz", "w|gz")b's.tgz'b's.tar'tarfile.open("TEST.TGZ", "w:gz")b'TEST.TGZ'b'TEST.tar'tarfile.open("test.tAr.Gz", "w:gz")b'test.tAr.Gz'b'test.tAr'gzip.open("data.tgz", "wb")b'data.tgz'b'data.tar'gzip.open("DATA.GZ", "wb")b'DATA.GZ'b'DATA'Two independent code paths carry the same defect:
GzipFile._write_gzip_header(used bygzip.openand bytarfile'sw:gz) and_Stream._init_write_gz(w|gz). Both are fixed identically.Following @serhiy-storchaka's direction
So the suffix is matched case-insensitively, and
.tgzbecomes.tarrather than being stripped. Matching the case-insensitivity is a wider change than the issue title suggests —gzip.open("DATA.GZ", "wb")now recordsDATAinstead ofDATA.GZ— so I am calling that out rather than letting it ride along.The stem keeps its case and the suffix becomes lowercase
.tar, which is what GNUmake_ofname()does:.tazis included, which the issue does not mention. It sits in the sameifas.tgzinmake_ofname(), so implementing only half of the criterion Serhiy cited seemed worse than implementing it. The stdlib already treats the two alike —mimetypes.py:462has'.tgz': '.tar.gz', '.taz': '.tar.gz'..tzis not included: GNU handles it through the genericknown_suffixes[]stripping, not the.tarspecial case.The logic is duplicated rather than shared.
_Stream.__init__imports onlyzlib;gzipis pulled in lazily and only byTarFile.gzopen, so a shared helper ingzip.pywould add agzipimport to thew|gzpath for four lines. The duplication predates this change; both copies now say the same thing.The comparison slices the original and lowercases only the tail rather than using
fname.lower().endswith(...), becausestr.lower()can change length ('İ') and the slice index would then be wrong.Not in scope, but noticed
python -m gzip -d(Lib/gzip.py:736) doesif arg[-3:] != ".gz": sys.exit(...), so the CLI that describes itself as "act likegunzip" is still case-sensitive and refuses.tgzoutright. That is a different behaviour — it rejects the file rather than misnaming a header field — so I left it alone. Happy to do it as a follow-up if you want the module internally consistent.Verification
Run on a build of
main(3.16.0a0):Reverting only
Lib/gzip.pyandLib/tarfile.pytomainwhile keeping the tests:run=863 failures=26, from two test methods (test_fname,test_metadata_name_suffix) whose subtests cover each suffix and case combination.A mutation lowercasing the stem as well as the suffix —
fname[:-4].lower() + b'.tar'— fails 2 tests, so the case-preserving half is pinned rather than incidental.mainonly; I am not proposing a backport, since the change alters bytes in generated files.Misc/NEWS.d/next/Library/entry added, plus a note under Porting to Python 3.16 rather than under Improved Modules, since the section is explicitly for "changes that may require changes to your code" and byte-for-byte comparisons of generated archives will shift.Thanks to @miikka for laying out the options in the thread, including the case that
GzipFilecannot know whether a.tgzreally is a tarball — which is why this follows the file name likegunzipdoes rather than inspecting content.