summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md14
-rw-r--r--ffi/Makefile11
-rw-r--r--ffi/lang/python/.gitignore8
-rw-r--r--ffi/lang/python/Makefile81
-rw-r--r--ffi/lang/python/README.md5
-rw-r--r--ffi/lang/python/examples/decrypt.py43
-rw-r--r--ffi/lang/python/sequoia/__init__.py8
-rw-r--r--ffi/lang/python/sequoia/core.py131
-rw-r--r--ffi/lang/python/sequoia/error.py69
-rw-r--r--ffi/lang/python/sequoia/glue.py102
-rw-r--r--ffi/lang/python/sequoia/net.py37
-rw-r--r--ffi/lang/python/sequoia/openpgp.py361
-rw-r--r--ffi/lang/python/sequoia/prelude.py6
-rw-r--r--ffi/lang/python/sequoia/sequoia_build.py51
-rw-r--r--ffi/lang/python/sequoia/store.py236
-rw-r--r--ffi/lang/python/setup.cfg2
-rw-r--r--ffi/lang/python/setup.py41
-rw-r--r--ffi/lang/python/tests/test_armor.py71
-rw-r--r--ffi/lang/python/tests/test_fingerprint.py45
-rw-r--r--ffi/lang/python/tests/test_keyid.py63
-rw-r--r--ffi/lang/python/tests/test_packet_parser.py51
-rw-r--r--ffi/lang/python/tests/test_store.py47
-rw-r--r--ffi/lang/python/tests/test_tpk.py83
-rw-r--r--openpgp/tests/data/keys/testy.asc30
24 files changed, 1591 insertions, 5 deletions
diff --git a/README.md b/README.md
index 38a2d2f5..590d649a 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,20 @@ use `make install`. The latter target honors `PREFIX` and `DESTDIR`.
Finally, to return your source tree to its pristine state, run `make
clean`.
+Bindings
+--------
+
+### Python
+
+The FFI crate contains Python bindings. To disable building, testing,
+and installing the Python bindings, use `make PYTHON=disable`.
+
+To build the Python bindings, you will need cffi and pytest for Python3.
+
+Debian:
+
+ $ apt install python3-cffi python3-pytest
+
Getting help
============
diff --git a/ffi/Makefile b/ffi/Makefile
index 60447429..64f48235 100644
--- a/ffi/Makefile
+++ b/ffi/Makefile
@@ -23,15 +23,12 @@ all: build
.PHONY: build
build:
- sed -e 's|VERSION|$(VERSION)|g' \
- -e 's|PREFIX|$(shell pwd)|g' \
- -e 's|libdir=.*|libdir='"$(CARGO_TARGET_DIR)"'/debug|g' \
- sequoia.pc.in \
- > $(CARGO_TARGET_DIR)/debug/sequoia.pc
+ $(MAKE) -Clang/python build
# Testing and examples.
.PHONY: test check
test check:
+ $(MAKE) -Clang/python test
.PHONY: examples
examples:
@@ -40,6 +37,7 @@ examples:
# Installation.
.PHONY: build-release
build-release:
+ $(MAKE) -Clang/python build-release
.PHONY: install
install:
@@ -62,9 +60,12 @@ install:
$(DESTDIR)$(PREFIX)/lib/libsequoia_ffi.so
$(INSTALL) $(CARGO_TARGET_DIR)/release/libsequoia_ffi.a \
$(DESTDIR)$(PREFIX)/lib/libsequoia_ffi.a
+ # Now the bindings.
+ $(MAKE) -Clang/python install
# Housekeeping.
.PHONY: clean
clean:
rm -f sequoia.pc
$(MAKE) -Cexamples clean
+ $(MAKE) -Clang/python clean
diff --git a/ffi/lang/python/.gitignore b/ffi/lang/python/.gitignore
new file mode 100644
index 00000000..6d916e40
--- /dev/null
+++ b/ffi/lang/python/.gitignore
@@ -0,0 +1,8 @@
+/build
+/dist
+/sequoia.egg-info
+/.eggs
+__pycache__
+*.pyc
+.stamp*
+_sequoia.*.so
diff --git a/ffi/lang/python/Makefile b/ffi/lang/python/Makefile
new file mode 100644
index 00000000..960045ac
--- /dev/null
+++ b/ffi/lang/python/Makefile
@@ -0,0 +1,81 @@
+# Makefile for Sequoia's Python bindings.
+
+# Configuration.
+PREFIX ?= /usr/local
+DESTDIR ?=
+CFLAGS += -I../../include
+
+# Tools.
+PYTHON ?= python3
+IPYTHON ?= ipython3
+PYTEST ?= pytest-3
+INSTALL ?= install
+
+ifneq "$(PYTHON)" "disable"
+PY_VERSION = $(shell $(PYTHON) -c \
+ 'import sys; print("{0.major}.{0.minor}".format(sys.version_info))')
+endif
+
+# Make sure subprocesses pick these up.
+export CFLAGS
+
+all: build
+
+.PHONY: build
+build: .stamp-build
+.stamp-build: sequoia/* ../../include/sequoia/*
+ifneq "$(PYTHON)" "disable"
+ LDFLAGS=-L../../../target/debug $(PYTHON) setup.py build
+ touch $@
+endif
+
+# Testing and examples.
+.PHONY: test check
+test check:
+ifneq "$(PYTHON)" "disable"
+ LDFLAGS=-L../../../target/debug LD_LIBRARY_PATH=../../../target/debug \
+ $(PYTHON) setup.py test
+endif
+
+.PHONY: shell
+shell: build
+ifneq "$(PYTHON)" "disable"
+ cp build/*/_sequoia.abi*.so . # XXX can we get setuptools to do that?
+ LDFLAGS=-L../../../target/debug LD_LIBRARY_PATH=../../../target/debug \
+ $(IPYTHON) -i -c \
+'from sequoia.prelude import *; ctx = Context("org.sequoia-pgp.tests.interactive")'
+endif
+
+# Installation.
+.PHONY: build-release
+build-release: .stamp-build-release
+.stamp-build-release:
+ifneq "$(PYTHON)" "disable"
+ rm -f .stamp-build
+ $(PYTHON) setup.py clean
+ LDFLAGS=-L../../../target/release \
+ $(PYTHON) setup.py build
+ touch $@
+endif
+
+ifneq "$(DESTDIR)" ""
+ root_arg=--root=$(DESTDIR)
+endif
+
+.PHONY: install
+install: build-release
+ifneq "$(PYTHON)" "disable"
+ $(INSTALL) -d $(DESTDIR)$(PREFIX)/lib/python$(PY_VERSION)/site-packages
+
+ LDFLAGS=-L../../../target/release \
+ $(PYTHON) setup.py install $(root_arg) --prefix=$(PREFIX)
+endif
+
+# Housekeeping.
+.PHONY: clean
+clean:
+ifneq "$(PYTHON)" "disable"
+ $(PYTHON) setup.py clean
+ rm -f _sequoia.*.so
+ rm -f .stamp-build .stamp-build-release
+endif
diff --git a/ffi/lang/python/README.md b/ffi/lang/python/README.md
new file mode 100644
index 00000000..3be34691
--- /dev/null
+++ b/ffi/lang/python/README.md
@@ -0,0 +1,5 @@
+Python bindings for Sequoia
+===========================
+
+These bindings are work in progress. You are welcome to play with
+them, but we will likely break the API in the future.
diff --git a/ffi/lang/python/examples/decrypt.py b/ffi/lang/python/examples/decrypt.py
new file mode 100644
index 00000000..c65f151d
--- /dev/null
+++ b/ffi/lang/python/examples/decrypt.py
@@ -0,0 +1,43 @@
+import sys
+import os
+from getpass import getpass
+from enum import Enum, auto
+from sequoia.core import Context, NetworkPolicy
+from sequoia.openpgp import Tag, PacketParser
+
+ctx = Context("org.sequoia-pgp.examples",
+ network_policy=NetworkPolicy.Offline,
+ ephemeral=True)
+
+class State(Enum):
+ Start = auto()
+ Decrypted = auto()
+ Deciphered = auto()
+ Done = auto()
+
+state = State.Start
+algo, key = None, None
+pp = PacketParser.open(ctx, sys.argv[1])
+while pp.has_packet:
+ packet = pp.packet
+ tag = packet.tag
+
+ if state == State.Start:
+ if tag == Tag.SKESK:
+ passphrase = getpass("Enter passphrase to decrypt message: ")
+ algo, key = packet.match().decrypt(passphrase.encode())
+ state = State.Decrypted
+ elif tag == Tag.PKESK:
+ sys.stderr.write("Decryption using PKESK not yet supported.\n")
+ elif state == State.Decrypted:
+ if tag == Tag.SEIP:
+ pp.decrypt(algo, key)
+ state = State.Deciphered
+ elif state == State.Deciphered:
+ if tag == Tag.Literal:
+ body = pp.buffer_unread_content()
+ os.write(sys.stdout.fileno(), body)
+ state = State.Done
+
+ pp.recurse()
+assert state == State.Done
diff --git a/ffi/lang/python/sequoia/__init__.py b/ffi/lang/python/sequoia/__init__.py
new file mode 100644
index 00000000..4f84346e
--- /dev/null
+++ b/ffi/lang/python/sequoia/__init__.py
@@ -0,0 +1,8 @@
+from . import (
+ prelude,
+ error,
+ openpgp,
+ core,
+ net,
+ store,
+)
diff --git a/ffi/lang/python/sequoia/core.py b/ffi/lang/python/sequoia/core.py
new file mode 100644
index 00000000..9810c27b
--- /dev/null
+++ b/ffi/lang/python/sequoia/core.py
@@ -0,0 +1,131 @@
+import io
+from enum import Enum
+
+from _sequoia import ffi, lib
+from .error import Error
+from .glue import SQObject
+
+class NetworkPolicy(Enum):
+ Offline = lib.SQ_NETWORK_POLICY_OFFLINE
+ Anonymized = lib.SQ_NETWORK_POLICY_ANONYMIZED
+ Encrypted = lib.SQ_NETWORK_POLICY_ENCRYPTED
+ Insecure = lib.SQ_NETWORK_POLICY_INSECURE
+
+class IPCPolicy(Enum):
+ External = lib.SQ_IPC_POLICY_EXTERNAL
+ Internal = lib.SQ_IPC_POLICY_INTERNAL
+ Robust = lib.SQ_IPC_POLICY_ROBUST
+
+class Context(SQObject):
+ _del = lib.sq_context_free
+ def __init__(self, domain,
+ home=None,
+ network_policy=NetworkPolicy.Encrypted,
+ ipc_policy=IPCPolicy.Robust,
+ ephemeral=False):
+ cfg = lib.sq_context_configure(domain.encode())
+ if home:
+ lib.sq_config_home(cfg, home.encode())
+ lib.sq_config_network_policy(cfg, network_policy.value)
+ lib.sq_config_ipc_policy(cfg, ipc_policy.value)
+ if ephemeral:
+ lib.sq_config_ephemeral(cfg)
+ err = ffi.new("sq_error_t[1]")
+ o = lib.sq_config_build(cfg, err)
+ if o == ffi.NULL:
+ raise Error._from(err[0])
+ super(Context, self).__init__(o)
+
+class AbstractReader(SQObject, io.RawIOBase):
+ _del = lib.sq_reader_free
+
+ def readable(self):
+ return True
+ def writable(self):
+ return False
+
+ def readinto(self, buf):
+ bytes_read = lib.sq_reader_read(
+ self.context().ref(), self.ref(),
+ ffi.cast("uint8_t *", ffi.from_buffer(buf)), len(buf))
+ if bytes_read < 0:
+ raise Error._last(self.context())
+ return bytes_read
+
+ def close(self):
+ self._delete()
+
+ # Implement the context manager protocol.
+ def __enter__(self):
+ return self
+ def __exit__(self, *args):
+ self.close()
+ return False
+
+class Reader(AbstractReader):
+ @classmethod
+ def open(cls, ctx, filename):
+ return Reader(
+ lib.sq_reader_from_file(
+ ctx.ref(),
+ filename.encode()),
+ context=ctx)
+
+ @classmethod
+ def from_fd(cls, ctx, fd):
+ return Reader(lib.sq_reader_from_fd(fd),
+ context=ctx)
+
+ @classmethod
+ def from_bytes(cls, ctx, buf):
+ return Reader(
+ lib.sq_reader_from_bytes(
+ ffi.cast("uint8_t *", ffi.from_buffer(buf)), len(buf)),
+ context=ctx)
+
+class AbstractWriter(SQObject, io.RawIOBase):
+ _del = lib.sq_writer_free
+
+ def readable(self):
+ return False
+ def writable(self):
+ return True
+
+ def write(self, buf):
+ bytes_written = lib.sq_writer_write(
+ self.context().ref(), self.ref(),
+ ffi.cast("const uint8_t *", ffi.from_buffer(buf)), len(buf))
+ if bytes_written < 0:
+ raise Error._last(self.context())
+ return bytes_written
+
+ def close(self):
+ self._delete()
+
+ # Implement the context manager protocol.
+ def __enter__(self):
+ return self
+ def __exit__(self, *args):
+ self.close()
+ return False
+
+class Writer(AbstractWriter):
+ @classmethod
+ def open(cls, ctx, filename):
+ return Writer(
+ lib.sq_writer_from_file(
+ ctx.ref(),
+ filename.encode()),
+ context=ctx)
+
+ @classmethod
+ def from_fd(cls, ctx, fd):
+ return Writer(lib.sq_writer_from_fd(fd),
+ context=ctx)
+
+ @classmethod
+ def from_bytes(cls, ctx, buf):
+ return Writer(
+ lib.sq_writer_from_bytes(
+ ffi.cast("uint8_t *", ffi.from_buffer(buf)), len(buf)),
+ context=ctx)
diff --git a/ffi/lang/python/sequoia/error.py b/ffi/lang/python/sequoia/error.py
new file mode 100644
index 00000000..29eb2545
--- /dev/null
+++ b/ffi/lang/python/sequoia/error.py
@@ -0,0 +1,69 @@
+from _sequoia import ffi, lib
+from .glue import sq_str
+
+class Error(Exception):
+ @classmethod
+ def _from(cls, o):
+ if o == ffi.NULL:
+ return MalformedValue()
+
+ status = lib.sq_error_status(o)
+ return _status_map[status](o)
+
+ @classmethod
+ def _last(cls, ctx):
+ if not ctx:
+ return MalformedValue()
+ return Error._from(lib.sq_context_last_error(ctx.ref()))
+
+class MalformedValue(Error, ValueError):
+ def __init__(self, message="Malformed value"):
+ super(MalformedValue, self).__init__(message)
+
+class SQError(Error):
+ def __init__(self, o):
+ self.__o = ffi.gc(o, lib.sq_error_free)
+ super(SQError, self).__init__(sq_str(lib.sq_error_string(self.__o)))
+
+class Success(SQError):
+ pass
+
+class UnknownError(SQError):
+ pass
+
+class NetworkPolicyViolation(SQError):
+ pass
+
+class IoError(SQError):
+ pass
+
+class InvalidOperataion(SQError):
+ pass
+
+class MalformedPacket(SQError):
+ pass
+
+class UnsupportedHashAlgorithm(SQError):
+ pass
+
+class UnsupportedSymmetricAlgorithm(SQError):
+ pass
+
+class InvalidPassword(SQError):
+ pass
+
+class InvalidSessionKey(SQError):
+ pass
+
+_status_map = {
+ lib.SQ_STATUS_SUCCESS: Success,
+ lib.SQ_STATUS_UNKNOWN_ERROR: UnknownError,
+ lib.SQ_STATUS_NETWORK_POLICY_VIOLATION: NetworkPolicyViolation,
+ lib.SQ_STATUS_IO_ERROR: IoError,
+ lib.SQ_STATUS_INVALID_OPERATION: InvalidOperataion,
+ lib.SQ_STATUS_MALFORMED_PACKET: MalformedPacket,
+ lib.SQ_STATUS_UNSUPPORTED_HASH_ALGORITHM: UnsupportedHashAlgorithm,
+ lib.SQ_STATUS_UNSUPPORTED_SYMMETRIC_ALGORITHM: UnsupportedSymmetricAlgorithm,
+ lib.SQ_STATUS_INVALID_PASSWORD: InvalidPassword,
+ lib.SQ_STATUS_INVALID_SESSION_KEY: InvalidSessionKey,
+}
diff --git a/ffi/lang/python/sequoia/glue.py b/ffi/lang/python/sequoia/glue.py
new file mode 100644
index 00000000..4b74d609
--- /dev/null
+++ b/ffi/lang/python/sequoia/glue.py
@@ -0,0 +1,102 @@
+from datetime import datetime, timezone
+
+from _sequoia import ffi, lib
+from . import error
+
+class SQObject(object):
+ # These class attributes determine what features the wrapper class
+ # implements. They must be set to the relevant Sequoia functions.
+ #
+ # XXX: Once we can assume Python3.6 we can use '__init_subclass__'
+ # and reflection on the 'lib' object to set them automatically
+ # using the type name.
+ _del = None
+ _clone = None
+ _eq = None
+ _str = None
+ _hash = None
+
+ def __init__(self, o, context=None, owner=None, references=None):
+ if o == ffi.NULL:
+ raise error.Error._last(context)
+ self.__o = None
+ self.ref_replace(o, owner=owner, references=references)
+ self.__ctx = context
+ if self.__class__._hash is None and not hasattr(self.__class__, '__hash__'):
+ # Unhashable types must have '__hash__' set to None.
+ # Until we can use '__init_subclass__', we need to patch
+ # the class here. Yuck.
+ self.__class__.__hash__ = None
+
+ def ref(self):
+ return self.__o
+
+ def ref_consume(self):
+ ref = self.ref()
+ self._delete(skip_free=True)
+ return ref
+
+ def ref_replace(self, new, owner=None, references=None):
+ old = self.ref_consume()
+ if self._del and owner == None:
+ # There is a destructor and We own the referenced object
+ # new.
+ self.__o = ffi.gc(new, self._del)
+ else:
+ self.__o = new
+ self.__owner = owner
+ self.__references = references
+ return old
+
+ def _delete(self, skip_free=False):
+ if not self.__o:
+ return
+ if self._del and skip_free:
+ ffi.gc(self.__o, None)
+ self.__o = None
+ self.__owner = None
+ self.__references = None
+
+ def context(self):
+ return self.__ctx
+
+ def __str__(self):
+ if self._str:
+ return _str(self._str(self.ref()))
+ else:
+ return repr(self)
+
+ def __eq__(self, other):
+ if self._eq:
+ return (isinstance(other, self.__class__)
+ and bool(self._eq(self.ref(), other.ref())))
+ else:
+ return NotImplemented
+
+ def copy(self):
+ if self._clone:
+ return self.__class__(self._clone(self.ref()))
+ else:
+ raise NotImplementedError()
+
+ def __hash__(self):
+ return self._hash(self.ref())
+
+def sq_str(s):
+ t = ffi.string(s).decode()
+ lib.sq_string_free(s)
+ return t
+_str = sq_str
+
+def sq_iterator(iterator, next_fn, map=lambda x: x):
+ while True:
+ entry = next_fn(iterator)
+ if entry == ffi.NULL:
+ break
+ yield map(entry)
+
+def sq_time(t):
+ if t == 0:
+ return None
+ else:
+ return datetime.fromtimestamp(t, timezone.utc)
diff --git a/ffi/lang/python/sequoia/net.py b/ffi/lang/python/sequoia/net.py
new file mode 100644
index 00000000..c5f4a7f4
--- /dev/null
+++ b/ffi/lang/python/sequoia/net.py
@@ -0,0 +1,37 @@
+from _sequoia import ffi, lib
+
+from .openpgp import TPK
+from .error import Error
+from .glue import SQObject
+
+class KeyServer(SQObject):
+ _del = lib.sq_keyserver_free
+
+ @classmethod
+ def new(cls, ctx, uri, cert=None):
+ if not cert:
+ ks = lib.sq_keyserver_new(ctx.ref(), uri.encode())
+ else:
+ ks = lib.sq_keyserver_with_cert(
+ ctx.ref(), uri.encode(),
+ ffi.cast("uint8_t *", ffi.from_buffer(cert)),
+ len(cert))
+ return KeyServer(ks, context=ctx)
+
+ @classmethod
+ def sks_pool(cls, ctx):
+ return KeyServer(lib.sq_keyserver_sks_pool(ctx.ref()),
+ context=ctx)
+
+ def get(self, keyid):
+ return TPK(lib.sq_keyserver_get(self.context().ref(),
+ self.ref(),
+ keyid.ref()),
+ context=self.context())
+
+ def send(self, tpk):
+ r = lib.sq_keyserver_send(self.context().ref(),
+ self.ref(),
+ tpk.ref())
+ if r:
+ raise Error._last(self.context())
diff --git a/ffi/lang/python/sequoia/openpgp.py b/ffi/lang/python/sequoia/openpgp.py
new file mode 100644
index 00000000..aaea5262
--- /dev/null
+++ b/ffi/lang/python/sequoia/openpgp.py
@@ -0,0 +1,361 @@
+from enum import Enum
+
+from _sequoia import ffi, lib
+from .error import Error
+from .glue import _str, SQObject
+from .core import AbstractReader, AbstractWriter
+
+class KeyID(SQObject):
+ _del = lib.sq_keyid_free
+ _clone = lib.sq_keyid_clone
+ _str = lib.sq_keyid_to_string
+ _eq = lib.sq_keyid_equal
+ _hash = lib.sq_keyid_hash
+
+ @classmethod
+ def from_bytes(cls, fp):
+ if len(fp) != 8:
+ raise Error("KeyID must be of length 8")
+ return KeyID(lib.sq_keyid_from_bytes(
+ ffi.cast("uint8_t *", ffi.from_buffer(fp))))
+
+ @classmethod
+ def from_hex(cls, fp):
+ return KeyID(lib.sq_keyid_from_hex(fp.encode()))
+
+ def hex(self):
+ return _str(lib.sq_keyid_to_hex(self.ref()))
+
+class Fingerprint(SQObject):
+ _del = lib.sq_fingerprint_free
+ _clone = lib.sq_fingerprint_clone
+ _str = lib.sq_fingerprint_to_string
+ _eq = lib.sq_fingerprint_equal
+ _hash = lib.sq_fingerprint_hash
+
+ @classmethod
+ def from_bytes(cls, fp):
+ return Fingerprint(lib.sq_fingerprint_from_bytes(
+ ffi.cast("uint8_t *", ffi.from_buffer(fp)), len(fp)))
+
+ @classmethod
+ def from_hex(cls, fp):
+ return Fingerprint(lib.sq_fingerprint_from_hex(fp.encode()))
+
+ def hex(self):
+ return _str(lib.sq_fingerprint_to_hex(self.ref()))
+
+ def keyid(self):
+ return KeyID(lib.sq_fingerprint_to_keyid(self.ref()))
+
+class PacketPile(SQObject):
+ _del = lib.sq_packet_pile_free
+ _clone = lib.sq_packet_pile_clone
+
+ @classmethod
+ def from_reader(cls, ctx, reader):
+ return PacketPile(lib.sq_packet_pile_from_reader(ctx.ref(), reader.ref()),
+ context=ctx)
+
+ @classmethod
+ def open(cls, ctx, filename):
+ return PacketPile(lib.sq_packet_pile_from_file(ctx.ref(), filename.encode()),
+ context=ctx)
+
+ @classmethod
+ def from_bytes(cls, ctx, source):
+ return PacketPile(lib.sq_packet_pile_from_bytes(ctx.ref(),
+ ffi.from_buffer(source),
+ len(source)),
+ context=ctx)
+
+ def serialize(self, writer):
+ status = lib.sq_packet_pile_serialize(self.context().ref(),
+ self.ref(),
+ writer.ref())
+ if status:
+ raise Error._last(self.context())
+
+class TPK(SQObject):
+ _del = lib.sq_tpk_free
+ _clone = lib.sq_tpk_clone
+ _eq = lib.sq_tpk_equal
+
+ @classmethod
+ def from_reader(cls, ctx, reader):
+ return TPK(lib.sq_tpk_from_reader(ctx.ref(), reader.ref()),
+ context=ctx)
+
+ @classmethod
+ def open(cls, ctx, filename):
+ return TPK(lib.sq_tpk_from_file(ctx.ref(), filename.encode()),
+ context=ctx)
+
+ @classmethod
+ def from_packet_pile(cls, ctx, packet_pile):
+ return TPK(lib.sq_tpk_from_packet_pile(ctx.ref(), packet_pile.ref_consume()),
+ context=ctx)
+
+ @classmethod
+ def from_bytes(cls, ctx, source):
+ return TPK(lib.sq_tpk_from_bytes(ctx.ref(),
+ ffi.from_buffer(source),
+ len(source)),
+ context=ctx)
+
+ def serialize(self, writer):
+ status = lib.sq_tpk_serialize(self.context().ref(),
+ self.ref(),
+ writer.ref())
+ if status:
+ raise Error._last(self.context())
+
+ def fingerprint(self):
+ return Fingerprint(lib.sq_tpk_fingerprint(self.ref()),
+ context=self.context())
+
+ def merge(self, other):
+ new = lib.sq_tpk_merge(self.context().ref(),
+ self.ref_consume(),