diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 3d3125ad17b126b..3d514b7bd316404 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -1045,6 +1045,16 @@ that may require changes to your code. raises :exc:`io.UnsupportedOperation` unless buffering is disabled. (Contributed by An Long in :gh:`86768`.) +* :mod:`gzip` and :mod:`tarfile` now derive the ``FNAME`` field of the gzip + header from the file name like :program:`gunzip` does: the suffix is matched + ignoring case, and ``.tgz`` is replaced with ``.tar`` instead of being left + in place. For example, an archive created as :file:`spam.tgz` now records + ``spam.tar`` rather than ``spam.tgz``, and :file:`spam.GZ` records ``spam`` + rather than ``spam.GZ``. Code comparing generated files byte for byte may + need to be updated. ``python -m gzip -d`` accepts the same names, where it + previously refused anything not ending in a lowercase ``.gz``. + (Contributed by Dmitry Voropaev in :gh:`88661`.) + Build changes ============= diff --git a/Lib/gzip.py b/Lib/gzip.py index 247efc01527bb14..4853ace32167aa7 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -145,6 +145,25 @@ def writable(self): return True +def _gunzip_name(name): + """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 + this module does not do :program:`compress`. Accepts and returns either + :class:`str` or :class:`bytes`. + """ + if isinstance(name, bytes): + gz, tgz, tar = b'.gz', b'.tgz', b'.tar' + else: + gz, tgz, tar = '.gz', '.tgz', '.tar' + if name[-3:].lower() == gz: + return name[:-3] + if name[-4:].lower() == tgz: + return name[:-4] + tar + return None + + class GzipFile(_streams.BaseStream): """The GzipFile class simulates most of the methods of a file object with the exception of the truncate() method. @@ -288,8 +307,9 @@ def _write_gzip_header(self, compresslevel): fname = os.path.basename(self.name) if not isinstance(fname, bytes): fname = fname.encode('latin-1') - if fname.endswith(b'.gz'): - fname = fname[:-3] + stripped = _gunzip_name(fname) + if stripped is not None: + fname = stripped except UnicodeEncodeError: fname = b'' flags = 0 @@ -729,10 +749,11 @@ def main(): f = GzipFile(filename="", mode="rb", fileobj=sys.stdin.buffer) g = sys.stdout.buffer else: - if arg[-3:] != ".gz": - sys.exit(f"filename doesn't end in .gz: {arg!r}") + out = _gunzip_name(arg) + if out is None: + sys.exit(f"filename doesn't end in .gz or .tgz: {arg!r}") f = open(arg, "rb") - g = builtins.open(arg[:-3], "wb") + g = builtins.open(out, "wb") else: if arg == "-": f = sys.stdin.buffer diff --git a/Lib/tarfile.py b/Lib/tarfile.py index a4f9ce3311f6dad..f908d23cc6d9ab9 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -456,8 +456,13 @@ def _init_write_gz(self, compresslevel, mtime): mtime = int(time.time()) timestamp = struct.pack("