# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2021
# Author: Cleber Rosa <crosa@redhat.com>
__doc__ = """
Module to read UNIX ar files
"""
import struct
#: The first eight bytes of all AR archives
MAGIC = b"!<arch>\n"
#: The header for each file in the archive
FILE_HEADER_FMT = "16s12s6s6s8s10s2c"
FILE_HEADER_SIZE = struct.calcsize(FILE_HEADER_FMT)
[docs]
class ArMember:
"""A member of an UNIX ar archive."""
def __init__(self, identifier, size, offset):
self.identifier = identifier
self.size = size
self.offset = offset
def __repr__(self):
return (
f'<ArMember "{self.identifier}" size={int(self.size)} '
f"offset={int(self.offset)}>"
)
[docs]
class Ar:
"""An UNIX ar archive iterator.
It reads ar archive file and iterates over the members in form of :class:`autils.archive.ar.ArMember`
"""
def __init__(self, path):
self._path = path
self._file = None
self._position = None
self._valid = False
def __enter__(self):
self._file = open(self._path, "rb")
return self._file
def __exit__(self, _exc_type, _exc_value, _traceback):
self._file.close()
[docs]
def is_valid(self):
"""Checks if a file looks like an AR archive.
:return: If file looks like an AR archive
:rtype: bool
"""
with self as open_file:
return open_file.read(8) == MAGIC
def __iter__(self):
self._position = 8
self._valid = self.is_valid()
return self
def __next__(self):
if not self._valid:
raise StopIteration
with self as open_file:
open_file.seek(self._position)
try:
member = struct.unpack(
FILE_HEADER_FMT, open_file.read(FILE_HEADER_SIZE)
)
except struct.error as exc:
raise StopIteration from exc
# No support for extended file names
identifier = member[0].decode("ascii").strip()
# from bytes containing a decimal to int
size = int(member[5].decode("ascii").strip())
data_position = self._position + FILE_HEADER_SIZE
# All data sections is aligned at 2 bytes
data_position += data_position % 2
self._position += FILE_HEADER_SIZE + size
return ArMember(identifier, size, data_position)
[docs]
def list(self):
"""List the members in the archive.
:return: List the names of the mebers in the archive
:rtype: list
"""
return [member.identifier for member in self]
[docs]
def read_member(self, identifier):
"""Reads the data for the given member identifier.
:param identifier: Archive member name.
:type identifier: str
:return: data for the given member
:rtype: bytes or None
"""
for member in self:
if identifier == member.identifier:
with self as open_file:
open_file.seek(member.offset)
return open_file.read(member.size)
return None