././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1709644463.4981325
libarchive-c-5.1/ 0000755 0002166 0000144 00000000000 14571615257 013450 5 ustar 00changaco users ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1480442606.0
libarchive-c-5.1/.gitattributes 0000644 0002166 0000144 00000000031 13017341356 016323 0 ustar 00changaco users version.py export-subst
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1709644463.4947991
libarchive-c-5.1/.github/ 0000755 0002166 0000144 00000000000 14571615257 015010 5 ustar 00changaco users ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1621866577.0
libarchive-c-5.1/.github/FUNDING.yml 0000644 0002166 0000144 00000000024 14052734121 016603 0 ustar 00changaco users liberapay: Changaco
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1709644463.4947991
libarchive-c-5.1/.github/workflows/ 0000755 0002166 0000144 00000000000 14571615257 017045 5 ustar 00changaco users ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1688027638.0
libarchive-c-5.1/.github/workflows/main.yml 0000644 0002166 0000144 00000001746 14447240766 020525 0 ustar 00changaco users name: CI
on:
# Trigger the workflow on push or pull request events but only for the master branch
push:
branches: [ master ]
pull_request:
branches: [ master ]
# Allow running this workflow manually from the Actions tab
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install libarchive
run: sudo apt-get install -y libarchive13
- name: Install Python 3.11
uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Install Python 3.10
uses: actions/setup-python@v2
with:
python-version: '3.10'
- name: Install Python 3.9
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install Python 3.8
uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Install tox
run: pip install tox
- name: Run the tests
run: tox
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1480442609.0
libarchive-c-5.1/.gitignore 0000644 0002166 0000144 00000000101 13017341361 015412 0 ustar 00changaco users *.egg-info/
/build/
/dist/
/env/
/htmlcov/
.coverage
*.pyc
.tox/
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1528634603.0
libarchive-c-5.1/LICENSE.md 0000644 0002166 0000144 00000000063 13307216353 015041 0 ustar 00changaco users https://creativecommons.org/publicdomain/zero/1.0/
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1480442602.0
libarchive-c-5.1/MANIFEST.in 0000644 0002166 0000144 00000000023 13017341352 015163 0 ustar 00changaco users include version.py
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1709644463.4981325
libarchive-c-5.1/PKG-INFO 0000644 0002166 0000144 00000012274 14571615257 014553 0 ustar 00changaco users Metadata-Version: 2.1
Name: libarchive-c
Version: 5.1
Summary: Python interface to libarchive
Home-page: https://github.com/Changaco/python-libarchive-c
Author: Changaco
Author-email: changaco@changaco.oy.lc
License: CC0
Keywords: archive libarchive 7z tar bz2 zip gz
Description-Content-Type: text/x-rst
License-File: LICENSE.md
A Python interface to libarchive. It uses the standard ctypes_ module to
dynamically load and access the C library.
.. _ctypes: https://docs.python.org/3/library/ctypes.html
Installation
============
pip install libarchive-c
Compatibility
=============
python
------
python-libarchive-c is currently tested with python 3.8, 3.9, 3.10 and 3.11.
If you find an incompatibility with older versions you can send us a small patch,
but we won't accept big changes.
libarchive
----------
python-libarchive-c may not work properly with obsolete versions of libarchive such as the ones included in MacOS. In that case you can install a recent version of libarchive (e.g. with ``brew install libarchive`` on MacOS) and use the ``LIBARCHIVE`` environment variable to point python-libarchive-c to it::
export LIBARCHIVE=/usr/local/Cellar/libarchive/3.3.3/lib/libarchive.13.dylib
Usage
=====
Import::
import libarchive
Extracting archives
-------------------
To extract an archive, use the ``extract_file`` function::
os.chdir('/path/to/target/directory')
libarchive.extract_file('test.zip')
Alternatively, the ``extract_memory`` function can be used to extract from a buffer,
and ``extract_fd`` from a file descriptor.
The ``extract_*`` functions all have an integer ``flags`` argument which is passed
directly to the C function ``archive_write_disk_set_options()``. You can import
the ``EXTRACT_*`` constants from the ``libarchive.extract`` module and see the
official description of each flag in the ``archive_write_disk(3)`` man page.
By default, when the ``flags`` argument is ``None``, the ``SECURE_NODOTDOT``,
``SECURE_NOABSOLUTEPATHS`` and ``SECURE_SYMLINKS`` flags are passed to
libarchive, unless the current directory is the root (``/``).
Reading archives
----------------
To read an archive, use the ``file_reader`` function::
with libarchive.file_reader('test.7z') as archive:
for entry in archive:
for block in entry.get_blocks():
...
Alternatively, the ``memory_reader`` function can be used to read from a buffer,
``fd_reader`` from a file descriptor, ``stream_reader`` from a stream object
(which must support the standard ``readinto`` method), and ``custom_reader``
from anywhere using callbacks.
To learn about the attributes of the ``entry`` object, see the ``libarchive/entry.py``
source code or run ``help(libarchive.entry.ArchiveEntry)`` in a Python shell.
Displaying progress
~~~~~~~~~~~~~~~~~~~
If your program processes large archives, you can keep track of its progress
with the ``bytes_read`` attribute. Here's an example of a progress bar using
`tqdm `_::
with tqdm(total=os.stat(archive_path).st_size, unit='bytes') as pbar, \
libarchive.file_reader(archive_path) as archive:
for entry in archive:
...
pbar.update(archive.bytes_read - pbar.n)
Creating archives
-----------------
To create an archive, use the ``file_writer`` function::
from libarchive.entry import FileType
with libarchive.file_writer('test.tar.gz', 'ustar', 'gzip') as archive:
# Add the `libarchive/` directory and everything in it (recursively),
# then the `README.rst` file.
archive.add_files('libarchive/', 'README.rst')
# Add a regular file defined from scratch.
data = b'foobar'
archive.add_file_from_memory('../escape-test', len(data), data)
# Add a directory defined from scratch.
early_epoch = (42, 42) # 1970-01-01 00:00:42.000000042
archive.add_file_from_memory(
'metadata-test', 0, b'',
filetype=FileType.DIRECTORY, permission=0o755, uid=4242, gid=4242,
atime=early_epoch, mtime=early_epoch, ctime=early_epoch, birthtime=early_epoch,
)
Alternatively, the ``memory_writer`` function can be used to write to a memory buffer,
``fd_writer`` to a file descriptor, and ``custom_writer`` to a callback function.
For each of those functions, the mandatory second argument is the archive format,
and the optional third argument is the compression format (called “filter” in
libarchive). The acceptable values are listed in ``libarchive.ffi.WRITE_FORMATS``
and ``libarchive.ffi.WRITE_FILTERS``.
File metadata codecs
--------------------
By default, UTF-8 is used to read and write file attributes from and to archives.
A different codec can be specified through the ``header_codec`` arguments of the
``*_reader`` and ``*_writer`` functions. Example::
with libarchive.file_writer('test.tar', 'ustar', header_codec='cp037') as archive:
...
with file_reader('test.tar', header_codec='cp037') as archive:
...
In addition to file paths (``pathname`` and ``linkpath``), the specified codec is
used to encode and decode user and group names (``uname`` and ``gname``).
License
=======
`CC0 Public Domain Dedication `_
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1688457938.0
libarchive-c-5.1/README.rst 0000644 0002166 0000144 00000011561 14450751322 015131 0 ustar 00changaco users A Python interface to libarchive. It uses the standard ctypes_ module to
dynamically load and access the C library.
.. _ctypes: https://docs.python.org/3/library/ctypes.html
Installation
============
pip install libarchive-c
Compatibility
=============
python
------
python-libarchive-c is currently tested with python 3.8, 3.9, 3.10 and 3.11.
If you find an incompatibility with older versions you can send us a small patch,
but we won't accept big changes.
libarchive
----------
python-libarchive-c may not work properly with obsolete versions of libarchive such as the ones included in MacOS. In that case you can install a recent version of libarchive (e.g. with ``brew install libarchive`` on MacOS) and use the ``LIBARCHIVE`` environment variable to point python-libarchive-c to it::
export LIBARCHIVE=/usr/local/Cellar/libarchive/3.3.3/lib/libarchive.13.dylib
Usage
=====
Import::
import libarchive
Extracting archives
-------------------
To extract an archive, use the ``extract_file`` function::
os.chdir('/path/to/target/directory')
libarchive.extract_file('test.zip')
Alternatively, the ``extract_memory`` function can be used to extract from a buffer,
and ``extract_fd`` from a file descriptor.
The ``extract_*`` functions all have an integer ``flags`` argument which is passed
directly to the C function ``archive_write_disk_set_options()``. You can import
the ``EXTRACT_*`` constants from the ``libarchive.extract`` module and see the
official description of each flag in the ``archive_write_disk(3)`` man page.
By default, when the ``flags`` argument is ``None``, the ``SECURE_NODOTDOT``,
``SECURE_NOABSOLUTEPATHS`` and ``SECURE_SYMLINKS`` flags are passed to
libarchive, unless the current directory is the root (``/``).
Reading archives
----------------
To read an archive, use the ``file_reader`` function::
with libarchive.file_reader('test.7z') as archive:
for entry in archive:
for block in entry.get_blocks():
...
Alternatively, the ``memory_reader`` function can be used to read from a buffer,
``fd_reader`` from a file descriptor, ``stream_reader`` from a stream object
(which must support the standard ``readinto`` method), and ``custom_reader``
from anywhere using callbacks.
To learn about the attributes of the ``entry`` object, see the ``libarchive/entry.py``
source code or run ``help(libarchive.entry.ArchiveEntry)`` in a Python shell.
Displaying progress
~~~~~~~~~~~~~~~~~~~
If your program processes large archives, you can keep track of its progress
with the ``bytes_read`` attribute. Here's an example of a progress bar using
`tqdm `_::
with tqdm(total=os.stat(archive_path).st_size, unit='bytes') as pbar, \
libarchive.file_reader(archive_path) as archive:
for entry in archive:
...
pbar.update(archive.bytes_read - pbar.n)
Creating archives
-----------------
To create an archive, use the ``file_writer`` function::
from libarchive.entry import FileType
with libarchive.file_writer('test.tar.gz', 'ustar', 'gzip') as archive:
# Add the `libarchive/` directory and everything in it (recursively),
# then the `README.rst` file.
archive.add_files('libarchive/', 'README.rst')
# Add a regular file defined from scratch.
data = b'foobar'
archive.add_file_from_memory('../escape-test', len(data), data)
# Add a directory defined from scratch.
early_epoch = (42, 42) # 1970-01-01 00:00:42.000000042
archive.add_file_from_memory(
'metadata-test', 0, b'',
filetype=FileType.DIRECTORY, permission=0o755, uid=4242, gid=4242,
atime=early_epoch, mtime=early_epoch, ctime=early_epoch, birthtime=early_epoch,
)
Alternatively, the ``memory_writer`` function can be used to write to a memory buffer,
``fd_writer`` to a file descriptor, and ``custom_writer`` to a callback function.
For each of those functions, the mandatory second argument is the archive format,
and the optional third argument is the compression format (called “filter” in
libarchive). The acceptable values are listed in ``libarchive.ffi.WRITE_FORMATS``
and ``libarchive.ffi.WRITE_FILTERS``.
File metadata codecs
--------------------
By default, UTF-8 is used to read and write file attributes from and to archives.
A different codec can be specified through the ``header_codec`` arguments of the
``*_reader`` and ``*_writer`` functions. Example::
with libarchive.file_writer('test.tar', 'ustar', header_codec='cp037') as archive:
...
with file_reader('test.tar', header_codec='cp037') as archive:
...
In addition to file paths (``pathname`` and ``linkpath``), the specified codec is
used to encode and decode user and group names (``uname`` and ``gname``).
License
=======
`CC0 Public Domain Dedication `_
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1709644463.4947991
libarchive-c-5.1/libarchive/ 0000755 0002166 0000144 00000000000 14571615257 015560 5 ustar 00changaco users ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1621868931.0
libarchive-c-5.1/libarchive/__init__.py 0000644 0002166 0000144 00000001131 14052740603 017651 0 ustar 00changaco users from .entry import ArchiveEntry
from .exception import ArchiveError
from .extract import extract_fd, extract_file, extract_memory
from .read import (
custom_reader, fd_reader, file_reader, memory_reader, stream_reader,
seekable_stream_reader
)
from .write import custom_writer, fd_writer, file_writer, memory_writer
__all__ = [x.__name__ for x in (
ArchiveEntry,
ArchiveError,
extract_fd, extract_file, extract_memory,
custom_reader, fd_reader, file_reader, memory_reader, stream_reader,
seekable_stream_reader,
custom_writer, fd_writer, file_writer, memory_writer
)]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1709644414.0
libarchive-c-5.1/libarchive/entry.py 0000644 0002166 0000144 00000034144 14571615176 017301 0 ustar 00changaco users from contextlib import contextmanager
from ctypes import create_string_buffer
from enum import IntEnum
import math
from . import ffi
class FileType(IntEnum):
NAMED_PIPE = AE_IFIFO = 0o010000 # noqa: E221
CHAR_DEVICE = AE_IFCHR = 0o020000 # noqa: E221
DIRECTORY = AE_IFDIR = 0o040000 # noqa: E221
BLOCK_DEVICE = AE_IFBLK = 0o060000 # noqa: E221
REGULAR_FILE = AE_IFREG = 0o100000 # noqa: E221
SYMBOLINK_LINK = AE_IFLNK = 0o120000 # noqa: E221
SOCKET = AE_IFSOCK = 0o140000 # noqa: E221
@contextmanager
def new_archive_entry():
entry_p = ffi.entry_new()
try:
yield entry_p
finally:
ffi.entry_free(entry_p)
def format_time(seconds, nanos):
""" return float of seconds.nanos when nanos set, or seconds when not """
if nanos:
return float(seconds) + float(nanos) / 1000000000.0
return int(seconds)
class ArchiveEntry:
__slots__ = ('_archive_p', '_entry_p', 'header_codec')
def __init__(self, archive_p=None, header_codec='utf-8', **attributes):
"""Allocate memory for an `archive_entry` struct.
The `header_codec` is used to decode and encode file paths and other
attributes.
The `**attributes` are passed to the `modify` method.
"""
self._archive_p = archive_p
self._entry_p = ffi.entry_new()
self.header_codec = header_codec
if attributes:
self.modify(**attributes)
def __del__(self):
"""Free the C struct"""
ffi.entry_free(self._entry_p)
def __str__(self):
"""Returns the file's path"""
return self.pathname
def modify(self, header_codec=None, **attributes):
"""Convenience method to modify the entry's attributes.
Args:
filetype (int): the file's type, see the `FileType` class for values
pathname (str): the file's path
linkpath (str): the other path of the file, if the file is a link
size (int | None): the file's size, in bytes
perm (int): the file's permissions in standard Unix format, e.g. 0o640
uid (int): the file owner's numerical identifier
gid (int): the file group's numerical identifier
uname (str | bytes): the file owner's name
gname (str | bytes): the file group's name
atime (int | Tuple[int, int] | float | None):
the file's most recent access time,
either in seconds or as a tuple (seconds, nanoseconds)
mtime (int | Tuple[int, int] | float | None):
the file's most recent modification time,
either in seconds or as a tuple (seconds, nanoseconds)
ctime (int | Tuple[int, int] | float | None):
the file's most recent metadata change time,
either in seconds or as a tuple (seconds, nanoseconds)
birthtime (int | Tuple[int, int] | float | None):
the file's creation time (for archive formats that support it),
either in seconds or as a tuple (seconds, nanoseconds)
rdev (int | Tuple[int, int]): device number, if the file is a device
rdevmajor (int): major part of the device number
rdevminor (int): minor part of the device number
"""
if header_codec:
self.header_codec = header_codec
for name, value in attributes.items():
setattr(self, name, value)
@property
def filetype(self):
return ffi.entry_filetype(self._entry_p)
@filetype.setter
def filetype(self, value):
ffi.entry_set_filetype(self._entry_p, value)
@property
def uid(self):
return ffi.entry_uid(self._entry_p)
@uid.setter
def uid(self, uid):
ffi.entry_set_uid(self._entry_p, uid)
@property
def gid(self):
return ffi.entry_gid(self._entry_p)
@gid.setter
def gid(self, gid):
ffi.entry_set_gid(self._entry_p, gid)
@property
def uname(self):
uname = ffi.entry_uname_w(self._entry_p)
if not uname:
uname = ffi.entry_uname(self._entry_p)
if uname is not None:
try:
uname = uname.decode(self.header_codec)
except UnicodeError:
pass
return uname
@uname.setter
def uname(self, value):
if not isinstance(value, bytes):
value = value.encode(self.header_codec)
if self.header_codec == 'utf-8':
ffi.entry_update_uname_utf8(self._entry_p, value)
else:
ffi.entry_copy_uname(self._entry_p, value)
@property
def gname(self):
gname = ffi.entry_gname_w(self._entry_p)
if not gname:
gname = ffi.entry_gname(self._entry_p)
if gname is not None:
try:
gname = gname.decode(self.header_codec)
except UnicodeError:
pass
return gname
@gname.setter
def gname(self, value):
if not isinstance(value, bytes):
value = value.encode(self.header_codec)
if self.header_codec == 'utf-8':
ffi.entry_update_gname_utf8(self._entry_p, value)
else:
ffi.entry_copy_gname(self._entry_p, value)
def get_blocks(self, block_size=ffi.page_size):
"""Read the file's content, keeping only one chunk in memory at a time.
Don't do anything like `list(entry.get_blocks())`, it would silently fail.
Args:
block_size (int): the buffer's size, in bytes
"""
archive_p = self._archive_p
if not archive_p:
raise TypeError("this entry isn't linked to any content")
buf = create_string_buffer(block_size)
read = ffi.read_data
while 1:
r = read(archive_p, buf, block_size)
if r == 0:
break
yield buf.raw[0:r]
self.__class__ = ConsumedArchiveEntry
@property
def isblk(self):
return self.filetype & 0o170000 == 0o060000
@property
def ischr(self):
return self.filetype & 0o170000 == 0o020000
@property
def isdir(self):
return self.filetype & 0o170000 == 0o040000
@property
def isfifo(self):
return self.filetype & 0o170000 == 0o010000
@property
def islnk(self):
return bool(ffi.entry_hardlink_w(self._entry_p) or
ffi.entry_hardlink(self._entry_p))
@property
def issym(self):
return self.filetype & 0o170000 == 0o120000
@property
def isreg(self):
return self.filetype & 0o170000 == 0o100000
@property
def isfile(self):
return self.isreg
@property
def issock(self):
return self.filetype & 0o170000 == 0o140000
@property
def isdev(self):
return self.ischr or self.isblk or self.isfifo or self.issock
@property
def atime(self):
if not ffi.entry_atime_is_set(self._entry_p):
return None
sec_val = ffi.entry_atime(self._entry_p)
nsec_val = ffi.entry_atime_nsec(self._entry_p)
return format_time(sec_val, nsec_val)
@atime.setter
def atime(self, value):
if value is None:
ffi.entry_unset_atime(self._entry_p)
elif isinstance(value, int):
self.set_atime(value)
elif isinstance(value, tuple):
self.set_atime(*value)
else:
seconds, fraction = math.modf(value)
self.set_atime(int(seconds), int(fraction * 1_000_000_000))
def set_atime(self, timestamp_sec, timestamp_nsec=0):
"Kept for backward compatibility. `entry.atime = ...` is supported now."
return ffi.entry_set_atime(self._entry_p, timestamp_sec, timestamp_nsec)
@property
def mtime(self):
if not ffi.entry_mtime_is_set(self._entry_p):
return None
sec_val = ffi.entry_mtime(self._entry_p)
nsec_val = ffi.entry_mtime_nsec(self._entry_p)
return format_time(sec_val, nsec_val)
@mtime.setter
def mtime(self, value):
if value is None:
ffi.entry_unset_mtime(self._entry_p)
elif isinstance(value, int):
self.set_mtime(value)
elif isinstance(value, tuple):
self.set_mtime(*value)
else:
seconds, fraction = math.modf(value)
self.set_mtime(int(seconds), int(fraction * 1_000_000_000))
def set_mtime(self, timestamp_sec, timestamp_nsec=0):
"Kept for backward compatibility. `entry.mtime = ...` is supported now."
return ffi.entry_set_mtime(self._entry_p, timestamp_sec, timestamp_nsec)
@property
def ctime(self):
if not ffi.entry_ctime_is_set(self._entry_p):
return None
sec_val = ffi.entry_ctime(self._entry_p)
nsec_val = ffi.entry_ctime_nsec(self._entry_p)
return format_time(sec_val, nsec_val)
@ctime.setter
def ctime(self, value):
if value is None:
ffi.entry_unset_ctime(self._entry_p)
elif isinstance(value, int):
self.set_ctime(value)
elif isinstance(value, tuple):
self.set_ctime(*value)
else:
seconds, fraction = math.modf(value)
self.set_ctime(int(seconds), int(fraction * 1_000_000_000))
def set_ctime(self, timestamp_sec, timestamp_nsec=0):
"Kept for backward compatibility. `entry.ctime = ...` is supported now."
return ffi.entry_set_ctime(self._entry_p, timestamp_sec, timestamp_nsec)
@property
def birthtime(self):
if not ffi.entry_birthtime_is_set(self._entry_p):
return None
sec_val = ffi.entry_birthtime(self._entry_p)
nsec_val = ffi.entry_birthtime_nsec(self._entry_p)
return format_time(sec_val, nsec_val)
@birthtime.setter
def birthtime(self, value):
if value is None:
ffi.entry_unset_birthtime(self._entry_p)
elif isinstance(value, int):
self.set_birthtime(value)
elif isinstance(value, tuple):
self.set_birthtime(*value)
else:
seconds, fraction = math.modf(value)
self.set_birthtime(int(seconds), int(fraction * 1_000_000_000))
def set_birthtime(self, timestamp_sec, timestamp_nsec=0):
"Kept for backward compatibility. `entry.birthtime = ...` is supported now."
return ffi.entry_set_birthtime(
self._entry_p, timestamp_sec, timestamp_nsec
)
@property
def pathname(self):
path = ffi.entry_pathname_w(self._entry_p)
if not path:
path = ffi.entry_pathname(self._entry_p)
if path is not None:
try:
path = path.decode(self.header_codec)
except UnicodeError:
pass
return path
@pathname.setter
def pathname(self, value):
if not isinstance(value, bytes):
value = value.encode(self.header_codec)
if self.header_codec == 'utf-8':
ffi.entry_update_pathname_utf8(self._entry_p, value)
else:
ffi.entry_copy_pathname(self._entry_p, value)
@property
def linkpath(self):
path = (
(
ffi.entry_symlink_w(self._entry_p) or
ffi.entry_symlink(self._entry_p)
) if self.issym else (
ffi.entry_hardlink_w(self._entry_p) or
ffi.entry_hardlink(self._entry_p)
)
)
if isinstance(path, bytes):
try:
path = path.decode(self.header_codec)
except UnicodeError:
pass
return path
@linkpath.setter
def linkpath(self, value):
if not isinstance(value, bytes):
value = value.encode(self.header_codec)
if self.header_codec == 'utf-8':
ffi.entry_update_link_utf8(self._entry_p, value)
else:
ffi.entry_copy_link(self._entry_p, value)
# aliases for compatibility with the standard `tarfile` module
path = property(pathname.fget, pathname.fset, doc="alias of pathname")
name = path
linkname = property(linkpath.fget, linkpath.fset, doc="alias of linkpath")
@property
def size(self):
if ffi.entry_size_is_set(self._entry_p):
return ffi.entry_size(self._entry_p)
@size.setter
def size(self, value):
if value is None:
ffi.entry_unset_size(self._entry_p)
else:
ffi.entry_set_size(self._entry_p, value)
@property
def mode(self):
return ffi.entry_mode(self._entry_p)
@mode.setter
def mode(self, value):
ffi.entry_set_mode(self._entry_p, value)
@property
def strmode(self):
"""The file's mode as a string, e.g. '?rwxrwx---'"""
# note we strip the mode because archive_entry_strmode
# returns a trailing space: strcpy(bp, "?rwxrwxrwx ");
return ffi.entry_strmode(self._entry_p).strip()
@property
def perm(self):
return ffi.entry_perm(self._entry_p)
@perm.setter
def perm(self, value):
ffi.entry_set_perm(self._entry_p, value)
@property
def rdev(self):
return ffi.entry_rdev(self._entry_p)
@rdev.setter
def rdev(self, value):
if isinstance(value, tuple):
ffi.entry_set_rdevmajor(self._entry_p, value[0])
ffi.entry_set_rdevminor(self._entry_p, value[1])
else:
ffi.entry_set_rdev(self._entry_p, value)
@property
def rdevmajor(self):
return ffi.entry_rdevmajor(self._entry_p)
@rdevmajor.setter
def rdevmajor(self, value):
ffi.entry_set_rdevmajor(self._entry_p, value)
@property
def rdevminor(self):
return ffi.entry_rdevminor(self._entry_p)
@rdevminor.setter
def rdevminor(self, value):
ffi.entry_set_rdevminor(self._entry_p, value)
class ConsumedArchiveEntry(ArchiveEntry):
__slots__ = ()
def get_blocks(self, **kw):
raise TypeError("the content of this entry has already been read")
class PassedArchiveEntry(ArchiveEntry):
__slots__ = ()
def get_blocks(self, **kw):
raise TypeError("this entry is passed, it's too late to read its content")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1621868945.0
libarchive-c-5.1/libarchive/exception.py 0000644 0002166 0000144 00000000562 14052740621 020117 0 ustar 00changaco users
class ArchiveError(Exception):
def __init__(self, msg, errno=None, retcode=None, archive_p=None):
self.msg = msg
self.errno = errno
self.retcode = retcode
self.archive_p = archive_p
def __str__(self):
p = '%s (errno=%s, retcode=%s, archive_p=%s)'
return p % (self.msg, self.errno, self.retcode, self.archive_p)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1642872498.0
libarchive-c-5.1/libarchive/extract.py 0000644 0002166 0000144 00000005206 14173037262 017577 0 ustar 00changaco users from contextlib import contextmanager
from ctypes import byref, c_longlong, c_size_t, c_void_p
import os
from .ffi import (
write_disk_new, write_disk_set_options, write_free, write_header,
read_data_block, write_data_block, write_finish_entry, ARCHIVE_EOF
)
from .read import fd_reader, file_reader, memory_reader
EXTRACT_OWNER = 0x0001
EXTRACT_PERM = 0x0002
EXTRACT_TIME = 0x0004
EXTRACT_NO_OVERWRITE = 0x0008
EXTRACT_UNLINK = 0x0010
EXTRACT_ACL = 0x0020
EXTRACT_FFLAGS = 0x0040
EXTRACT_XATTR = 0x0080
EXTRACT_SECURE_SYMLINKS = 0x0100
EXTRACT_SECURE_NODOTDOT = 0x0200
EXTRACT_NO_AUTODIR = 0x0400
EXTRACT_NO_OVERWRITE_NEWER = 0x0800
EXTRACT_SPARSE = 0x1000
EXTRACT_MAC_METADATA = 0x2000
EXTRACT_NO_HFS_COMPRESSION = 0x4000
EXTRACT_HFS_COMPRESSION_FORCED = 0x8000
EXTRACT_SECURE_NOABSOLUTEPATHS = 0x10000
EXTRACT_CLEAR_NOCHANGE_FFLAGS = 0x20000
PREVENT_ESCAPE = (
EXTRACT_SECURE_NOABSOLUTEPATHS |
EXTRACT_SECURE_NODOTDOT |
EXTRACT_SECURE_SYMLINKS
)
@contextmanager
def new_archive_write_disk(flags):
archive_p = write_disk_new()
write_disk_set_options(archive_p, flags)
try:
yield archive_p
finally:
write_free(archive_p)
def extract_entries(entries, flags=None):
"""Extracts the given archive entries into the current directory.
"""
if flags is None:
if os.getcwd() == '/':
# If the current directory is the root, then trying to prevent
# escaping is probably undesirable.
flags = 0
else:
flags = PREVENT_ESCAPE
buff, size, offset = c_void_p(), c_size_t(), c_longlong()
buff_p, size_p, offset_p = byref(buff), byref(size), byref(offset)
with new_archive_write_disk(flags) as write_p:
for entry in entries:
write_header(write_p, entry._entry_p)
read_p = entry._archive_p
while 1:
r = read_data_block(read_p, buff_p, size_p, offset_p)
if r == ARCHIVE_EOF:
break
write_data_block(write_p, buff, size, offset)
write_finish_entry(write_p)
def extract_fd(fd, flags=None):
"""Extracts an archive from a file descriptor into the current directory.
"""
with fd_reader(fd) as archive:
extract_entries(archive, flags)
def extract_file(filepath, flags=None):
"""Extracts an archive from a file into the current directory."""
with file_reader(filepath) as archive:
extract_entries(archive, flags)
def extract_memory(buffer_, flags=None):
"""Extracts an archive from memory into the current directory."""
with memory_reader(buffer_) as archive:
extract_entries(archive, flags)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1688457938.0
libarchive-c-5.1/libarchive/ffi.py 0000644 0002166 0000144 00000030461 14450751322 016670 0 ustar 00changaco users from ctypes import (
c_char_p, c_int, c_uint, c_long, c_longlong, c_size_t, c_int64,
c_void_p, c_wchar_p, CFUNCTYPE, POINTER,
)
try:
from ctypes import c_ssize_t
except ImportError:
from ctypes import c_longlong as c_ssize_t
import ctypes
from ctypes.util import find_library
import logging
import mmap
import os
import sysconfig
from .exception import ArchiveError
logger = logging.getLogger('libarchive')
page_size = mmap.PAGESIZE
libarchive_path = os.environ.get('LIBARCHIVE') or find_library('archive')
libarchive = ctypes.cdll.LoadLibrary(libarchive_path)
# Constants
ARCHIVE_EOF = 1 # Found end of archive.
ARCHIVE_OK = 0 # Operation was successful.
ARCHIVE_RETRY = -10 # Retry might succeed.
ARCHIVE_WARN = -20 # Partial success.
ARCHIVE_FAILED = -25 # Current operation cannot complete.
ARCHIVE_FATAL = -30 # No more operations are possible.
# Callback types
WRITE_CALLBACK = CFUNCTYPE(
c_ssize_t, c_void_p, c_void_p, POINTER(c_void_p), c_size_t
)
READ_CALLBACK = CFUNCTYPE(
c_ssize_t, c_void_p, c_void_p, POINTER(c_void_p)
)
SEEK_CALLBACK = CFUNCTYPE(
c_longlong, c_void_p, c_void_p, c_longlong, c_int
)
OPEN_CALLBACK = CFUNCTYPE(c_int, c_void_p, c_void_p)
CLOSE_CALLBACK = CFUNCTYPE(c_int, c_void_p, c_void_p)
NO_OPEN_CB = ctypes.cast(None, OPEN_CALLBACK)
NO_CLOSE_CB = ctypes.cast(None, CLOSE_CALLBACK)
# Type aliases, for readability
c_archive_p = c_void_p
c_archive_entry_p = c_void_p
if sysconfig.get_config_var('SIZEOF_TIME_T') == 8:
c_time_t = c_int64
else:
c_time_t = c_long
# Helper functions
def _error_string(archive_p):
msg = error_string(archive_p)
if msg is None:
return
try:
return msg.decode('ascii')
except UnicodeDecodeError:
return msg
def archive_error(archive_p, retcode):
msg = _error_string(archive_p)
return ArchiveError(msg, errno(archive_p), retcode, archive_p)
def check_null(ret, func, args):
if ret is None:
raise ArchiveError(func.__name__+' returned NULL')
return ret
def check_int(retcode, func, args):
if retcode >= 0:
return retcode
elif retcode == ARCHIVE_WARN:
logger.warning(_error_string(args[0]))
return retcode
else:
raise archive_error(args[0], retcode)
def ffi(name, argtypes, restype, errcheck=None):
f = getattr(libarchive, 'archive_'+name)
f.argtypes = argtypes
f.restype = restype
if errcheck:
f.errcheck = errcheck
globals()[name] = f
return f
def get_read_format_function(format_name):
function_name = 'read_support_format_' + format_name
func = globals().get(function_name)
if func:
return func
try:
return ffi(function_name, [c_archive_p], c_int, check_int)
except AttributeError:
raise ValueError('the read format %r is not available' % format_name)
def get_read_filter_function(filter_name):
function_name = 'read_support_filter_' + filter_name
func = globals().get(function_name)
if func:
return func
try:
return ffi(function_name, [c_archive_p], c_int, check_int)
except AttributeError:
raise ValueError('the read filter %r is not available' % filter_name)
def get_write_format_function(format_name):
function_name = 'write_set_format_' + format_name
func = globals().get(function_name)
if func:
return func
try:
return ffi(function_name, [c_archive_p], c_int, check_int)
except AttributeError:
raise ValueError('the write format %r is not available' % format_name)
def get_write_filter_function(filter_name):
function_name = 'write_add_filter_' + filter_name
func = globals().get(function_name)
if func:
return func
try:
return ffi(function_name, [c_archive_p], c_int, check_int)
except AttributeError:
raise ValueError('the write filter %r is not available' % 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)
error_string = ffi('error_string', [c_archive_p], c_char_p)
ffi('filter_bytes', [c_archive_p, c_int], c_longlong)
ffi('filter_count', [c_archive_p], c_int)
ffi('filter_name', [c_archive_p, c_int], c_char_p)
ffi('format_name', [c_archive_p], c_char_p)
# archive_entry
ffi('entry_new', [], c_archive_entry_p, check_null)
ffi('entry_filetype', [c_archive_entry_p], c_int)
ffi('entry_atime', [c_archive_entry_p], c_time_t)
ffi('entry_birthtime', [c_archive_entry_p], c_time_t)
ffi('entry_mtime', [c_archive_entry_p], c_time_t)
ffi('entry_ctime', [c_archive_entry_p], c_time_t)
ffi('entry_atime_nsec', [c_archive_entry_p], c_long)
ffi('entry_birthtime_nsec', [c_archive_entry_p], c_long)
ffi('entry_mtime_nsec', [c_archive_entry_p], c_long)
ffi('entry_ctime_nsec', [c_archive_entry_p], c_long)
ffi('entry_atime_is_set', [c_archive_entry_p], c_int)
ffi('entry_birthtime_is_set', [c_archive_entry_p], c_int)
ffi('entry_mtime_is_set', [c_archive_entry_p], c_int)
ffi('entry_ctime_is_set', [c_archive_entry_p], c_int)
ffi('entry_pathname', [c_archive_entry_p], c_char_p)
ffi('entry_pathname_w', [c_archive_entry_p], c_wchar_p)
ffi('entry_sourcepath', [c_archive_entry_p], c_char_p)
ffi('entry_size', [c_archive_entry_p], c_longlong)
ffi('entry_size_is_set', [c_archive_entry_p], c_int)
ffi('entry_mode', [c_archive_entry_p], c_int)
ffi('entry_strmode', [c_archive_entry_p], c_char_p)
ffi('entry_perm', [c_archive_entry_p], c_int)
ffi('entry_hardlink', [c_archive_entry_p], c_char_p)
ffi('entry_hardlink_w', [c_archive_entry_p], c_wchar_p)
ffi('entry_symlink', [c_archive_entry_p], c_char_p)
ffi('entry_symlink_w', [c_archive_entry_p], c_wchar_p)
ffi('entry_rdev', [c_archive_entry_p], c_uint)
ffi('entry_rdevmajor', [c_archive_entry_p], c_uint)
ffi('entry_rdevminor', [c_archive_entry_p], c_uint)
ffi('entry_uid', [c_archive_entry_p], c_longlong)
ffi('entry_gid', [c_archive_entry_p], c_longlong)
ffi('entry_uname', [c_archive_entry_p], c_char_p)
ffi('entry_gname', [c_archive_entry_p], c_char_p)
ffi('entry_uname_w', [c_archive_entry_p], c_wchar_p)
ffi('entry_gname_w', [c_archive_entry_p], c_wchar_p)
ffi('entry_set_size', [c_archive_entry_p, c_longlong], None)
ffi('entry_set_filetype', [c_archive_entry_p, c_uint], None)
ffi('entry_set_uid', [c_archive_entry_p, c_longlong], None)
ffi('entry_set_gid', [c_archive_entry_p, c_longlong], None)
ffi('entry_set_mode', [c_archive_entry_p, c_int], None)
ffi('entry_set_perm', [c_archive_entry_p, c_int], None)
ffi('entry_set_atime', [c_archive_entry_p, c_time_t, c_long], None)
ffi('entry_set_mtime', [c_archive_entry_p, c_time_t, c_long], None)
ffi('entry_set_ctime', [c_archive_entry_p, c_time_t, c_long], None)
ffi('entry_set_birthtime', [c_archive_entry_p, c_time_t, c_long], None)
ffi('entry_set_rdev', [c_archive_entry_p, c_uint], None)
ffi('entry_set_rdevmajor', [c_archive_entry_p, c_uint], None)
ffi('entry_set_rdevminor', [c_archive_entry_p, c_uint], None)
ffi('entry_unset_size', [c_archive_entry_p], None)
ffi('entry_unset_atime', [c_archive_entry_p], None)
ffi('entry_unset_mtime', [c_archive_entry_p], None)
ffi('entry_unset_ctime', [c_archive_entry_p], None)
ffi('entry_unset_birthtime', [c_archive_entry_p], None)
ffi('entry_copy_pathname', [c_archive_entry_p, c_char_p], None)
ffi('entry_update_pathname_utf8', [c_archive_entry_p, c_char_p], c_int, check_int)
ffi('entry_copy_link', [c_archive_entry_p, c_char_p], None)
ffi('entry_update_link_utf8', [c_archive_entry_p, c_char_p], c_int, check_int)
ffi('entry_copy_uname', [c_archive_entry_p, c_char_p], None)
ffi('entry_update_uname_utf8', [c_archive_entry_p, c_char_p], c_int, check_int)
ffi('entry_copy_gname', [c_archive_entry_p, c_char_p], None)
ffi('entry_update_gname_utf8', [c_archive_entry_p, c_char_p], c_int, check_int)
ffi('entry_clear', [c_archive_entry_p], c_archive_entry_p)
ffi('entry_free', [c_archive_entry_p], None)
# archive_read
ffi('read_new', [], c_archive_p, check_null)
READ_FORMATS = set((
'7zip', 'all', 'ar', 'cab', 'cpio', 'empty', 'iso9660', 'lha', 'mtree',
'rar', 'raw', 'tar', 'xar', 'zip', 'warc'
))
for f_name in list(READ_FORMATS):
try:
get_read_format_function(f_name)
except ValueError as e: # pragma: no cover
logger.info(str(e))
READ_FORMATS.remove(f_name)
READ_FILTERS = set((
'all', 'bzip2', 'compress', 'grzip', 'gzip', 'lrzip', 'lzip', 'lzma',
'lzop', 'none', 'rpm', 'uu', 'xz', 'lz4', 'zstd'
))
for f_name in list(READ_FILTERS):
try:
get_read_filter_function(f_name)
except ValueError as e: # pragma: no cover
logger.info(str(e))
READ_FILTERS.remove(f_name)
ffi('read_set_seek_callback', [c_archive_p, SEEK_CALLBACK], c_int, check_int)
ffi('read_open',
[c_archive_p, c_void_p, OPEN_CALLBACK, READ_CALLBACK, CLOSE_CALLBACK],
c_int, check_int)
ffi('read_open_fd', [c_archive_p, c_int, c_size_t], c_int, check_int)
ffi('read_open_filename_w', [c_archive_p, c_wchar_p, c_size_t],
c_int, check_int)
ffi('read_open_memory', [c_archive_p, c_void_p, c_size_t], c_int, check_int)
ffi('read_next_header', [c_archive_p, POINTER(c_void_p)], c_int, check_int)
ffi('read_next_header2', [c_archive_p, c_void_p], c_int, check_int)
ffi('read_close', [c_archive_p], c_int, check_int)
ffi('read_free', [c_archive_p], c_int, check_int)
# archive_read_disk
ffi('read_disk_new', [], c_archive_p, check_null)
ffi('read_disk_set_behavior', [c_archive_p, c_int], c_int, check_int)
ffi('read_disk_set_standard_lookup', [c_archive_p], c_int, check_int)
ffi('read_disk_open', [c_archive_p, c_char_p], c_int, check_int)
ffi('read_disk_open_w', [c_archive_p, c_wchar_p], c_int, check_int)
ffi('read_disk_descend', [c_archive_p], c_int, check_int)
# archive_read_data
ffi('read_data_block',
[c_archive_p, POINTER(c_void_p), POINTER(c_size_t), POINTER(c_longlong)],
c_int, check_int)
ffi('read_data', [c_archive_p, c_void_p, c_size_t], c_ssize_t, check_int)
ffi('read_data_skip', [c_archive_p], c_int, check_int)
# archive_write
ffi('write_new', [], c_archive_p, check_null)
ffi('write_set_options', [c_archive_p, c_char_p], c_int, check_int)
ffi('write_disk_new', [], c_archive_p, check_null)
ffi('write_disk_set_options', [c_archive_p, c_int], c_int, check_int)
WRITE_FORMATS = set((
'7zip', 'ar_bsd', 'ar_svr4', 'cpio', 'cpio_newc', 'gnutar', 'iso9660',
'mtree', 'mtree_classic', 'pax', 'pax_restricted', 'shar', 'shar_dump',
'ustar', 'v7tar', 'xar', 'zip', 'warc'
))
for f_name in list(WRITE_FORMATS):
try:
get_write_format_function(f_name)
except ValueError as e: # pragma: no cover
logger.info(str(e))
WRITE_FORMATS.remove(f_name)
WRITE_FILTERS = set((
'b64encode', 'bzip2', 'compress', 'grzip', 'gzip', 'lrzip', 'lzip', 'lzma',
'lzop', 'uuencode', 'xz', 'lz4', 'zstd'
))
for f_name in list(WRITE_FILTERS):
try:
get_write_filter_function(f_name)
except ValueError as e: # pragma: no cover
logger.info(str(e))
WRITE_FILTERS.remove(f_name)
ffi('write_open',
[c_archive_p, c_void_p, OPEN_CALLBACK, WRITE_CALLBACK, CLOSE_CALLBACK],
c_int, check_int)
ffi('write_open_fd', [c_archive_p, c_int], c_int, check_int)
ffi('write_open_filename', [c_archive_p, c_char_p], c_int, check_int)
ffi('write_open_filename_w', [c_archive_p, c_wchar_p], c_int, check_int)
ffi('write_open_memory',
[c_archive_p, c_void_p, c_size_t, POINTER(c_size_t)],
c_int, check_int)
ffi('write_get_bytes_in_last_block', [c_archive_p], c_int, check_int)
ffi('write_get_bytes_per_block', [c_archive_p], c_int, check_int)
ffi('write_set_bytes_in_last_block', [c_archive_p, c_int], c_int, check_int)
ffi('write_set_bytes_per_block', [c_archive_p, c_int], c_int, check_int)
ffi('write_header', [c_archive_p, c_void_p], c_int, check_int)
ffi('write_data', [c_archive_p, c_void_p, c_size_t], c_ssize_t, check_int)
ffi('write_data_block', [c_archive_p, c_void_p, c_size_t, c_longlong],
c_int, check_int)
ffi('write_finish_entry', [c_archive_p], c_int, check_int)
ffi('write_fail', [c_archive_p], c_int, check_int)
ffi('write_close', [c_archive_p], c_int, check_int)
ffi('write_free', [c_archive_p], 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 {libarchive_path}) doesn't support encryption"
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1528637234.0
libarchive-c-5.1/libarchive/flags.py 0000644 0002166 0000144 00000000323 13307223462 017211 0 ustar 00changaco users READDISK_RESTORE_ATIME = 0x0001
READDISK_HONOR_NODUMP = 0x0002
READDISK_MAC_COPYFILE = 0x0004
READDISK_NO_TRAVERSE_MOUNTS = 0x0008
READDISK_NO_XATTR = 0x0010
READDISK_NO_ACL = 0x0020
READDISK_NO_FFLAGS = 0x0040
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1709644414.0
libarchive-c-5.1/libarchive/read.py 0000644 0002166 0000144 00000013443 14571615176 017052 0 ustar 00changaco users from contextlib import contextmanager
from ctypes import cast, c_void_p, POINTER, create_string_buffer
from os import fstat, stat
from . import ffi
from .ffi import (
ARCHIVE_EOF, OPEN_CALLBACK, READ_CALLBACK, CLOSE_CALLBACK, SEEK_CALLBACK,
NO_OPEN_CB, NO_CLOSE_CB, page_size,
)
from .entry import ArchiveEntry, PassedArchiveEntry
class ArchiveRead:
def __init__(self, archive_p, header_codec='utf-8'):
self._pointer = archive_p
self.header_codec = header_codec
def __iter__(self):
"""Iterates through an archive's entries.
"""
archive_p = self._pointer
header_codec = self.header_codec
read_next_header2 = ffi.read_next_header2
while 1:
entry = ArchiveEntry(archive_p, header_codec)
r = read_next_header2(archive_p, entry._entry_p)
if r == ARCHIVE_EOF:
return
yield entry
entry.__class__ = PassedArchiveEntry
@property
def bytes_read(self):
return ffi.filter_bytes(self._pointer, -1)
@property
def filter_names(self):
count = ffi.filter_count(self._pointer)
return [ffi.filter_name(self._pointer, i) for i in range(count - 1)]
@property
def format_name(self):
return ffi.format_name(self._pointer)
@contextmanager
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
finally:
ffi.read_free(archive_p)
@contextmanager
def custom_reader(
read_func, format_name='all', filter_name='all',
open_func=None, seek_func=None, close_func=None,
block_size=page_size, archive_read_class=ArchiveRead, passphrase=None,
header_codec='utf-8',
):
"""Read an archive using a custom function.
"""
open_cb = OPEN_CALLBACK(open_func) if open_func else NO_OPEN_CB
read_cb = READ_CALLBACK(read_func)
close_cb = CLOSE_CALLBACK(close_func) if close_func else NO_CLOSE_CB
seek_cb = SEEK_CALLBACK(seek_func)
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
if seek_func:
ffi.read_set_seek_callback(archive_p, seek_cb)
ffi.read_open(archive_p, None, open_cb, read_cb, close_cb)
yield archive_read_class(archive_p, header_codec)
@contextmanager
def fd_reader(
fd, format_name='all', filter_name='all', block_size=4096, passphrase=None,
header_codec='utf-8',
):
"""Read an archive from a file descriptor.
"""
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
pass
ffi.read_open_fd(archive_p, fd, block_size)
yield ArchiveRead(archive_p, header_codec)
@contextmanager
def file_reader(
path, format_name='all', filter_name='all', block_size=4096, passphrase=None,
header_codec='utf-8',
):
"""Read an archive from a file.
"""
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
pass
ffi.read_open_filename_w(archive_p, path, block_size)
yield ArchiveRead(archive_p, header_codec)
@contextmanager
def memory_reader(
buf, format_name='all', filter_name='all', passphrase=None,
header_codec='utf-8',
):
"""Read an archive from memory.
"""
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, header_codec)
@contextmanager
def stream_reader(
stream, format_name='all', filter_name='all', block_size=page_size,
passphrase=None, header_codec='utf-8',
):
"""Read an archive from a stream.
The `stream` object must support the standard `readinto` method.
If `stream.seekable()` returns `True`, then an appropriate seek callback is
passed to libarchive.
"""
buf = create_string_buffer(block_size)
buf_p = cast(buf, c_void_p)
def read_func(archive_p, context, ptrptr):
# readinto the buffer, returns number of bytes read
length = stream.readinto(buf)
# write the address of the buffer into the pointer
ptrptr = cast(ptrptr, POINTER(c_void_p))
ptrptr[0] = buf_p
# tell libarchive how much data was written into the buffer
return length
def seek_func(archive_p, context, offset, whence):
stream.seek(offset, whence)
# tell libarchive the current position
return stream.tell()
open_cb = NO_OPEN_CB
read_cb = READ_CALLBACK(read_func)
close_cb = NO_CLOSE_CB
seek_cb = SEEK_CALLBACK(seek_func)
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
if stream.seekable():
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, header_codec)
seekable_stream_reader = stream_reader
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1688457938.0
libarchive-c-5.1/libarchive/write.py 0000644 0002166 0000144 00000024314 14450751322 017256 0 ustar 00changaco users from contextlib import contextmanager
from ctypes import byref, cast, c_char, c_size_t, c_void_p, POINTER
from posixpath import join
import warnings
from . import ffi
from .entry import ArchiveEntry, FileType
from .ffi import (
OPEN_CALLBACK, WRITE_CALLBACK, CLOSE_CALLBACK, NO_OPEN_CB, NO_CLOSE_CB,
ARCHIVE_EOF,
page_size, entry_sourcepath, entry_clear, read_disk_new, read_disk_open_w,
read_next_header2, read_disk_descend, read_free, write_header, write_data,
write_finish_entry,
read_disk_set_behavior
)
@contextmanager
def new_archive_read_disk(path, flags=0, lookup=False):
archive_p = read_disk_new()
read_disk_set_behavior(archive_p, flags)
if lookup:
ffi.read_disk_set_standard_lookup(archive_p)
read_disk_open_w(archive_p, path)
try:
yield archive_p
finally:
read_free(archive_p)
class ArchiveWrite:
def __init__(self, archive_p, header_codec='utf-8'):
self._pointer = archive_p
self.header_codec = header_codec
def add_entries(self, entries):
"""Add the given entries to the archive.
"""
write_p = self._pointer
for entry in entries:
write_header(write_p, entry._entry_p)
for block in entry.get_blocks():
write_data(write_p, block, len(block))
write_finish_entry(write_p)
def add_files(
self, *paths, flags=0, lookup=False, pathname=None, recursive=True,
**attributes
):
"""Read files through the OS and add them to the archive.
Args:
paths (str): the paths of the files to add to the archive
flags (int):
passed to the C function `archive_read_disk_set_behavior`;
use the `libarchive.flags.READDISK_*` constants
lookup (bool):
when True, the C function `archive_read_disk_set_standard_lookup`
is called to enable the lookup of user and group names
pathname (str | None):
the path of the file in the archive, defaults to the source path
recursive (bool):
when False, if a path in `paths` is a directory,
only the directory itself is added.
attributes (dict): passed to `ArchiveEntry.modify()`
Raises:
ArchiveError: if a file doesn't exist or can't be accessed, or if
adding it to the archive fails
"""
write_p = self._pointer
block_size = ffi.write_get_bytes_per_block(write_p)
if block_size <= 0:
block_size = 10240 # pragma: no cover
entry = ArchiveEntry(header_codec=self.header_codec)
entry_p = entry._entry_p
destination_path = attributes.pop('pathname', None)
for path in paths:
with new_archive_read_disk(path, flags, lookup) as read_p:
while 1:
r = read_next_header2(read_p, entry_p)
if r == ARCHIVE_EOF:
break
entry_path = entry.pathname
if destination_path:
if entry_path == path:
entry_path = destination_path
else:
assert entry_path.startswith(path)
entry_path = join(
destination_path,
entry_path[len(path):].lstrip('/')
)
entry.pathname = entry_path.lstrip('/')
if attributes:
entry.modify(**attributes)
read_disk_descend(read_p)
write_header(write_p, entry_p)
if entry.isreg:
with open(entry_sourcepath(entry_p), 'rb') as f:
while 1:
data = f.read(block_size)
if not data:
break
write_data(write_p, data, len(data))
write_finish_entry(write_p)
entry_clear(entry_p)
if not recursive:
break
def add_file(self, path, **kw):
"Single-path alias of `add_files()`"
return self.add_files(path, **kw)
def add_file_from_memory(
self, entry_path, entry_size, entry_data,
filetype=FileType.REGULAR_FILE, permission=0o664,
**other_attributes
):
""""Add file from memory to archive.
Args:
entry_path (str | bytes): the file's path
entry_size (int): the file's size, in bytes
entry_data (bytes | Iterable[bytes]): the file's content
filetype (int): see `libarchive.entry.ArchiveEntry.modify()`
permission (int): see `libarchive.entry.ArchiveEntry.modify()`
other_attributes: see `libarchive.entry.ArchiveEntry.modify()`
"""
archive_pointer = self._pointer
if isinstance(entry_data, bytes):
entry_data = (entry_data,)
elif isinstance(entry_data, str):
raise TypeError(
"entry_data: expected bytes, got %r" % type(entry_data)
)
entry = ArchiveEntry(
pathname=entry_path, size=entry_size, filetype=filetype,
perm=permission, header_codec=self.header_codec,
**other_attributes
)
write_header(archive_pointer, entry._entry_p)
for chunk in entry_data:
if not chunk:
break
write_data(archive_pointer, chunk, len(chunk))
write_finish_entry(archive_pointer)
@contextmanager
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)
except Exception:
ffi.write_fail(archive_p)
ffi.write_free(archive_p)
raise
@property
def bytes_written(self):
return ffi.filter_bytes(self._pointer, -1)
@contextmanager
def custom_writer(
write_func, format_name, filter_name=None,
open_func=None, close_func=None, block_size=page_size,
archive_write_class=ArchiveWrite, options='', passphrase=None,
header_codec='utf-8',
):
"""Create an archive and send it in chunks to the `write_func` function.
For formats and filters, see `WRITE_FORMATS` and `WRITE_FILTERS` in the
`libarchive.ffi` module.
"""
def write_cb_internal(archive_p, context, buffer_, length):
data = cast(buffer_, POINTER(c_char * length))[0]
return write_func(data)
open_cb = OPEN_CALLBACK(open_func) if open_func else NO_OPEN_CB
write_cb = WRITE_CALLBACK(write_cb_internal)
close_cb = CLOSE_CALLBACK(close_func) if close_func else NO_CLOSE_CB
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)
yield archive_write_class(archive_p, header_codec)
@contextmanager
def fd_writer(
fd, format_name, filter_name=None,
archive_write_class=ArchiveWrite, options='', passphrase=None,
header_codec='utf-8',
):
"""Create an archive and write it into a file descriptor.
For formats and filters, see `WRITE_FORMATS` and `WRITE_FILTERS` in the
`libarchive.ffi` module.
"""
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, header_codec)
@contextmanager
def file_writer(
filepath, format_name, filter_name=None,
archive_write_class=ArchiveWrite, options='', passphrase=None,
header_codec='utf-8',
):
"""Create an archive and write it into a file.
For formats and filters, see `WRITE_FORMATS` and `WRITE_FILTERS` in the
`libarchive.ffi` module.
"""
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, header_codec)
@contextmanager
def memory_writer(
buf, format_name, filter_name=None,
archive_write_class=ArchiveWrite, options='', passphrase=None,
header_codec='utf-8',
):
"""Create an archive and write it into a buffer.
For formats and filters, see `WRITE_FORMATS` and `WRITE_FILTERS` in the
`libarchive.ffi` module.
"""
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)
yield archive_write_class(archive_p, header_codec)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1709644463.4981325
libarchive-c-5.1/libarchive_c.egg-info/ 0000755 0002166 0000144 00000000000 14571615257 017554 5 ustar 00changaco users ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1709644463.0
libarchive-c-5.1/libarchive_c.egg-info/PKG-INFO 0000644 0002166 0000144 00000012274 14571615257 020657 0 ustar 00changaco users Metadata-Version: 2.1
Name: libarchive-c
Version: 5.1
Summary: Python interface to libarchive
Home-page: https://github.com/Changaco/python-libarchive-c
Author: Changaco
Author-email: changaco@changaco.oy.lc
License: CC0
Keywords: archive libarchive 7z tar bz2 zip gz
Description-Content-Type: text/x-rst
License-File: LICENSE.md
A Python interface to libarchive. It uses the standard ctypes_ module to
dynamically load and access the C library.
.. _ctypes: https://docs.python.org/3/library/ctypes.html
Installation
============
pip install libarchive-c
Compatibility
=============
python
------
python-libarchive-c is currently tested with python 3.8, 3.9, 3.10 and 3.11.
If you find an incompatibility with older versions you can send us a small patch,
but we won't accept big changes.
libarchive
----------
python-libarchive-c may not work properly with obsolete versions of libarchive such as the ones included in MacOS. In that case you can install a recent version of libarchive (e.g. with ``brew install libarchive`` on MacOS) and use the ``LIBARCHIVE`` environment variable to point python-libarchive-c to it::
export LIBARCHIVE=/usr/local/Cellar/libarchive/3.3.3/lib/libarchive.13.dylib
Usage
=====
Import::
import libarchive
Extracting archives
-------------------
To extract an archive, use the ``extract_file`` function::
os.chdir('/path/to/target/directory')
libarchive.extract_file('test.zip')
Alternatively, the ``extract_memory`` function can be used to extract from a buffer,
and ``extract_fd`` from a file descriptor.
The ``extract_*`` functions all have an integer ``flags`` argument which is passed
directly to the C function ``archive_write_disk_set_options()``. You can import
the ``EXTRACT_*`` constants from the ``libarchive.extract`` module and see the
official description of each flag in the ``archive_write_disk(3)`` man page.
By default, when the ``flags`` argument is ``None``, the ``SECURE_NODOTDOT``,
``SECURE_NOABSOLUTEPATHS`` and ``SECURE_SYMLINKS`` flags are passed to
libarchive, unless the current directory is the root (``/``).
Reading archives
----------------
To read an archive, use the ``file_reader`` function::
with libarchive.file_reader('test.7z') as archive:
for entry in archive:
for block in entry.get_blocks():
...
Alternatively, the ``memory_reader`` function can be used to read from a buffer,
``fd_reader`` from a file descriptor, ``stream_reader`` from a stream object
(which must support the standard ``readinto`` method), and ``custom_reader``
from anywhere using callbacks.
To learn about the attributes of the ``entry`` object, see the ``libarchive/entry.py``
source code or run ``help(libarchive.entry.ArchiveEntry)`` in a Python shell.
Displaying progress
~~~~~~~~~~~~~~~~~~~
If your program processes large archives, you can keep track of its progress
with the ``bytes_read`` attribute. Here's an example of a progress bar using
`tqdm `_::
with tqdm(total=os.stat(archive_path).st_size, unit='bytes') as pbar, \
libarchive.file_reader(archive_path) as archive:
for entry in archive:
...
pbar.update(archive.bytes_read - pbar.n)
Creating archives
-----------------
To create an archive, use the ``file_writer`` function::
from libarchive.entry import FileType
with libarchive.file_writer('test.tar.gz', 'ustar', 'gzip') as archive:
# Add the `libarchive/` directory and everything in it (recursively),
# then the `README.rst` file.
archive.add_files('libarchive/', 'README.rst')
# Add a regular file defined from scratch.
data = b'foobar'
archive.add_file_from_memory('../escape-test', len(data), data)
# Add a directory defined from scratch.
early_epoch = (42, 42) # 1970-01-01 00:00:42.000000042
archive.add_file_from_memory(
'metadata-test', 0, b'',
filetype=FileType.DIRECTORY, permission=0o755, uid=4242, gid=4242,
atime=early_epoch, mtime=early_epoch, ctime=early_epoch, birthtime=early_epoch,
)
Alternatively, the ``memory_writer`` function can be used to write to a memory buffer,
``fd_writer`` to a file descriptor, and ``custom_writer`` to a callback function.
For each of those functions, the mandatory second argument is the archive format,
and the optional third argument is the compression format (called “filter” in
libarchive). The acceptable values are listed in ``libarchive.ffi.WRITE_FORMATS``
and ``libarchive.ffi.WRITE_FILTERS``.
File metadata codecs
--------------------
By default, UTF-8 is used to read and write file attributes from and to archives.
A different codec can be specified through the ``header_codec`` arguments of the
``*_reader`` and ``*_writer`` functions. Example::
with libarchive.file_writer('test.tar', 'ustar', header_codec='cp037') as archive:
...
with file_reader('test.tar', header_codec='cp037') as archive:
...
In addition to file paths (``pathname`` and ``linkpath``), the specified codec is
used to encode and decode user and group names (``uname`` and ``gname``).
License
=======
`CC0 Public Domain Dedication `_
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1709644463.0
libarchive-c-5.1/libarchive_c.egg-info/SOURCES.txt 0000644 0002166 0000144 00000001762 14571615257 021446 0 ustar 00changaco users .gitattributes
.gitignore
LICENSE.md
MANIFEST.in
README.rst
setup.cfg
setup.py
tox.ini
version.py
.github/FUNDING.yml
.github/workflows/main.yml
libarchive/__init__.py
libarchive/entry.py
libarchive/exception.py
libarchive/extract.py
libarchive/ffi.py
libarchive/flags.py
libarchive/read.py
libarchive/write.py
libarchive_c.egg-info/PKG-INFO
libarchive_c.egg-info/SOURCES.txt
libarchive_c.egg-info/dependency_links.txt
libarchive_c.egg-info/top_level.txt
tests/__init__.py
tests/test_atime_mtime_ctime.py
tests/test_convert.py
tests/test_entry.py
tests/test_errors.py
tests/test_rwx.py
tests/test_security_flags.py
tests/data/flags.tar
tests/data/special.tar
tests/data/tar_relative.tar
tests/data/testtar.README
tests/data/testtar.tar
tests/data/testtar.tar.json
tests/data/unicode.tar
tests/data/unicode.tar.json
tests/data/unicode.zip
tests/data/unicode.zip.json
tests/data/unicode2.zip
tests/data/unicode2.zip.json
tests/data/프로그램.README
tests/data/프로그램.zip
tests/data/프로그램.zip.json ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1709644463.0
libarchive-c-5.1/libarchive_c.egg-info/dependency_links.txt 0000644 0002166 0000144 00000000001 14571615257 023622 0 ustar 00changaco users
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1709644463.0
libarchive-c-5.1/libarchive_c.egg-info/top_level.txt 0000644 0002166 0000144 00000000013 14571615257 022300 0 ustar 00changaco users libarchive
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1709644463.4981325
libarchive-c-5.1/setup.cfg 0000644 0002166 0000144 00000000210 14571615257 015262 0 ustar 00changaco users [wheel]
universal = 1
[flake8]
exclude = .?*,env*/
ignore = E226,E731,W504
max-line-length = 85
[egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1688027638.0
libarchive-c-5.1/setup.py 0000644 0002166 0000144 00000001235 14447240766 015164 0 ustar 00changaco users import os
from os.path import join, dirname
from setuptools import setup, find_packages
from version import get_version
os.umask(0o022)
with open(join(dirname(__file__), 'README.rst'), encoding="utf-8") as f:
README = f.read()
setup(
name='libarchive-c',
version=get_version(),
description='Python interface to libarchive',
author='Changaco',
author_email='changaco@changaco.oy.lc',
url='https://github.com/Changaco/python-libarchive-c',
license='CC0',
packages=find_packages(exclude=['tests']),
long_description=README,
long_description_content_type='text/x-rst',
keywords='archive libarchive 7z tar bz2 zip gz',
)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1709644463.4981325
libarchive-c-5.1/tests/ 0000755 0002166 0000144 00000000000 14571615257 014612 5 ustar 00changaco users ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1621868945.0
libarchive-c-5.1/tests/__init__.py 0000644 0002166 0000144 00000010311 14052740621 016703 0 ustar 00changaco users from contextlib import closing, contextmanager
from copy import copy
from os import chdir, getcwd, stat, walk
from os.path import abspath, dirname, join
from stat import S_ISREG
import tarfile
try:
from stat import filemode
except ImportError: # Python 2
filemode = tarfile.filemode
from libarchive import file_reader
data_dir = join(dirname(__file__), 'data')
def check_archive(archive, tree):
tree2 = copy(tree)
for e in archive:
epath = str(e).rstrip('/')
assert epath in tree2
estat = tree2.pop(epath)
assert e.mtime == int(estat['mtime'])
if not e.isdir:
size = e.size
if size is not None:
assert size == estat['size']
with open(epath, 'rb') as f:
for block in e.get_blocks():
assert f.read(len(block)) == block
leftover = f.read()
assert not leftover
# Check that there are no missing directories or files
assert len(tree2) == 0
def get_entries(location):
"""
Using the archive file at `location`, return an iterable of name->value
mappings for each libarchive.ArchiveEntry objects essential attributes.
Paths are base64-encoded because JSON is UTF-8 and cannot handle
arbitrary binary pathdata.
"""
with file_reader(location) as arch:
for entry in arch:
# libarchive introduces prefixes such as h prefix for
# hardlinks: tarfile does not, so we ignore the first char
mode = entry.strmode[1:].decode('ascii')
yield {
'path': surrogate_decode(entry.pathname),
'mtime': entry.mtime,
'size': entry.size,
'mode': mode,
'isreg': entry.isreg,
'isdir': entry.isdir,
'islnk': entry.islnk,
'issym': entry.issym,
'linkpath': surrogate_decode(entry.linkpath),
'isblk': entry.isblk,
'ischr': entry.ischr,
'isfifo': entry.isfifo,
'isdev': entry.isdev,
'uid': entry.uid,
'gid': entry.gid
}
def get_tarinfos(location):
"""
Using the tar archive file at `location`, return an iterable of
name->value mappings for each tarfile.TarInfo objects essential
attributes.
Paths are base64-encoded because JSON is UTF-8 and cannot handle
arbitrary binary pathdata.
"""
with closing(tarfile.open(location)) as tar:
for entry in tar:
path = surrogate_decode(entry.path or '')
if entry.isdir() and not path.endswith('/'):
path += '/'
# libarchive introduces prefixes such as h prefix for
# hardlinks: tarfile does not, so we ignore the first char
mode = filemode(entry.mode)[1:]
yield {
'path': path,
'mtime': entry.mtime,
'size': entry.size,
'mode': mode,
'isreg': entry.isreg(),
'isdir': entry.isdir(),
'islnk': entry.islnk(),
'issym': entry.issym(),
'linkpath': surrogate_decode(entry.linkpath or None),
'isblk': entry.isblk(),
'ischr': entry.ischr(),
'isfifo': entry.isfifo(),
'isdev': entry.isdev(),
'uid': entry.uid,
'gid': entry.gid
}
@contextmanager
def in_dir(dirpath):
prev = abspath(getcwd())
chdir(dirpath)
try:
yield
finally:
chdir(prev)
def stat_dict(path):
keys = set(('uid', 'gid', 'mtime'))
mode, _, _, _, uid, gid, size, _, mtime, _ = stat(path)
if S_ISREG(mode):
keys.add('size')
return {k: v for k, v in locals().items() if k in keys}
def treestat(d, stat_dict=stat_dict):
r = {}
for dirpath, dirnames, filenames in walk(d):
r[dirpath] = stat_dict(dirpath)
for fname in filenames:
fpath = join(dirpath, fname)
r[fpath] = stat_dict(fpath)
return r
def surrogate_decode(o):
if isinstance(o, bytes):
return o.decode('utf8', errors='surrogateescape')
return o
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1709644463.4981325
libarchive-c-5.1/tests/data/ 0000755 0002166 0000144 00000000000 14571615257 015523 5 ustar 00changaco users ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1528631099.0
libarchive-c-5.1/tests/data/flags.tar 0000644 0002166 0000144 00000024000 13307207473 017314 0 ustar 00changaco users /tmp/python-libarchive-c-test-absolute-file 0000644 0001750 0001750 00000000000 13106045633 017676 0 ustar neko neko ../python-libarchive-c-test-dot-dot-file 0000644 0001750 0001750 00000000000 13106045633 016766 0 ustar neko neko ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1480442606.0
libarchive-c-5.1/tests/data/special.tar 0000644 0002166 0000144 00000334000 13017341356 017641 0 ustar 00changaco users 0-REGTYPE 0000644 0001754 0000145 00000007265 10077465401 013372 0 ustar tarfile tarfile 0000000 0000000 GIF87a\` νssscccRRR999))) , \` I8ͻ Z@UZJf*tmx<$` À@dKp`&zx4AX3~4h鈑b{u
OhA6 GZhΨʇ
@h
n0ɇDyESh``}[[Ȑ>& Mb\JcÏ Ǩ+l"a+H!cPʆj8QTgoJ4/&%/sN&4Tի
QhRU
#$Y8(Kb؞;4
SAUUYԗt;aP] [bx)"BA3~ITPhZ@c lC@[f/\"rɅ0 s ,]:B6%_mtW)@`'܇ˀO wK)pq`5B6a6UVhh[š['4#-!`WN`"@)HxdցyAX$G:dFd:0Tixed6uB`Օu0gd&m