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
1 change: 1 addition & 0 deletions Lib/dbm/dumb.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ def reorganize(self):
reorganize_pos += blocks_occupied * _BLOCKSIZE

f.truncate(reorganize_pos)
self._modified = True
# Commit changes to index, which were not in-place.
self._commit()

Expand Down
11 changes: 8 additions & 3 deletions Lib/tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -1613,17 +1613,22 @@ def _proc_pax(self, tarfile):
if self.type in (XHDTYPE, SOLARIS_XHDTYPE):
# Patch the TarInfo object with the extended header info.
next._apply_pax_info(pax_headers, tarfile.encoding, tarfile.errors)
next.offset = self.offset

if "size" in pax_headers:
# If the extended header replaces the size field,
# we need to recalculate the offset where the next
# header starts.
offset = next.offset_data
offset = next.offset + BLOCKSIZE
if next.isreg() or next.type not in SUPPORTED_TYPES:
offset += next._block(next.size)
try:
size = PAX_NUMBER_FIELDS["size"](pax_headers["size"])
except ValueError:
size = 0
offset += next._block(size)
tarfile.offset = offset

next.offset = self.offset

return next

def _proc_gnusparse_00(self, next, raw_headers):
Expand Down
10 changes: 10 additions & 0 deletions Lib/test/test_dbm_dumb.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ def test_write_write_read(self):
with contextlib.closing(dumbdbm.open(_fname)) as f:
self.assertEqual(f[b'1'], b'hello2')

def test_reorganize_persists_changed_offsets(self):
with dumbdbm.open(_fname, 'n') as f:
f[b'deleted'] = b'x'
f[b'retained'] = b'value'
del f[b'deleted']
f.reorganize()

with dumbdbm.open(_fname, 'r') as f:
self.assertEqual(f[b'retained'], b'value')

def test_str_read(self):
self.init_db()
with contextlib.closing(dumbdbm.open(_fname, 'r')) as f:
Expand Down
56 changes: 56 additions & 0 deletions Lib/test/test_tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,37 @@ def test_sparse_file_01(self):
def test_sparse_file_10(self):
self._test_sparse_file("gnu/sparse-1.0")

def test_sparse_file_10_pax_size(self):
# gh-83869: when the pax header replaces the size field, the offset
# of the next header must be computed from the size of the data in
# the archive, not from the apparent size of the sparse file.
data = b"payload!" * 4
realsize = 1 << 20
smap = b"1\n%d\n%d\n" % (realsize - len(data), len(data))
smap += b"\0" * (-len(smap) % tarfile.BLOCKSIZE)

sparse = tarfile.TarInfo("sparse")
sparse.size = len(smap) + len(data)
sparse.pax_headers = {
"GNU.sparse.major": "1",
"GNU.sparse.minor": "0",
"GNU.sparse.name": "sparse",
"GNU.sparse.realsize": str(realsize),
"size": str(sparse.size),
}
buf = sparse.tobuf(tarfile.PAX_FORMAT)
buf += smap + data + b"\0" * (-len(data) % tarfile.BLOCKSIZE)

last = tarfile.TarInfo("last")
last.size = len(data)
buf += last.tobuf(tarfile.PAX_FORMAT)
buf += data + b"\0" * (-len(data) % tarfile.BLOCKSIZE)
buf += b"\0" * (tarfile.BLOCKSIZE * 2)

with tarfile.open(fileobj=io.BytesIO(buf)) as tar:
self.assertEqual(tar.getnames(), ["sparse", "last"])
self.assertEqual(tar.extractfile("last").read(), data)

@staticmethod
def _fs_supports_holes():
# Return True if the platform knows the st_blocks stat attribute and
Expand Down Expand Up @@ -1473,6 +1504,31 @@ def test_pax_global_headers(self):
finally:
tar.close()

def test_offset_after_global_header(self):
# gh-83869: a global header is a member of its own, the member which
# follows it keeps the offset of its own header.
rec = b"30 comment=global header here\n"
glob = tarfile.TarInfo("././@PaxHeader")
glob.type = tarfile.XGLTYPE
glob.size = len(rec)
buf = glob.tobuf(tarfile.USTAR_FORMAT)
buf += rec + b"\0" * (-len(rec) % tarfile.BLOCKSIZE)

member = tarfile.TarInfo("member")
data = b"hello\n"
member.size = len(data)
offset = len(buf)
buf += member.tobuf(tarfile.USTAR_FORMAT)
buf += data + b"\0" * (-len(data) % tarfile.BLOCKSIZE)
buf += b"\0" * (tarfile.BLOCKSIZE * 2)

with tarfile.open(fileobj=io.BytesIO(buf)) as tar:
tarinfo = tar.getmember("member")
self.assertEqual(tarinfo.offset, offset)
self.assertEqual(tarinfo.pax_headers.get("comment"),
"global header here")
self.assertEqual(tar.extractfile(tarinfo).read(), data)

def test_pax_number_fields(self):
# All following number fields are read from the pax header.
tar = tarfile.open(tarname, encoding="iso8859-1")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fix :mod:`tarfile` reading an archive with a GNU sparse 1.0 member whose
size is set in the pax extended header.
The offset of the next header was computed from the offset of the data,
which is already past the sparse map, and from the size of the member,
which can be the apparent size of the sparse file.
All following members were unreachable.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :meth:`!reorganize` in :mod:`dbm.dumb` failing to persist updated value
offsets, which could cause data loss after reopening the database.
Loading