Skip to content

Commit 66861f1

Browse files
miss-islingtonnitmirserhiy-storchaka
authored
[3.13] gh-83869: tarfile: compute next header offset using pax size for sparse file (GH-18562) (GH-155868)
In case of a sparse file, the tarinfo.size attribute is set to the sparse file expanded size (pax attribute GNU.sparse.size or GNU.sparse.size) and do not correspond to the actual size of the data block. The size of the data block is specified by the size pax header if present or by the ustar size header. Moreover, for GNU sparse 1.0 files, the data block start at the beginning of the sparse mapping and not after the sparse mapping and so the offset should be computed from here. (cherry picked from commit 1a52eae) Co-authored-by: Valentin Samir <valentin.samir@crans.org> Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent a01e680 commit 66861f1

3 files changed

Lines changed: 70 additions & 3 deletions

File tree

Lib/tarfile.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,17 +1605,22 @@ def _proc_pax(self, tarfile):
16051605
if self.type in (XHDTYPE, SOLARIS_XHDTYPE):
16061606
# Patch the TarInfo object with the extended header info.
16071607
next._apply_pax_info(pax_headers, tarfile.encoding, tarfile.errors)
1608-
next.offset = self.offset
16091608

16101609
if "size" in pax_headers:
16111610
# If the extended header replaces the size field,
16121611
# we need to recalculate the offset where the next
16131612
# header starts.
1614-
offset = next.offset_data
1613+
offset = next.offset + BLOCKSIZE
16151614
if next.isreg() or next.type not in SUPPORTED_TYPES:
1616-
offset += next._block(next.size)
1615+
try:
1616+
size = PAX_NUMBER_FIELDS["size"](pax_headers["size"])
1617+
except ValueError:
1618+
size = 0
1619+
offset += next._block(size)
16171620
tarfile.offset = offset
16181621

1622+
next.offset = self.offset
1623+
16191624
return next
16201625

16211626
def _proc_gnusparse_00(self, next, raw_headers):

Lib/test/test_tarfile.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,6 +1348,37 @@ def test_sparse_file_01(self):
13481348
def test_sparse_file_10(self):
13491349
self._test_sparse_file("gnu/sparse-1.0")
13501350

1351+
def test_sparse_file_10_pax_size(self):
1352+
# gh-83869: when the pax header replaces the size field, the offset
1353+
# of the next header must be computed from the size of the data in
1354+
# the archive, not from the apparent size of the sparse file.
1355+
data = b"payload!" * 4
1356+
realsize = 1 << 20
1357+
smap = b"1\n%d\n%d\n" % (realsize - len(data), len(data))
1358+
smap += b"\0" * (-len(smap) % tarfile.BLOCKSIZE)
1359+
1360+
sparse = tarfile.TarInfo("sparse")
1361+
sparse.size = len(smap) + len(data)
1362+
sparse.pax_headers = {
1363+
"GNU.sparse.major": "1",
1364+
"GNU.sparse.minor": "0",
1365+
"GNU.sparse.name": "sparse",
1366+
"GNU.sparse.realsize": str(realsize),
1367+
"size": str(sparse.size),
1368+
}
1369+
buf = sparse.tobuf(tarfile.PAX_FORMAT)
1370+
buf += smap + data + b"\0" * (-len(data) % tarfile.BLOCKSIZE)
1371+
1372+
last = tarfile.TarInfo("last")
1373+
last.size = len(data)
1374+
buf += last.tobuf(tarfile.PAX_FORMAT)
1375+
buf += data + b"\0" * (-len(data) % tarfile.BLOCKSIZE)
1376+
buf += b"\0" * (tarfile.BLOCKSIZE * 2)
1377+
1378+
with tarfile.open(fileobj=io.BytesIO(buf)) as tar:
1379+
self.assertEqual(tar.getnames(), ["sparse", "last"])
1380+
self.assertEqual(tar.extractfile("last").read(), data)
1381+
13511382
@staticmethod
13521383
def _fs_supports_holes():
13531384
# Return True if the platform knows the st_blocks stat attribute and
@@ -1400,6 +1431,31 @@ def test_pax_global_headers(self):
14001431
finally:
14011432
tar.close()
14021433

1434+
def test_offset_after_global_header(self):
1435+
# gh-83869: a global header is a member of its own, the member which
1436+
# follows it keeps the offset of its own header.
1437+
rec = b"30 comment=global header here\n"
1438+
glob = tarfile.TarInfo("././@PaxHeader")
1439+
glob.type = tarfile.XGLTYPE
1440+
glob.size = len(rec)
1441+
buf = glob.tobuf(tarfile.USTAR_FORMAT)
1442+
buf += rec + b"\0" * (-len(rec) % tarfile.BLOCKSIZE)
1443+
1444+
member = tarfile.TarInfo("member")
1445+
data = b"hello\n"
1446+
member.size = len(data)
1447+
offset = len(buf)
1448+
buf += member.tobuf(tarfile.USTAR_FORMAT)
1449+
buf += data + b"\0" * (-len(data) % tarfile.BLOCKSIZE)
1450+
buf += b"\0" * (tarfile.BLOCKSIZE * 2)
1451+
1452+
with tarfile.open(fileobj=io.BytesIO(buf)) as tar:
1453+
tarinfo = tar.getmember("member")
1454+
self.assertEqual(tarinfo.offset, offset)
1455+
self.assertEqual(tarinfo.pax_headers.get("comment"),
1456+
"global header here")
1457+
self.assertEqual(tar.extractfile(tarinfo).read(), data)
1458+
14031459
def test_pax_number_fields(self):
14041460
# All following number fields are read from the pax header.
14051461
tar = tarfile.open(tarname, encoding="iso8859-1")
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fix :mod:`tarfile` reading an archive with a GNU sparse 1.0 member whose
2+
size is set in the pax extended header.
3+
The offset of the next header was computed from the offset of the data,
4+
which is already past the sparse map, and from the size of the member,
5+
which can be the apparent size of the sparse file.
6+
All following members were unreachable.

0 commit comments

Comments
 (0)