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
15 changes: 13 additions & 2 deletions libarchive/ffi.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ def get_write_filter_function(filter_name):

# FFI declarations

# library version
version_number = ffi('version_number', [], c_int, check_int)

# archive_util

errno = ffi('errno', [c_archive_p], c_int)
Expand Down Expand Up @@ -317,5 +320,13 @@ def get_write_filter_function(filter_name):
ffi('write_close', [c_archive_p], c_int, check_int)
ffi('write_free', [c_archive_p], c_int, check_int)

# library version
ffi('version_number', [], c_int, check_int)
# archive encryption

try:
ffi('read_add_passphrase', [c_archive_p, c_char_p], c_int, check_int)
ffi('write_set_passphrase', [c_archive_p, c_char_p], c_int, check_int)
except AttributeError:
logger.info(
f"the libarchive being used (version {version_number()}, "
f"path {ffi.libarchive_path}) doesn't support encryption"
)
38 changes: 25 additions & 13 deletions libarchive/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,23 @@ def __iter__(self):


@contextmanager
def new_archive_read(format_name='all', filter_name='all'):
def new_archive_read(format_name='all', filter_name='all', passphrase=None):
"""Creates an archive struct suitable for reading from an archive.

Returns a pointer if successful. Raises ArchiveError on error.
"""
archive_p = ffi.read_new()
try:
if passphrase:
if not isinstance(passphrase, bytes):
passphrase = passphrase.encode('utf-8')
try:
ffi.read_add_passphrase(archive_p, passphrase)
except AttributeError:
raise NotImplementedError(
f"the libarchive being used (version {ffi.version_number()}, "
f"path {ffi.libarchive_path}) doesn't support encryption"
)
ffi.get_read_filter_function(filter_name)(archive_p)
ffi.get_read_format_function(format_name)(archive_p)
yield archive_p
Expand All @@ -46,23 +56,24 @@ def new_archive_read(format_name='all', filter_name='all'):
def custom_reader(
read_func, format_name='all', filter_name='all',
open_func=VOID_CB, close_func=VOID_CB, block_size=page_size,
archive_read_class=ArchiveRead
archive_read_class=ArchiveRead, passphrase=None,
):
"""Read an archive using a custom function.
"""
open_cb = OPEN_CALLBACK(open_func)
read_cb = READ_CALLBACK(read_func)
close_cb = CLOSE_CALLBACK(close_func)
with new_archive_read(format_name, filter_name) as archive_p:
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
ffi.read_open(archive_p, None, open_cb, read_cb, close_cb)
yield archive_read_class(archive_p)


@contextmanager
def fd_reader(fd, format_name='all', filter_name='all', block_size=4096):
def fd_reader(fd, format_name='all', filter_name='all', block_size=4096,
passphrase=None):
"""Read an archive from a file descriptor.
"""
with new_archive_read(format_name, filter_name) as archive_p:
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
try:
block_size = fstat(fd).st_blksize
except (OSError, AttributeError): # pragma: no cover
Expand All @@ -72,10 +83,11 @@ def fd_reader(fd, format_name='all', filter_name='all', block_size=4096):


@contextmanager
def file_reader(path, format_name='all', filter_name='all', block_size=4096):
def file_reader(path, format_name='all', filter_name='all', block_size=4096,
passphrase=None):
"""Read an archive from a file.
"""
with new_archive_read(format_name, filter_name) as archive_p:
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
try:
block_size = stat(path).st_blksize
except (OSError, AttributeError): # pragma: no cover
Expand All @@ -85,17 +97,17 @@ def file_reader(path, format_name='all', filter_name='all', block_size=4096):


@contextmanager
def memory_reader(buf, format_name='all', filter_name='all'):
def memory_reader(buf, format_name='all', filter_name='all', passphrase=None):
"""Read an archive from memory.
"""
with new_archive_read(format_name, filter_name) as archive_p:
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
ffi.read_open_memory(archive_p, cast(buf, c_void_p), len(buf))
yield ArchiveRead(archive_p)


@contextmanager
def stream_reader(stream, format_name='all', filter_name='all',
block_size=page_size):
block_size=page_size, passphrase=None):
"""Read an archive from a stream.

The `stream` object must support the standard `readinto` method.
Expand All @@ -115,14 +127,14 @@ def read_func(archive_p, context, ptrptr):
open_cb = OPEN_CALLBACK(VOID_CB)
read_cb = READ_CALLBACK(read_func)
close_cb = CLOSE_CALLBACK(VOID_CB)
with new_archive_read(format_name, filter_name) as archive_p:
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
ffi.read_open(archive_p, None, open_cb, read_cb, close_cb)
yield ArchiveRead(archive_p)


@contextmanager
def seekable_stream_reader(stream, format_name='all', filter_name='all',
block_size=page_size):
block_size=page_size, passphrase=None):
"""Read an archive from a seekable stream.

The `stream` object must support the standard `readinto`, 'seek' and
Expand All @@ -149,7 +161,7 @@ def seek_func(archive_p, context, offset, whence):
read_cb = READ_CALLBACK(read_func)
seek_cb = SEEK_CALLBACK(seek_func)
close_cb = CLOSE_CALLBACK(VOID_CB)
with new_archive_read(format_name, filter_name) as archive_p:
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
ffi.read_set_seek_callback(archive_p, seek_cb)
ffi.read_open(archive_p, None, open_cb, read_cb, close_cb)
yield ArchiveRead(archive_p)
45 changes: 36 additions & 9 deletions libarchive/write.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from contextlib import contextmanager
from ctypes import byref, cast, c_char, c_size_t, c_void_p, POINTER
import warnings

from . import ffi
from .entry import ArchiveEntry, new_archive_entry
Expand Down Expand Up @@ -145,16 +146,38 @@ def add_file_from_memory(


@contextmanager
def new_archive_write(format_name, filter_name=None, options=''):
def new_archive_write(format_name,
filter_name=None,
options='',
passphrase=None):
archive_p = ffi.write_new()
try:
ffi.get_write_format_function(format_name)(archive_p)
if filter_name:
ffi.get_write_filter_function(filter_name)(archive_p)
if passphrase and 'encryption' not in options:
if format_name == 'zip':
warnings.warn(
"The default encryption scheme of zip archives is weak. "
"Use `options='encryption=$type'` to specify the encryption "
"type you want to use. The supported values are 'zipcrypt' "
"(the weak default), 'aes128' and 'aes256'."
)
options += ',encryption' if options else 'encryption'
if options:
if not isinstance(options, bytes):
options = options.encode('utf-8')
ffi.write_set_options(archive_p, options)
if passphrase:
if not isinstance(passphrase, bytes):
passphrase = passphrase.encode('utf-8')
try:
ffi.write_set_passphrase(archive_p, passphrase)
except AttributeError:
raise NotImplementedError(
f"the libarchive being used (version {ffi.version_number()}, "
f"path {ffi.libarchive_path}) doesn't support encryption"
)
yield archive_p
ffi.write_close(archive_p)
ffi.write_free(archive_p)
Expand All @@ -168,7 +191,7 @@ def new_archive_write(format_name, filter_name=None, options=''):
def custom_writer(
write_func, format_name, filter_name=None,
open_func=VOID_CB, close_func=VOID_CB, block_size=page_size,
archive_write_class=ArchiveWrite, options=''
archive_write_class=ArchiveWrite, options='', passphrase=None,
):

def write_cb_internal(archive_p, context, buffer_, length):
Expand All @@ -179,7 +202,8 @@ def write_cb_internal(archive_p, context, buffer_, length):
write_cb = WRITE_CALLBACK(write_cb_internal)
close_cb = CLOSE_CALLBACK(close_func)

with new_archive_write(format_name, filter_name, options) as archive_p:
with new_archive_write(format_name, filter_name, options,
passphrase) as archive_p:
ffi.write_set_bytes_in_last_block(archive_p, 1)
ffi.write_set_bytes_per_block(archive_p, block_size)
ffi.write_open(archive_p, None, open_cb, write_cb, close_cb)
Expand All @@ -189,29 +213,32 @@ def write_cb_internal(archive_p, context, buffer_, length):
@contextmanager
def fd_writer(
fd, format_name, filter_name=None,
archive_write_class=ArchiveWrite, options=''
archive_write_class=ArchiveWrite, options='', passphrase=None,
):
with new_archive_write(format_name, filter_name, options) as archive_p:
with new_archive_write(format_name, filter_name, options,
passphrase) as archive_p:
ffi.write_open_fd(archive_p, fd)
yield archive_write_class(archive_p)


@contextmanager
def file_writer(
filepath, format_name, filter_name=None,
archive_write_class=ArchiveWrite, options=''
archive_write_class=ArchiveWrite, options='', passphrase=None,
):
with new_archive_write(format_name, filter_name, options) as archive_p:
with new_archive_write(format_name, filter_name, options,
passphrase) as archive_p:
ffi.write_open_filename_w(archive_p, filepath)
yield archive_write_class(archive_p)


@contextmanager
def memory_writer(
buf, format_name, filter_name=None,
archive_write_class=ArchiveWrite, options=''
archive_write_class=ArchiveWrite, options='', passphrase=None,
):
with new_archive_write(format_name, filter_name, options) as archive_p:
with new_archive_write(format_name, filter_name, options,
passphrase) as archive_p:
used = byref(c_size_t())
buf_p = cast(buf, c_void_p)
ffi.write_open_memory(archive_p, buf_p, len(buf), used)
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ universal = 1
[flake8]
exclude=.?*,env*/
ignore = E226,E731,W504
max-line-length = 80
max-line-length = 85