Skip to content

Commit b0e32e2

Browse files
committed
Fix SF bug # 1330039, patch # 1331635 from Lars Gustaebel (tarfile maintainer)
Problem: if two files are assigned the same inode number by the filesystem, the second one will be added as a hardlink to the first, which means that the content will be lost. The patched code checks if the file's st_nlink is greater 1. So only for files that actually have several links pointing to them hardlinks will be created, which is what GNU tar does. Will backport.
1 parent 40563ed commit b0e32e2

File tree

2 files changed

+50
-2
lines changed

2 files changed

+50
-2
lines changed

Lib/tarfile.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1150,7 +1150,8 @@ def gettarinfo(self, name=None, arcname=None, fileobj=None):
11501150
stmd = statres.st_mode
11511151
if stat.S_ISREG(stmd):
11521152
inode = (statres.st_ino, statres.st_dev)
1153-
if inode in self.inodes and not self.dereference:
1153+
if not self.dereference and \
1154+
statres.st_nlink > 1 and inode in self.inodes:
11541155
# Is it a hardlink to an already
11551156
# archived file?
11561157
type = LNKTYPE

Lib/test/test_tarfile.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,53 @@ def test_hardlink(self):
372372
if e.errno == errno.ENOENT:
373373
self.fail("hardlink not extracted properly")
374374

375+
class CreateHardlinkTest(BaseTest):
376+
"""Test the creation of LNKTYPE (hardlink) members in an archive.
377+
In this respect tarfile.py mimics the behaviour of GNU tar: If
378+
a file has a st_nlink > 1, it will be added a REGTYPE member
379+
only the first time.
380+
"""
381+
382+
def setUp(self):
383+
self.tar = tarfile.open(tmpname(), "w")
384+
385+
self.foo = os.path.join(dirname(), "foo")
386+
self.bar = os.path.join(dirname(), "bar")
387+
388+
if os.path.exists(self.foo):
389+
os.remove(self.foo)
390+
if os.path.exists(self.bar):
391+
os.remove(self.bar)
392+
393+
file(self.foo, "w").write("foo")
394+
self.tar.add(self.foo)
395+
396+
def test_add_twice(self):
397+
# If st_nlink == 1 then the same file will be added as
398+
# REGTYPE every time.
399+
tarinfo = self.tar.gettarinfo(self.foo)
400+
self.assertEqual(tarinfo.type, tarfile.REGTYPE,
401+
"add file as regular failed")
402+
403+
def test_add_hardlink(self):
404+
# If st_nlink > 1 then the same file will be added as
405+
# LNKTYPE.
406+
os.link(self.foo, self.bar)
407+
tarinfo = self.tar.gettarinfo(self.foo)
408+
self.assertEqual(tarinfo.type, tarfile.LNKTYPE,
409+
"add file as hardlink failed")
410+
411+
tarinfo = self.tar.gettarinfo(self.bar)
412+
self.assertEqual(tarinfo.type, tarfile.LNKTYPE,
413+
"add file as hardlink failed")
414+
415+
def test_dereference_hardlink(self):
416+
self.tar.dereference = True
417+
os.link(self.foo, self.bar)
418+
tarinfo = self.tar.gettarinfo(self.bar)
419+
self.assertEqual(tarinfo.type, tarfile.REGTYPE,
420+
"dereferencing hardlink failed")
421+
375422

376423
# Gzip TestCases
377424
class ReadTestGzip(ReadTest):
@@ -387,7 +434,6 @@ class ReadAsteriskTestGzip(ReadAsteriskTest):
387434
class ReadStreamAsteriskTestGzip(ReadStreamAsteriskTest):
388435
comp = "gz"
389436

390-
391437
# Filemode test cases
392438

393439
class FileModeTest(unittest.TestCase):
@@ -440,6 +486,7 @@ def test_main():
440486

441487
if hasattr(os, "link"):
442488
tests.append(ExtractHardlinkTest)
489+
tests.append(CreateHardlinkTest)
443490

444491
if gzip:
445492
tests.extend([

0 commit comments

Comments
 (0)