mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-08 00:54:38 +02:00
65 lines
3.5 KiB
Python
65 lines
3.5 KiB
Python
"""Retain exact tested bytes, internal symlinks, and modes outside owned scratch."""
|
|
import hashlib
|
|
from pathlib import Path, PurePosixPath
|
|
import tarfile
|
|
from source_guard import require, sha
|
|
|
|
def archive_relative(name):
|
|
path = PurePosixPath(name)
|
|
require(name.startswith('provider-pack/') and not path.is_absolute()
|
|
and '..' not in path.parts and path.as_posix() == name, 'Unsafe archive member path')
|
|
return name.removeprefix('provider-pack/')
|
|
|
|
def verify_archive(output, inventory):
|
|
expected = {row['path']: row for row in inventory}
|
|
require(len(expected) == len(inventory), 'Duplicate inventory member')
|
|
observed = set()
|
|
regular_members = set()
|
|
archived_groups = {}
|
|
with tarfile.open(output, 'r:gz') as archive:
|
|
for member in archive:
|
|
if member.name == 'provider-pack':
|
|
require(member.isdir(), 'Archive root must be directory')
|
|
continue
|
|
relative = archive_relative(member.name)
|
|
require(relative in expected and relative not in observed, 'Unexpected archive member')
|
|
row = expected[relative]; observed.add(relative)
|
|
require(member.mode == row['mode'], 'Archive mode mismatch')
|
|
if row['kind'] == 'directory':
|
|
require(member.isdir(), 'Archive directory mismatch')
|
|
elif row['kind'] == 'symlink':
|
|
require(member.issym() and member.linkname == row['target'], 'Archive symlink mismatch')
|
|
else:
|
|
require(row['kind'] == 'file', 'Unknown inventory member kind')
|
|
if member.islnk():
|
|
target = archive_relative(member.linkname)
|
|
require(target in regular_members, 'Archive hard link target must be an earlier regular file')
|
|
target_row = expected[target]
|
|
require(row.get('links', 1) > 1 and member.size == 0
|
|
and row.get('hardlinkGroup') == target_row.get('hardlinkGroup')
|
|
and all(row[key] == target_row[key] for key in ('sha256', 'size', 'mode', 'links')),
|
|
'Archive hard link identity mismatch')
|
|
else:
|
|
require(member.isfile() and member.size == row['size'], 'Archive file mismatch')
|
|
if row.get('links', 1) > 1:
|
|
group = row['hardlinkGroup']
|
|
require(group not in archived_groups, 'Archive lost hard link identity')
|
|
archived_groups[group] = relative
|
|
regular_members.add(relative)
|
|
digest = hashlib.sha256()
|
|
size = 0
|
|
with archive.extractfile(member) as stream:
|
|
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
|
|
digest.update(chunk); size += len(chunk)
|
|
require(size == row['size'] and digest.hexdigest() == row['sha256'], 'Archive bytes mismatch')
|
|
require(observed == set(expected), 'Archive omitted pack members')
|
|
|
|
def retain_pack(pack, output, inventory):
|
|
pack, output = Path(pack), Path(output)
|
|
require(not output.exists() and not output.resolve().is_relative_to(pack.resolve()), 'Archive must be new and outside pack')
|
|
with tarfile.open(output, 'w:gz', compresslevel=1, dereference=False) as archive:
|
|
archive.add(pack, arcname='provider-pack')
|
|
verify_archive(output, inventory)
|
|
return {'path':str(output), 'sha256':sha(output), 'bytes':output.stat().st_size,
|
|
'inventoryVerified':True, 'internalSymlinksAndModesPreserved':True}
|