Commit b10dc873 authored by A. Wilcox's avatar A. Wilcox 🦊
Browse files

I/O: we can actually write unsigned APKs that work now

parent 10d3506a
"""I/O classes and helpers for APK files."""
from apkkit.base.package import Package
# Not used, but we need to raise ImportError if gzip isn't built.
import gzip # pylint: disable=unused-import
from import recursive_size
import glob
import gzip
import hashlib
import io
import logging
import os
import shutil
from subprocess import Popen, PIPE
import sys
import tarfile
from tempfile import mkstemp
LOGGER = logging.getLogger(__name__)
import rsa
except ImportError:
LOGGER.warning("RSA module is not available - signing packages won't work.")
def _ensure_no_debug(filename):
"""tarfile exclusion predicate to ensure /usr/lib/debug isn't included.
:returns bool: True if the file is a debug file, otherwise False.
return 'usr/lib/debug' in filename
def _sign_control(control, privkey, pubkey):
"""Sign control.tar.
:param control:
A file-like object representing the current control.tar.
:param privkey:
The path to the private key.
:param pubkey:
The public name of the public key (this will be included in the
signature, so it must match /etc/apk/keys/<name>).
A file-like object representing the signed control.tar.
control_hash = hashlib.sha256(
signature = b'signed sha256sum here'
iosignature = io.BytesIO(signature)
new_control = io.BytesIO()
new_control_tar ='w', fileobj=new_control)
tarinfo = tarfile.TarInfo('.SIGN.RSA.' + pubkey)
tarinfo.size = len(signature)
new_control_tar.addfile(tarinfo, fileobj=iosignature), 2)
shutil.copyfileobj(control, new_control)
return new_control
class APKFile:
"""Represents an APK file on disk (or in memory)."""
def __init__(self, filename, mode='r'):
self.tar =, mode)
self.package = Package.from_pkginfo(self.tar.extractfile('.PKGINFO'))
def __init__(self, filename=None, mode='r', fileobj=None, package=None):
if filename is not None:
self.tar =, mode)
elif fileobj is not None:
self.tar =, fileobj=fileobj)
self.fileobj = fileobj
raise ValueError("No filename or file object specified.")
if package is None:
self.package = Package.from_pkginfo(
self.package = package
def create(cls, package, datadir, sign=True, signfile=None, data_hash=True,
......@@ -37,4 +111,88 @@ class APKFile:
The hash method to use for hashing the data - default is sha1 to
maintain compatibility with upstream apk-tools.
raise NotImplementedError'Creating APK from data in: %s', datadir)
package.size = recursive_size(datadir)
# eventually we need to just a write tarfile replacement that can do
# the sign-mangling required for APK
if sys.version_info[:2] >= (3, 5):
mode = 'x'
mode = 'w'
fd, pkg_data_path = mkstemp(prefix='apkkit-', suffix='.tar')
gzio = io.BytesIO()'Creating data.tar...')
with os.fdopen(fd, 'xb') as fdfile:
with, fileobj=fdfile,
format=tarfile.PAX_FORMAT) as data:
for item in glob.glob(datadir + '/*'):
data.add(item, arcname=os.path.basename(item),
exclude=_ensure_no_debug)'Hashing data.tar [pass 1]...')
abuild_pipe = Popen(['abuild-tar', '--hash'], stdin=fdfile,
stdout=PIPE)'Compressing data...')
with gzip.GzipFile(mode='wb', fileobj=gzio) as gzobj:
# make the datahash
if data_hash:'Hashing data.tar [pass 2]...')
hasher = getattr(hashlib, hash_method)(
package.data_hash = hasher.hexdigest()
# if we made the hash, we need to seek back again
# if we didn't, we haven't seeked back yet
# we are finished with fdfile (data.tar), now let's make control'Creating package header...')
control = io.BytesIO()
control_tar =, fileobj=control)
ioinfo = io.BytesIO(package.to_pkginfo().encode('utf-8'))
tarinfo = tarfile.TarInfo('.PKGINFO'), 2)
tarinfo.size = ioinfo.tell()
control_tar.addfile(tarinfo, fileobj=ioinfo)
# we do NOT close control_tar yet, because we don't want the end of
# archive written out.
if sign:'Signing package...')
signfile = os.getenv('PACKAGE_PRIVKEY', signfile)
pubkey = os.getenv('PACKAGE_PUBKEY',
os.path.basename(signfile) + '.pub')
control = _sign_control(control, signfile, pubkey)'Compressing package header...')
controlgz = io.BytesIO()
with gzip.GzipFile(mode='wb', fileobj=controlgz) as gzobj:
shutil.copyfileobj(control, gzobj)
control_tar.close() # we are done with it now'Creating package file (in memory)...')
combined = io.BytesIO()
shutil.copyfileobj(controlgz, combined)
shutil.copyfileobj(gzio, combined)
return cls(fileobj=combined, package=package)
def write(self, path):'Writing APK to %s', path)
with open(path, 'xb') as new_package:
shutil.copyfileobj(self.fileobj, new_package)
"""I/O-related utility functions for APK Kit."""
from os import scandir # Python 3.5! :D
except ImportError:
from scandir import scandir # Python older-than-3.5!
def recursive_size(path):
"""Calculate the size of an entire directory tree, starting at `path`."""
running_total = 0
for entry in scandir(path):
if entry.is_file():
running_total += entry.stat().st_size
elif entry.is_dir():
running_total += recursive_size(entry.path)
return running_total
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment