Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Doc/library/gzip.rst
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,13 @@ Once executed the :mod:`!gzip` module keeps the input file(s).
Add a new command line interface with a usage.
By default, when you will execute the CLI, the default compression level is 6.

.. versionchanged:: next

An existing output file is removed and created anew instead of being
opened for writing, so a symbolic link at that name is replaced rather
than followed. Like :program:`gzip`, the output file is created with the
permission bits of the input file.

Command-line options
^^^^^^^^^^^^^^^^^^^^

Expand Down
28 changes: 26 additions & 2 deletions Lib/gzip.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,25 @@ def decompress(data):
break


def _create_output(filename, infile):
# Like "gzip -f": remove an existing file at the output name instead of
# writing through it (it may be a link to another file), and create the
# new file with the permission bits of the input file.
def opener(path, flags):
return os.open(path, flags, mode)

try:
mode = os.fstat(infile.fileno()).st_mode & 0o777
try:
os.unlink(filename)
except FileNotFoundError:
pass
return builtins.open(filename, "xb", opener=opener)
except:
infile.close()
raise


def main():
from argparse import ArgumentParser
parser = ArgumentParser(description=
Expand All @@ -747,6 +766,7 @@ def main():
compresslevel = _COMPRESS_LEVEL_BEST

for arg in args.args:
outfile = None
if args.decompress:
if arg == "-":
f = GzipFile(filename="", mode="rb", fileobj=sys.stdin.buffer)
Expand All @@ -755,22 +775,26 @@ def main():
if arg[-3:] != ".gz":
sys.exit(f"filename doesn't end in .gz: {arg!r}")
f = open(arg, "rb")
g = builtins.open(arg[:-3], "wb")
g = outfile = _create_output(arg[:-3], f)
else:
if arg == "-":
f = sys.stdin.buffer
g = GzipFile(filename="", mode="wb", fileobj=sys.stdout.buffer,
compresslevel=compresslevel)
else:
f = builtins.open(arg, "rb")
g = open(arg + ".gz", "wb", compresslevel=compresslevel)
outfile = _create_output(arg + ".gz", f)
g = GzipFile(fileobj=outfile, mode="wb",
compresslevel=compresslevel)
while True:
chunk = f.read(READ_BUFFER_SIZE)
if not chunk:
break
g.write(chunk)
if g is not sys.stdout.buffer:
g.close()
if outfile is not None:
outfile.close()
if f is not sys.stdin.buffer:
f.close()

Expand Down
77 changes: 77 additions & 0 deletions Lib/test/test_gzip.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import gc
import io
import os
import stat
import struct
import sys
import unittest
Expand Down Expand Up @@ -1196,6 +1197,26 @@ def test_decompress_infile_outfile(self):
self.assertEqual(out, b'')
self.assertEqual(err, b'')

@os_helper.skip_unless_symlink
@create_and_remove_directory(TEMPDIR)
def test_decompress_does_not_follow_outfile_symlink(self):
gzipname = os.path.join(TEMPDIR, 'testgzip.gz')
outname = os.path.join(TEMPDIR, 'testgzip')
target = os.path.join(TEMPDIR, 'target')
with gzip.open(gzipname, mode='wb') as fp:
fp.write(self.data)
with open(target, 'wb') as fp:
fp.write(b'precious')
os.symlink(target, outname)

assert_python_ok('-m', 'gzip', '-d', gzipname)

self.assertFalse(os.path.islink(outname))
with open(outname, 'rb') as fp:
self.assertEqual(fp.read(), self.data)
with open(target, 'rb') as fp:
self.assertEqual(fp.read(), b'precious')

def test_decompress_infile_outfile_error(self):
rc, out, err = assert_python_failure('-m', 'gzip', '-d', 'thisisatest.out')
self.assertEqual(b"filename doesn't end in .gz: 'thisisatest.out'", err.strip())
Expand Down Expand Up @@ -1249,6 +1270,62 @@ def test_compress_infile_outfile(self):
os.remove(gzipname)
self.assertFalse(os.path.exists(gzipname))

@os_helper.skip_unless_symlink
@create_and_remove_directory(TEMPDIR)
def test_compress_does_not_follow_outfile_symlink(self):
local_testgzip = os.path.join(TEMPDIR, 'testgzip')
gzipname = local_testgzip + '.gz'
target = os.path.join(TEMPDIR, 'target')
with open(local_testgzip, 'wb') as fp:
fp.write(self.data)
with open(target, 'wb') as fp:
fp.write(b'precious')
os.symlink(target, gzipname)

assert_python_ok('-m', 'gzip', local_testgzip)

self.assertFalse(os.path.islink(gzipname))
with gzip.open(gzipname, 'rb') as fp:
self.assertEqual(fp.read(), self.data)
with open(target, 'rb') as fp:
self.assertEqual(fp.read(), b'precious')

@unittest.skipUnless(os.name == 'posix', 'requires POSIX file permissions')
@os_helper.skip_unless_working_chmod
@create_and_remove_directory(TEMPDIR)
def test_infile_outfile_permissions(self):
local_testgzip = os.path.join(TEMPDIR, 'testgzip')
gzipname = local_testgzip + '.gz'

def create_old_output(filename):
# An existing output file is replaced, it does not keep its mode.
os_helper.unlink(filename)
with open(filename, 'wb') as fp:
fp.write(b'old content' * 100)
os.chmod(filename, 0o644)

for mode in 0o400, 0o600, 0o640, 0o666:
expected = mode & ~0o022
with self.subTest(mode=oct(mode)), os_helper.temp_umask(0o022):
os_helper.unlink(local_testgzip)
with open(local_testgzip, 'wb') as fp:
fp.write(self.data)
os.chmod(local_testgzip, mode)
create_old_output(gzipname)

assert_python_ok('-m', 'gzip', local_testgzip)
self.assertEqual(stat.S_IMODE(os.stat(gzipname).st_mode),
expected)
with gzip.open(gzipname) as fp:
self.assertEqual(fp.read(), self.data)

create_old_output(local_testgzip)
assert_python_ok('-m', 'gzip', '-d', gzipname)
self.assertEqual(stat.S_IMODE(os.stat(local_testgzip).st_mode),
expected)
with open(local_testgzip, 'rb') as fp:
self.assertEqual(fp.read(), self.data)

def test_compress_fast_best_are_exclusive(self):
rc, out, err = assert_python_failure('-m', 'gzip', '--fast', '--best')
self.assertIn(b"error: argument --best: not allowed with argument --fast", err)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix the :mod:`gzip` command-line interface writing through a symbolic link
at the output file name. Like :program:`gzip`, it now replaces an existing
output file with a new one that has the permission bits of the input file.
Loading