Skip to content

Commit efc6f4e

Browse files
juliospainJulio Martin-HidalgoChangaco
authored
create seekable_stream_reader context manager (#107)
Co-authored-by: Julio Martin-Hidalgo <julio.martin-hidalgo@airbus.com> Co-authored-by: Charly C <changaco@changaco.oy.lc>
1 parent 074732b commit efc6f4e

4 files changed

Lines changed: 60 additions & 2 deletions

File tree

libarchive/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from .exception import ArchiveError
33
from .extract import extract_fd, extract_file, extract_memory
44
from .read import (
5-
custom_reader, fd_reader, file_reader, memory_reader, stream_reader
5+
custom_reader, fd_reader, file_reader, memory_reader, stream_reader,
6+
seekable_stream_reader
67
)
78
from .write import custom_writer, fd_writer, file_writer, memory_writer
89

@@ -11,5 +12,6 @@
1112
ArchiveError,
1213
extract_fd, extract_file, extract_memory,
1314
custom_reader, fd_reader, file_reader, memory_reader, stream_reader,
15+
seekable_stream_reader,
1416
custom_writer, fd_writer, file_writer, memory_writer
1517
)]

libarchive/ffi.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848
READ_CALLBACK = CFUNCTYPE(
4949
c_ssize_t, c_void_p, c_void_p, POINTER(c_void_p)
5050
)
51+
SEEK_CALLBACK = CFUNCTYPE(
52+
c_longlong, c_int, c_void_p, c_longlong, c_int
53+
)
5154
OPEN_CALLBACK = CFUNCTYPE(c_int, c_void_p, c_void_p)
5255
CLOSE_CALLBACK = CFUNCTYPE(c_int, c_void_p, c_void_p)
5356
VOID_CB = lambda *_: ARCHIVE_OK
@@ -226,6 +229,8 @@ def get_write_filter_function(filter_name):
226229
logger.info(str(e))
227230
READ_FILTERS.remove(f_name)
228231

232+
ffi('read_set_seek_callback', [c_archive_p, SEEK_CALLBACK], c_int, check_int)
233+
229234
ffi('read_open',
230235
[c_archive_p, c_void_p, OPEN_CALLBACK, READ_CALLBACK, CLOSE_CALLBACK],
231236
c_int, check_int)

libarchive/read.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from . import ffi
88
from .ffi import (ARCHIVE_EOF, OPEN_CALLBACK, READ_CALLBACK, CLOSE_CALLBACK,
9-
VOID_CB, page_size)
9+
SEEK_CALLBACK, VOID_CB, page_size)
1010
from .entry import ArchiveEntry, new_archive_entry
1111

1212

@@ -120,3 +120,38 @@ def read_func(archive_p, context, ptrptr):
120120
with new_archive_read(format_name, filter_name) as archive_p:
121121
ffi.read_open(archive_p, None, open_cb, read_cb, close_cb)
122122
yield ArchiveRead(archive_p)
123+
124+
125+
@contextmanager
126+
def seekable_stream_reader(stream, format_name='all', filter_name='all',
127+
block_size=page_size):
128+
"""Read an archive from a seekable stream.
129+
130+
The `stream` object must support the standard `readinto`, 'seek' and
131+
'tell' methods.
132+
"""
133+
buf = create_string_buffer(block_size)
134+
buf_p = cast(buf, c_void_p)
135+
136+
def read_func(archive_p, context, ptrptr):
137+
# readinto the buffer, returns number of bytes read
138+
length = stream.readinto(buf)
139+
# write the address of the buffer into the pointer
140+
ptrptr = cast(ptrptr, POINTER(c_void_p))
141+
ptrptr[0] = buf_p
142+
# tell libarchive how much data was written into the buffer
143+
return length
144+
145+
def seek_func(archive_p, context, offset, whence):
146+
stream.seek(offset, whence)
147+
# tell libarchive the current position
148+
return stream.tell()
149+
150+
open_cb = OPEN_CALLBACK(VOID_CB)
151+
read_cb = READ_CALLBACK(read_func)
152+
seek_cb = SEEK_CALLBACK(seek_func)
153+
close_cb = CLOSE_CALLBACK(VOID_CB)
154+
with new_archive_read(format_name, filter_name) as archive_p:
155+
ffi.read_set_seek_callback(archive_p, seek_cb)
156+
ffi.read_open(archive_p, None, open_cb, read_cb, close_cb)
157+
yield ArchiveRead(archive_p)

tests/test_rwx.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,22 @@ def test_custom_writer_and_stream_reader():
9999
check_archive(archive, tree)
100100

101101

102+
def test_custom_writer_and_seekable_stream_reader():
103+
# Collect information on what should be in the archive
104+
tree = treestat('libarchive')
105+
106+
# Create an archive of our libarchive/ directory
107+
stream = io.BytesIO()
108+
with libarchive.custom_writer(stream.write, '7zip') as archive:
109+
archive.add_files('libarchive/')
110+
stream.seek(0)
111+
112+
# Read the archive and check that the data is correct
113+
with libarchive.seekable_stream_reader(stream, '7zip') as archive:
114+
paths = [entry.name.rstrip('/') for entry in archive]
115+
assert sorted(paths) == sorted(tree)
116+
117+
102118
@patch('libarchive.ffi.write_fail')
103119
def test_write_fail(write_fail_mock):
104120
buf = bytes(bytearray(1000000))

0 commit comments

Comments
 (0)