├── tests ├── __init__.py ├── test_subfile.py ├── test_cli.py └── test_extract.py ├── .gitignore ├── MANIFEST.in ├── .travis.yml ├── rpmfile ├── __main__.py ├── errors.py ├── rpmdefs.py ├── io_extra.py ├── cli.py ├── __init__.py ├── headers.py └── cpiofile.py ├── setup.py ├── .settings └── org.eclipse.core.resources.prefs ├── .project ├── .pydevproject ├── pyproject.toml ├── .github └── workflows │ ├── tests.yml │ └── release.yml ├── setup.cfg ├── LICENSE └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyor 3 | *.egg-info/ 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | script: 5 | - python setup.py test 6 | -------------------------------------------------------------------------------- /rpmfile/__main__.py: -------------------------------------------------------------------------------- 1 | from .cli import console_script_entry_point 2 | 3 | if __name__ == "__main__": 4 | console_script_entry_point() 5 | -------------------------------------------------------------------------------- /rpmfile/errors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jan 10, 2014 3 | 4 | @author: sean 5 | """ 6 | 7 | 8 | class RPMError(Exception): 9 | pass 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | @author: sean 3 | """ 4 | 5 | from setuptools import setup 6 | 7 | if __name__ == "__main__": 8 | setup(use_scm_version=True) 9 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding//rpmfile/__init__.py=iso-8859-15 3 | encoding//rpmfile/cpiofile.py=utf-8 4 | encoding//rpmfile/rpmdefs.py=iso-8859-15 5 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | rpmquery 4 | 5 | 6 | 7 | 8 | 9 | org.python.pydev.PyDevBuilder 10 | 11 | 12 | 13 | 14 | 15 | org.python.pydev.pythonNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Default 6 | python 2.7 7 | 8 | /rpmquery 9 | 10 | 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=44", "wheel", "setuptools_scm[toml]>=3.4.3"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | 7 | [tool.black] 8 | target-version = ['py27'] 9 | 10 | exclude = ''' 11 | ( 12 | /( 13 | \.eggs # exclude a few common directories in the 14 | | \.git # root of the project 15 | | \.hg 16 | | \.mypy_cache 17 | | \.tox 18 | | \.venv 19 | | _build 20 | | buck-out 21 | | build 22 | | dist 23 | ) 24 | ) 25 | ''' 26 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | max-parallel: 40 11 | matrix: 12 | python-version: 13 | - "3.10" 14 | - "3.11" 15 | - "3.12" 16 | - "3.13" 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v1 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Dependencies 24 | run: | 25 | pip install -U pip zstandard setuptools wheel build 26 | - name: Test 27 | run: | 28 | python setup.py test 29 | - name: Style 30 | if: ${{ matrix.python-version == '3.7' }} 31 | run: | 32 | pip install -U black==23.1.0 33 | black -t py37 --check . 34 | - name: Package 35 | run: | 36 | python -m build 37 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = rpmfile 3 | author = Sean Ross-Ross 4 | author_email = srossross@gmail.com 5 | license = MIT 6 | description = Read rpm archive files 7 | url = https://github.com/srossross/rpmfile 8 | long_description = file: README.md 9 | long_description_content_type = text/markdown 10 | classifiers = 11 | Intended Audience :: Developers 12 | License :: OSI Approved :: MIT License 13 | Natural Language :: English 14 | Operating System :: OS Independent 15 | Programming Language :: Python :: 2.6 16 | Programming Language :: Python :: 2.7 17 | Programming Language :: Python :: 3.4 18 | Programming Language :: Python :: 3.5 19 | Programming Language :: Python :: 3.6 20 | Programming Language :: Python :: 3.7 21 | Programming Language :: Python :: 3.8 22 | 23 | [options] 24 | packages = rpmfile 25 | setup_requires = 26 | setuptools_scm 27 | 28 | [options.extras_require] 29 | zstd = zstandard>=0.13.0 30 | 31 | [options.entry_points] 32 | console_scripts = 33 | rpmfile = rpmfile.cli:console_script_entry_point 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Sean Ross-Ross 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_subfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jan 11, 2014 3 | 4 | @author: sean 5 | """ 6 | import unittest 7 | 8 | import rpmfile 9 | import io 10 | 11 | 12 | class Test(unittest.TestCase): 13 | def test_seek(self): 14 | fd = io.BytesIO(b"Hello world") 15 | sub = rpmfile._SubFile(fd, start=2, size=4) 16 | 17 | sub.seek(0) 18 | self.assertEqual(sub.tell(), 0) 19 | 20 | sub.seek(1) 21 | self.assertEqual(sub.tell(), 1) 22 | 23 | sub.seek(1, 1) 24 | self.assertEqual(sub.tell(), 2) 25 | 26 | sub.seek(-1, 1) 27 | self.assertEqual(sub.tell(), 1) 28 | 29 | sub.seek(-10, 1) 30 | self.assertEqual(sub.tell(), 0) 31 | 32 | def test_read(self): 33 | fd = io.BytesIO(b"Hello world") 34 | sub = rpmfile._SubFile(fd, start=2, size=4) 35 | 36 | self.assertEqual(sub.read(), b"llo ") 37 | self.assertEqual(sub.read(), b"") 38 | 39 | sub.seek(0) 40 | self.assertEqual(sub.read(2), b"ll") 41 | 42 | sub.seek(0) 43 | self.assertEqual(sub.read(10), b"llo ") 44 | 45 | 46 | if __name__ == "__main__": 47 | # import sys;sys.argv = ['', 'Test.testSeek'] 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: 14 | - "3.10" 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install PyPi Release Dependencies 22 | run: | 23 | pip install -U setuptools wheel twine build 24 | - name: Build Package 25 | run: | 26 | python -m build 27 | - name: Get tag 28 | id: get-tag 29 | run: | 30 | echo "tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT 31 | if [ "x${{ secrets.PYPI_RPMFILE }}" == "x" ]; then 32 | echo "has_pypi_token=no" >> $GITHUB_OUTPUT 33 | else 34 | echo "has_pypi_token=yes" >> $GITHUB_OUTPUT 35 | fi 36 | - name: PyPi Release 37 | id: pypi-release 38 | if: ${{ steps.get-tag.outputs.has_pypi_token == 'yes' }} 39 | run: | 40 | export TWINE_USERNAME=__token__ 41 | export TWINE_PASSWORD=${{ secrets.PYPI_RPMFILE }} 42 | python -m twine upload dist/* 43 | - name: GitHub Release 44 | uses: "marvinpinto/action-automatic-releases@4edd7a5aabb1bc62e6dc99b3302d587bf3134e20" 45 | with: 46 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 47 | automatic_release_tag: "${{ steps.get-tag.outputs.tag }}" 48 | prerelease: false 49 | title: "${{ steps.pypi-release.outputs.tag }}" 50 | files: | 51 | dist/* 52 | -------------------------------------------------------------------------------- /rpmfile/rpmdefs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: iso-8859-15 -*- 2 | # -*- Mode: Python; py-ident-offset: 4 -*- 3 | # vim:ts=4:sw=4:et 4 | """ 5 | rpm definitions 6 | 7 | Mario Morgado (BSD) https://github.com/mjvm/pyrpm 8 | """ 9 | __revision__ = "$Rev$"[6:-2] 10 | 11 | RPM_LEAD_MAGIC_NUMBER = "\xed\xab\xee\xdb" 12 | RPM_HEADER_MAGIC_NUMBER = "\x8e\xad\xe8" 13 | 14 | RPMTAG_MIN_NUMBER = 1000 15 | RPMTAG_MAX_NUMBER = 1146 16 | 17 | # signature tags 18 | RPMSIGTAG_SIZE = 1000 19 | RPMSIGTAG_LEMD5_1 = 1001 20 | RPMSIGTAG_PGP = 1002 21 | RPMSIGTAG_LEMD5_2 = 1003 22 | RPMSIGTAG_MD5 = 1004 23 | RPMSIGTAG_GPG = 1005 24 | RPMSIGTAG_PGP5 = 1006 25 | 26 | 27 | MD5_SIZE = 16 # 16 bytes long 28 | PGP_SIZE = 152 # 152 bytes long 29 | 30 | 31 | # data types definition 32 | RPM_DATA_TYPE_NULL = 0 33 | RPM_DATA_TYPE_CHAR = 1 34 | RPM_DATA_TYPE_INT8 = 2 35 | RPM_DATA_TYPE_INT16 = 3 36 | RPM_DATA_TYPE_INT32 = 4 37 | RPM_DATA_TYPE_INT64 = 5 38 | RPM_DATA_TYPE_STRING = 6 39 | RPM_DATA_TYPE_BIN = 7 40 | RPM_DATA_TYPE_STRING_ARRAY = 8 41 | RPM_DATA_TYPE_I18NSTRING_TYPE = 9 42 | 43 | RPM_DATA_TYPES = ( 44 | RPM_DATA_TYPE_NULL, 45 | RPM_DATA_TYPE_CHAR, 46 | RPM_DATA_TYPE_INT8, 47 | RPM_DATA_TYPE_INT16, 48 | RPM_DATA_TYPE_INT32, 49 | RPM_DATA_TYPE_INT64, 50 | RPM_DATA_TYPE_STRING, 51 | RPM_DATA_TYPE_BIN, 52 | RPM_DATA_TYPE_STRING_ARRAY, 53 | ) 54 | 55 | RPMTAG_NAME = 1000 56 | RPMTAG_VERSION = 1001 57 | RPMTAG_RELEASE = 1002 58 | RPMTAG_DESCRIPTION = 1005 59 | RPMTAG_COPYRIGHT = 1014 60 | RPMTAG_URL = 1020 61 | RPMTAG_ARCH = 1022 62 | 63 | 64 | RPMTAGS = ( 65 | RPMTAG_NAME, 66 | RPMTAG_VERSION, 67 | RPMTAG_RELEASE, 68 | RPMTAG_DESCRIPTION, 69 | RPMTAG_COPYRIGHT, 70 | RPMTAG_URL, 71 | RPMTAG_ARCH, 72 | ) 73 | -------------------------------------------------------------------------------- /rpmfile/io_extra.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jan 11, 2014 3 | 4 | @author: sean 5 | """ 6 | import io 7 | 8 | 9 | def _doc(from_func): 10 | """copy doc from one function to another 11 | use as a decorator eg:: 12 | 13 | @_doc(file.tell) 14 | def tell(..): 15 | ... 16 | """ 17 | 18 | def decorator(to_func): 19 | to_func.__doc__ = from_func.__doc__ 20 | return to_func 21 | 22 | return decorator 23 | 24 | 25 | class _SubFile(object): 26 | """A thin wrapper around an existing file object that 27 | provides a part of its data as an individual file 28 | object. 29 | """ 30 | 31 | def __init__(self, fileobj, start=0, size=None): 32 | self._fileobj = fileobj 33 | self._start = start 34 | if size is None: 35 | fileobj.seek(0, 2) 36 | pos = fileobj.tell() 37 | self._size = pos - start 38 | else: 39 | self._size = size 40 | self._pos = 0 41 | 42 | def __enter__(self): 43 | return self 44 | 45 | def __exit__(self, *excinfo): 46 | pass 47 | 48 | def close(self): 49 | pass 50 | 51 | def __getattr__(self, attr): 52 | return getattr(self._fileobj, attr) 53 | 54 | @_doc(io.FileIO.tell) 55 | def tell(self): 56 | return self._pos 57 | 58 | @_doc(io.FileIO.seek) 59 | def seek(self, offset, whence=0): 60 | if whence == 0: 61 | self._pos = offset 62 | elif whence == 1: 63 | self._pos += offset 64 | else: 65 | self._pos = self._size + offset 66 | 67 | self._pos = max(0, self._pos) 68 | 69 | def _n(self, size=None): 70 | if not size: 71 | size = self._size 72 | return min(size, self._size - self._pos) 73 | 74 | @_doc(io.FileIO.read) 75 | def read(self, size=None): 76 | self._fileobj.seek(self._pos + self._start, 0) 77 | 78 | n = self._n(size) 79 | self._pos += n 80 | 81 | return self._fileobj.read(n) 82 | 83 | @_doc(io.FileIO.readline) 84 | def readline(self, size=None): 85 | self._fileobj.seek(self._pos + self._start, 0) 86 | n = self._n(size) 87 | line = self._fileobj.readline(n) 88 | self._pos += len(line) 89 | return line 90 | 91 | @_doc(io.FileIO.readlines) 92 | def readlines(self, size=None): 93 | n = self._n(size) 94 | line = self.readline(n) 95 | n -= len(line) 96 | while line: 97 | yield line 98 | line = self.readline(n) 99 | n -= len(line) 100 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | import time 5 | import unittest 6 | 7 | from rpmfile.cli import main 8 | 9 | from .test_extract import download 10 | 11 | 12 | class TempCLI(unittest.TestCase): 13 | GOPACKET_LICENSE_DIRS = [".", "usr", "share", "doc", "gopacket-license"] 14 | GOPACKET_LICENSE_FILES = ["AUTHORS", "LICENSE"] 15 | GOPACKET_LICENSE_INFO = ( 16 | "Name : gopacket-license\n" 17 | "Version : 2019_04_08T07_36_42Z\n" 18 | "Release : 1\n" 19 | "Architecture: noarch\n" 20 | "Group : default\n" 21 | "Size : 3223\n" 22 | "License : BSD\n" 23 | "Signature : None\n" 24 | "Source RPM : gopacket-license-2019_04_08T07_36_42Z-1.src.rpm\n" 25 | "Build Date : Tue Apr 9 08:55:16 2019\n" 26 | "Build Host : jenkins-slave-fat-cloud-nlbzt\n" 27 | "URL : http://example.com/no-uri-given\n" 28 | "Summary : License for gopacket-license\n" 29 | "Description : \n" 30 | "License for gopacket-license\n" 31 | ) 32 | 33 | def setUp(cls): 34 | cls.prevdir = os.getcwd() 35 | cls.tempdir = tempfile.mkdtemp() 36 | os.chdir(cls.tempdir) 37 | os.environ["LC_ALL"] = "C" 38 | os.environ["TZ"] = "UTC" 39 | time.tzset() 40 | 41 | def tearDown(cls): 42 | shutil.rmtree(cls.tempdir) 43 | os.chdir(cls.prevdir) 44 | 45 | @download( 46 | "https://github.com/srossross/rpmfile/files/3150016/gopacket-license.noarch.rpm.gz", 47 | "gopacket.rpm", 48 | ) 49 | def test_extract(self, rpmpath): 50 | """That the command line extracts correctly""" 51 | dest = os.path.join(self.tempdir, "mydir") 52 | os.mkdir(dest) 53 | 54 | _args, output = main("-xC", dest, rpmpath) 55 | 56 | extracted_files = os.listdir( 57 | os.path.join(self.tempdir, "mydir", *self.GOPACKET_LICENSE_DIRS) 58 | ) 59 | self.assertEqual(self.GOPACKET_LICENSE_FILES, sorted(extracted_files)) 60 | 61 | self.assertEqual(len(output["extracted"]), len(self.GOPACKET_LICENSE_FILES)) 62 | for filename in self.GOPACKET_LICENSE_FILES: 63 | self.assertIn(self.GOPACKET_LICENSE_DIRS + [filename], output["extracted"]) 64 | 65 | @download( 66 | "https://github.com/srossross/rpmfile/files/3150016/gopacket-license.noarch.rpm.gz", 67 | "gopacket.rpm", 68 | ) 69 | def test_list(self, rpmpath): 70 | """That the command line lists correctly""" 71 | _args, output = main("-l", rpmpath) 72 | 73 | self.assertEqual(len(output["list"]), len(self.GOPACKET_LICENSE_FILES)) 74 | for filename in self.GOPACKET_LICENSE_FILES: 75 | self.assertIn(self.GOPACKET_LICENSE_DIRS + [filename], output["list"]) 76 | 77 | @download( 78 | "https://github.com/srossross/rpmfile/files/3150016/gopacket-license.noarch.rpm.gz", 79 | "gopacket.rpm", 80 | ) 81 | def test_info(self, rpmpath): 82 | """That the command line get RPM infomation correctly""" 83 | _args, output = main("-i", rpmpath) 84 | self.assertEqual(output["info"], self.GOPACKET_LICENSE_INFO) 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rpmfile 2 | 3 | [![Build Status](https://travis-ci.org/srossross/rpmfile.svg?branch=master)](https://travis-ci.org/srossross/rpmfile) 4 | [![Actions Status](https://github.com/srossross/rpmfile/workflows/Tests/badge.svg?branch=master&event=push)](https://github.com/srossross/rpmfile/actions) 5 | [![PyPI version](https://img.shields.io/pypi/v/rpmfile.svg)](https://pypi.org/project/rpmfile) 6 | 7 | Tools for inspecting RPM files in python. This module is modeled after the 8 | [tarfile](https://docs.python.org/3/library/tarfile.html) module. 9 | 10 | ## Install 11 | 12 | ```console 13 | $ python -m pip install -U rpmfile 14 | ``` 15 | 16 | If you want to use `rpmfile` with `zstd` compressed rpms, you'll need to install 17 | the [zstandard](https://pypi.org/project/zstandard/) module. 18 | 19 | `zstd` also requires that you are using Python >= 3.5 20 | 21 | ```console 22 | $ python -m pip install -U zstandard 23 | ``` 24 | 25 | ## Example 26 | 27 | See the [tests](tests/test_extract.py) for more examples. 28 | 29 | ```python 30 | import rpmfile 31 | 32 | with rpmfile.open('file.rpm') as rpm: 33 | 34 | # Inspect the RPM headers 35 | print(rpm.headers.keys()) 36 | print(rpm.headers.get('arch', 'noarch')) 37 | 38 | # Extract a fileobject from the archive 39 | fd = rpm.extractfile('./usr/bin/script') 40 | print(fd.read()) 41 | 42 | for member in rpm.getmembers(): 43 | print(member) 44 | ``` 45 | 46 | ## Command line usage 47 | 48 | You can use `rpmfile` via it's module invocation or via `rpmfile` command if 49 | your `PATH` environment variable is configured correctly. Pass `--help` for all 50 | options. 51 | 52 | List RPM contents 53 | 54 | ```conosle 55 | curl -sfL 'https://example.com/some.rpm.gz' | gzip -d - | python -m rpmfile -l - 56 | ./path/to/file 57 | ``` 58 | 59 | Extract files 60 | 61 | ```conosle 62 | curl -sfL 'https://example.com/some.rpm.gz' | gzip -d - | rpmfile -xv - 63 | ./path/to/file 64 | ``` 65 | 66 | Extract files to directory 67 | 68 | ```conosle 69 | curl -sfL 'https://example.com/some.rpm.gz' | gzip -d - | rpmfile -xvC /tmp - 70 | /tmp/path/to/file 71 | ``` 72 | 73 | Display RPM information (similar to command `rpm -qip` in Linux) 74 | 75 | ```conosle 76 | curl -sfL 'https://example.com/some.rpm.gz' |gzip -d - | rpmfile -i - 77 | Name : something 78 | Version : 1.02 79 | Release : 1 80 | Architecture: noarch 81 | Group : default 82 | Size : 1234 83 | License : BSD 84 | Signature : None 85 | Source RPM : some.src.rpm 86 | Build Date : Tue Apr 9 08:55:16 2019 87 | Build Host : demo 88 | URL : http://example.com/some 89 | Summary : Example of something 90 | Description : 91 | The description of something. 92 | It can display more than one line. 93 | ``` 94 | 95 | 96 | ## Classes 97 | 98 | * rpmfile.RPMFile: The RPMFile object provides an interface to a RPM archive 99 | * rpmfile.RPMInfo: An RPMInfo object represents one member in a RPMFile. 100 | 101 | ## Contributing 102 | 103 | The [black](https://github.com/psf/black) formater should be used on all files 104 | before submitting a contribution. Version 19.10b0. 105 | 106 | ```console 107 | $ pip install black==19.10b0 108 | $ black . 109 | ``` 110 | 111 | ## Code in this module was borrowed from: 112 | 113 | * https://bitbucket.org/krp/cpiofile 114 | * https://github.com/mjvm/pyrpm 115 | -------------------------------------------------------------------------------- /rpmfile/cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from datetime import datetime 4 | import os 5 | import io 6 | import sys 7 | import argparse 8 | 9 | import rpmfile 10 | 11 | 12 | def console_script_entry_point(): 13 | main(*sys.argv[1:]) 14 | 15 | 16 | def main(*argv): 17 | parser = argparse.ArgumentParser(prog="rpmfile") 18 | parser.add_argument("infile") 19 | parser.add_argument( 20 | "-x", 21 | "--extract", 22 | dest="extract", 23 | action="store_true", 24 | help="Extract the input RPM", 25 | ) 26 | parser.add_argument( 27 | "-C", 28 | "--directory", 29 | type=str, 30 | dest="dest", 31 | help="Extract to this directory when extracting files", 32 | default=".", 33 | ) 34 | parser.add_argument( 35 | "-l", 36 | "--list", 37 | dest="list", 38 | action="store_true", 39 | help="List files in RPM without extracting", 40 | ) 41 | parser.add_argument( 42 | "-i", 43 | "--info", 44 | dest="info", 45 | action="store_true", 46 | help="Display RPM information without extracting", 47 | ) 48 | parser.add_argument( 49 | "-v", 50 | "--verbose", 51 | dest="verbose", 52 | action="store_true", 53 | help="Print filenames when extracting", 54 | ) 55 | args = parser.parse_args(argv) 56 | 57 | if args.infile == "-": 58 | args.infile = sys.stdin 59 | if sys.version_info.major >= 3: 60 | args.infile = args.infile.buffer 61 | else: 62 | args.infile = open(args.infile, "rb") 63 | 64 | # NOTE Not sure why but piping to rpmfile doesn't work unless we read 65 | # everything first into a buffer. Probably because of seek(). 66 | buf = io.BytesIO(args.infile.read()) 67 | 68 | output = {} 69 | 70 | if args.list: 71 | output["list"] = [] 72 | with rpmfile.open(fileobj=buf) as rpm: 73 | for rpminfo in rpm.getmembers(): 74 | print(rpminfo.name) 75 | output["list"].append(rpminfo.name.split("/")) 76 | elif args.info: 77 | output["info"] = "" 78 | with rpmfile.open(fileobj=buf) as rpm: 79 | headers_titles = { 80 | "name": "Name", 81 | "version": "Version", 82 | "release": "Release", 83 | "arch": "Architecture", 84 | "group": "Group", 85 | "size": "Size", 86 | "copyright": "License", 87 | "signature": "Signature", 88 | "sourcerpm": "Source RPM", 89 | "buildtime": "Build Date", 90 | "buildhost": "Build Host", 91 | "url": "URL", 92 | "summary": "Summary", 93 | "description": "Description", 94 | } 95 | for header in headers_titles: 96 | value = rpm.headers.get(header) 97 | if isinstance(value, bytes): 98 | value = value.decode() 99 | if header == "buildtime": 100 | value = datetime.fromtimestamp(value).strftime("%c") 101 | if header == "description": 102 | value = "\n" + value 103 | line = "%s: %s" % (headers_titles.get(header).ljust(12), value) 104 | print(line) 105 | output["info"] += line + "\n" 106 | elif args.extract: 107 | output["extracted"] = [] 108 | dest = os.path.abspath(args.dest) 109 | if not os.path.isdir(dest): 110 | raise FileNotFoundError(dest + " is not a directory") 111 | with rpmfile.open(fileobj=buf) as rpm: 112 | for rpminfo in rpm.getmembers(): 113 | with rpm.extractfile(rpminfo.name) as rpmfileobj: 114 | dirs = rpminfo.name.split("/") 115 | filename = dirs.pop() 116 | if dirs: 117 | dirs_path = os.path.abspath(os.path.join(dest, *dirs)) 118 | if not dirs_path.startswith(dest): 119 | raise ValueError("Attempted path traveral: " + dirs_path) 120 | if not os.path.isdir(dirs_path): 121 | os.makedirs(dirs_path) 122 | target = os.path.abspath(os.path.join(dest, *(dirs + [filename]))) 123 | if not target.startswith(dest): 124 | raise ValueError("Attempted path traveral: " + target) 125 | outfile = open(target, "wb") 126 | try: 127 | outfile.write(rpmfileobj.read()) 128 | finally: 129 | outfile.close() 130 | if args.verbose: 131 | print(target) 132 | output["extracted"].append(rpminfo.name.split("/")) 133 | else: 134 | raise Exception("Nothing to do") 135 | 136 | buf.close() 137 | args.infile.close() 138 | 139 | return args, output 140 | -------------------------------------------------------------------------------- /tests/test_extract.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import gzip 4 | import shutil 5 | import hashlib 6 | import stat 7 | import tempfile 8 | import unittest 9 | from functools import wraps 10 | 11 | try: 12 | from urllib2 import urlopen 13 | except ImportError: 14 | from urllib.request import urlopen 15 | 16 | import rpmfile 17 | 18 | 19 | def download(url, rpmname): 20 | def _downloader(func): 21 | @wraps(func) 22 | def wrapper(*args, **kwds): 23 | args = list(args) 24 | rpmpath = os.path.join(args[0].tempdir, rpmname) 25 | gztemp = os.path.join(args[0].tempdir, "temp.gz") 26 | args.append(rpmpath) 27 | download = urlopen(url) 28 | if url[::-1].startswith(".gz"[::-1]): 29 | with open(gztemp, "wb") as gztemp_file: 30 | gztemp_file.write(download.read()) 31 | download.close() 32 | download = gzip.open(gztemp, "rb") 33 | with open(rpmpath, "wb") as target_file: 34 | target_file.write(download.read()) 35 | download.close() 36 | if url[::-1].startswith(".gz"[::-1]): 37 | os.unlink(gztemp) 38 | return func(*args, **kwds) 39 | 40 | return wrapper 41 | 42 | return _downloader 43 | 44 | 45 | class TempDirTest(unittest.TestCase): 46 | @classmethod 47 | def setUpClass(cls): 48 | cls.prevdir = os.getcwd() 49 | cls.tempdir = tempfile.mkdtemp() 50 | os.chdir(cls.tempdir) 51 | 52 | @classmethod 53 | def tearDownClass(cls): 54 | shutil.rmtree(cls.tempdir) 55 | os.chdir(cls.prevdir) 56 | 57 | @unittest.skipUnless( 58 | sys.version_info.major >= 3 and sys.version_info.minor >= 3, "Need lzma module" 59 | ) 60 | @download( 61 | "https://download.clearlinux.org/releases/10540/clear/x86_64/os/Packages/sudo-setuid-1.8.17p1-34.x86_64.rpm", 62 | "sudo.rpm", 63 | ) 64 | def test_lzma_sudo(self, rpmpath): 65 | with rpmfile.open(rpmpath) as rpm: 66 | # Inspect the RPM headers 67 | self.assertIn("name", rpm.headers.keys()) 68 | self.assertEqual(rpm.headers.get("arch", "noarch"), b"x86_64") 69 | 70 | members = list(rpm.getmembers()) 71 | self.assertEqual(len(members), 1) 72 | 73 | with rpm.extractfile("./usr/bin/sudo") as fd: 74 | calculated = hashlib.md5(fd.read()).hexdigest() 75 | self.assertEqual(calculated, "a208f3d9170ecfa69a0f4ccc78d2f8f6") 76 | 77 | @unittest.skipUnless( 78 | sys.version_info.major >= 3 and sys.version_info.minor >= 5, "Need io.BytesIO" 79 | ) 80 | @download( 81 | "https://github.com/srossross/rpmfile/files/4505148/xmlstarlet-1.6.1-14.fc32.x86_64.txt", 82 | "xmlstarlet.rpm", 83 | ) 84 | def test_zstd_xmlstarlet(self, rpmpath): 85 | with rpmfile.open(rpmpath) as rpm: 86 | # Inspect the RPM headers 87 | self.assertIn("name", rpm.headers.keys()) 88 | self.assertEqual(rpm.headers.get("arch", "noarch"), b"x86_64") 89 | 90 | members = list(rpm.getmembers()) 91 | self.assertEqual(len(members), 12) 92 | 93 | with rpm.extractfile("./usr/bin/xmlstarlet") as fd: 94 | calculated = hashlib.md5(fd.read()).hexdigest() 95 | self.assertEqual(calculated, "c5e22d7e47751565b56e507cb6ee375e") 96 | 97 | with rpm.extractfile("./usr/share/doc/xmlstarlet/ChangeLog") as fd: 98 | calculated = hashlib.md5(fd.read()).hexdigest() 99 | self.assertEqual(calculated, "68b329da9893e34099c7d8ad5cb9c940") 100 | 101 | @download( 102 | "https://github.com/srossross/rpmfile/files/5561331/rpm-4.15.0-6.fc31.src.rpm.txt", 103 | "sample.rpm", 104 | ) 105 | def test_autoclose(self, rpmpath): 106 | """Test that RPMFile.open context manager properly closes rpm file""" 107 | 108 | rpm_ref = None 109 | with rpmfile.open(rpmpath) as rpm: 110 | rpm_ref = rpm 111 | 112 | # Inspect the RPM headers 113 | self.assertIn("name", rpm.headers.keys()) 114 | self.assertEqual(rpm.headers.get("arch", "noarch"), b"armv7hl") 115 | 116 | members = list(rpm.getmembers()) 117 | self.assertEqual(len(members), 12) 118 | 119 | # Test that subfile does not close parent file by calling close and 120 | # then extractfile again 121 | fd = rpm.extractfile("rpm-4.15.x-ldflags.patch") 122 | calculated = hashlib.md5(fd.read()).hexdigest() 123 | self.assertEqual(calculated, "65224837f744ab699d0c8762147b2a6b") 124 | fd.close() 125 | 126 | fd = rpm.extractfile("rpm.spec") 127 | calculated = hashlib.md5(fd.read()).hexdigest() 128 | self.assertEqual(calculated, "e59ea2cc856d3cc83538b00833f4d7b8") 129 | fd.close() 130 | 131 | # Test that RPMFile owned file descriptor and that underlying file is really closed 132 | self.assertTrue(rpm_ref._fileobj.closed) 133 | self.assertTrue(rpm_ref._ownes_fd) 134 | 135 | @download( 136 | "https://github.com/srossross/rpmfile/files/3150016/gopacket-license.noarch.rpm.gz", 137 | "gopacket.rpm", 138 | ) 139 | def test_issue_19(self, rpmpath): 140 | with rpmfile.open(rpmpath) as rpm: 141 | self.assertEqual(len(list(rpm.getmembers())), 2) 142 | 143 | @download( 144 | "https://github.com/srossross/rpmfile/files/14625568/hello-2.12.1-2.fc39.x86_64.rpm.gz", 145 | "hello.rpm", 146 | ) 147 | def test_unsigned_ints(self, rpmpath): 148 | with rpmfile.open(rpmpath) as rpm: 149 | self.assertEqual(1689811200, rpm.headers["filemtimes"][0]) 150 | mode = rpm.headers["filemodes"][0] 151 | st_type = stat.S_IFMT(mode) 152 | st_mode = stat.S_IMODE(mode) 153 | self.assertTrue(stat.S_ISREG(st_type)) 154 | self.assertEqual( 155 | stat.S_IRUSR 156 | | stat.S_IWUSR 157 | | stat.S_IXUSR 158 | | stat.S_IRGRP 159 | | stat.S_IXGRP 160 | | stat.S_IROTH 161 | | stat.S_IXOTH, 162 | st_mode, 163 | ) 164 | -------------------------------------------------------------------------------- /rpmfile/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, unicode_literals, absolute_import 2 | from .headers import get_headers 3 | import sys 4 | import io 5 | import gzip 6 | import bz2 7 | 8 | try: 9 | import lzma 10 | except ImportError: 11 | pass 12 | try: 13 | import zstandard 14 | except ImportError: 15 | pass 16 | import struct 17 | from rpmfile import cpiofile 18 | from functools import wraps 19 | from rpmfile.io_extra import _SubFile 20 | 21 | pad = lambda fileobj: (4 - (fileobj.tell() % 4)) % 4 22 | 23 | 24 | class NoLZMAModuleError(NotImplementedError): 25 | pass 26 | 27 | 28 | class NoZSTANDARDModuleError(NotImplementedError): 29 | pass 30 | 31 | 32 | class NoBytesIOError(NotImplementedError): 33 | pass 34 | 35 | 36 | class RPMInfo(object): 37 | """ 38 | Informational class which holds the details about an 39 | archive member given by an RPM entry block. 40 | RPMInfo objects are returned by RPMFile.getmember() and 41 | RPMFile.getmembers() and are 42 | usually created internally. 43 | """ 44 | 45 | _new_coder = struct.Struct(b"8s8s8s8s8s8s8s8s8s8s8s8s8s") 46 | 47 | def __init__(self, name, file_start, file_size, initial_offset, isdir): 48 | self.name = name 49 | self.file_start = file_start 50 | self.size = file_size 51 | self.initial_offset = initial_offset 52 | self._isdir = isdir 53 | 54 | @property 55 | def isdir(self): 56 | return self._isdir 57 | 58 | def __repr__(self): 59 | return "" % self.name 60 | 61 | @classmethod 62 | def _read(cls, magic, fileobj): 63 | if magic == b"070701": 64 | return cls._read_new(fileobj, magic=magic) 65 | else: 66 | raise Exception("bad magic number %r" % magic) 67 | 68 | @classmethod 69 | def _read_new(cls, fileobj, magic=None): 70 | coder = cls._new_coder 71 | 72 | initial_offset = fileobj.tell() 73 | d = coder.unpack_from(fileobj.read(coder.size)) 74 | 75 | namesize = int(d[11], 16) 76 | name = fileobj.read(namesize)[:-1].decode("utf-8") 77 | fileobj.seek(pad(fileobj), 1) 78 | file_start = fileobj.tell() 79 | file_size = int(d[6], 16) 80 | fileobj.seek(file_size, 1) 81 | fileobj.seek(pad(fileobj), 1) 82 | # https://www.mankier.com/5/cpio under Old Binary Format mode bits 83 | mode = int(d[1], 16) 84 | isdir = mode & int("0040000", 8) 85 | return cls(name, file_start, file_size, initial_offset, isdir) 86 | 87 | 88 | class RPMFile(object): 89 | """ 90 | Open an RPM archive `name'. `mode' must be 'r' to 91 | read from an existing archive. 92 | 93 | If `fileobj' is given, it is used for reading or writing data. If it 94 | can be determined, `mode' is overridden by `fileobj's mode. 95 | `fileobj' is not closed, when TarFile is closed. 96 | 97 | """ 98 | 99 | def __init__(self, name=None, mode="rb", fileobj=None): 100 | if mode != "rb": 101 | raise NotImplementedError("currently the only supported mode is 'rb'") 102 | self._fileobj = fileobj or io.open(name, mode) 103 | self._header_range, self._headers = get_headers(self._fileobj) 104 | self._ownes_fd = fileobj is None 105 | 106 | @property 107 | def data_offset(self): 108 | return self._header_range[1] 109 | 110 | @property 111 | def header_range(self): 112 | return self._header_range 113 | 114 | @property 115 | def headers(self): 116 | "RPM headers" 117 | return self._headers 118 | 119 | def __enter__(self): 120 | return self 121 | 122 | def __exit__(self, *excinfo): 123 | if self._ownes_fd: 124 | self._fileobj.close() 125 | 126 | _members = None 127 | 128 | def getmembers(self): 129 | """ 130 | Return the members of the archive as a list of RPMInfo objects. The 131 | list has the same order as the members in the archive. 132 | """ 133 | if self._members is None: 134 | self._members = _members = [] 135 | g = self.data_file 136 | magic = g.read(2) 137 | while magic: 138 | if magic == b"07": 139 | magic += g.read(4) 140 | member = RPMInfo._read(magic, g) 141 | 142 | if member.name == "TRAILER!!!": 143 | break 144 | 145 | if not member.isdir: 146 | _members.append(member) 147 | 148 | magic = g.read(2) 149 | return _members 150 | return self._members 151 | 152 | def getmember(self, name): 153 | """ 154 | Return an RPMInfo object for member `name'. If `name' can not be 155 | found in the archive, KeyError is raised. If a member occurs more 156 | than once in the archive, its last occurrence is assumed to be the 157 | most up-to-date version. 158 | """ 159 | members = self.getmembers() 160 | for m in members[::-1]: 161 | if m.name == name: 162 | return m 163 | 164 | raise KeyError("member %s could not be found" % name) 165 | 166 | def extractfile(self, member): 167 | """ 168 | Extract a member from the archive as a file object. `member' may be 169 | a filename or an RPMInfo object. 170 | The file-like object is read-only and provides the following 171 | methods: read(), readline(), readlines(), seek() and tell() 172 | """ 173 | if not isinstance(member, RPMInfo): 174 | member = self.getmember(member) 175 | return _SubFile(self.data_file, member.file_start, member.size) 176 | 177 | _data_file = None 178 | 179 | @property 180 | def data_file(self): 181 | """Return the uncompressed raw CPIO data of the RPM archive.""" 182 | 183 | if self._data_file is None: 184 | fileobj = _SubFile(self._fileobj, self.data_offset) 185 | 186 | if self.headers["archive_compression"] == b"xz": 187 | if not getattr(sys.modules[__name__], "lzma", False): 188 | raise NoLZMAModuleError("lzma module not present") 189 | self._data_file = lzma.LZMAFile(fileobj) 190 | elif self.headers["archive_compression"] == b"zstd": 191 | if not getattr(sys.modules[__name__], "zstandard", False): 192 | raise NoZSTANDARDModuleError("zstandard module not present") 193 | if not (sys.version_info.major >= 3 and sys.version_info.minor >= 5): 194 | raise NoBytesIOError("Need io.BytesIO (Python >= 3.5)") 195 | with zstandard.ZstdDecompressor().stream_reader(fileobj) as zstd_data: 196 | self._data_file = io.BytesIO(zstd_data.read()) 197 | 198 | elif self.headers["archive_compression"] == b"bzip2": 199 | self._data_file = bz2.BZ2File(fileobj) 200 | else: 201 | self._data_file = gzip.GzipFile(fileobj=fileobj) 202 | 203 | return self._data_file 204 | 205 | 206 | def open(name=None, mode="rb", fileobj=None): 207 | """ 208 | Open an RPM archive for reading. Return 209 | an appropriate RPMFile class. 210 | """ 211 | return RPMFile(name, mode, fileobj) 212 | 213 | 214 | def main(): 215 | print(sys.argv[1]) 216 | with open(sys.argv[1]) as rpm: 217 | print(rpm.headers) 218 | for m in rpm.getmembers(): 219 | print(m) 220 | print("done") 221 | 222 | 223 | if __name__ == "__main__": 224 | main() 225 | -------------------------------------------------------------------------------- /rpmfile/headers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jan 10, 2014 3 | 4 | @author: sean 5 | """ 6 | from __future__ import print_function, unicode_literals, absolute_import 7 | import sys 8 | import struct 9 | from pprint import pprint 10 | from .errors import RPMError 11 | 12 | sigtags = { 13 | "headerimage": 61, 14 | "headersignatures": 62, 15 | "size": 1000, 16 | "lemd5_1": 1001, 17 | "pgp": 1002, 18 | "lemd5_2": 1003, 19 | "sigmd5": 1004, # md5 in C code but collides with md5 here 20 | "gpg": 1005, 21 | "pgp5": 1006, 22 | "payloadsize": 1007, 23 | "reservedspace": 1008, 24 | "badsha1_1": 264, 25 | "badsha1_2": 265, 26 | "signature": 267, 27 | "rsaheader": 268, 28 | "md5": 269, # sha1header in C code 29 | "longsigsize": 270, 30 | "longarchivesize": 271, 31 | "sha256": 273, 32 | "filesignatures": 274, 33 | "filesignaturelength": 275, 34 | "veritysignatures": 276, 35 | "veritysignaturealgo": 277, 36 | } 37 | 38 | tags = { 39 | "headerimage": 61, 40 | "headersignatures": 62, 41 | "headerimmutable": 63, 42 | "headerregions": 64, 43 | "headeri18ntable": 100, 44 | "sig_base": 256, 45 | "sigsize": 257, 46 | "siglemd5_1": 258, 47 | "sigpgp": 259, 48 | "siglemd5_2": 260, 49 | "sigmd5": 261, 50 | "siggpg": 262, 51 | "sigpgp5": 263, 52 | "badsha1_1": 264, 53 | "badsha1_2": 265, 54 | "pubkeys": 266, 55 | "signature": 267, # dsaheader in C code 56 | "rsaheader": 268, 57 | "md5": 269, # sha1header in C code 58 | "longsigsize": 270, 59 | "longarchivesize": 271, 60 | "sha256": 273, 61 | "veritysignatures": 276, 62 | "veritysignaturealgo": 277, 63 | "name": 1000, 64 | "version": 1001, 65 | "release": 1002, 66 | "serial": 1003, 67 | "summary": 1004, 68 | "description": 1005, 69 | "buildtime": 1006, 70 | "buildhost": 1007, 71 | "installtime": 1008, 72 | "size": 1009, 73 | "distribution": 1010, 74 | "vendor": 1011, 75 | "gif": 1012, 76 | "xpm": 1013, 77 | "copyright": 1014, 78 | "packager": 1015, 79 | "group": 1016, 80 | "changelog": 1017, 81 | "source": 1018, 82 | "patch": 1019, 83 | "url": 1020, 84 | "os": 1021, 85 | "arch": 1022, 86 | "prein": 1023, 87 | "postin": 1024, 88 | "preun": 1025, 89 | "postun": 1026, 90 | "filenames": 1027, 91 | "filesizes": 1028, 92 | "filestates": 1029, 93 | "filemodes": 1030, 94 | "fileuids": 1031, 95 | "filegids": 1032, 96 | "filerdevs": 1033, 97 | "filemtimes": 1034, 98 | "filemd5s": 1035, 99 | "filelinktos": 1036, 100 | "fileflags": 1037, 101 | "root": 1038, 102 | "fileusername": 1039, 103 | "filegroupname": 1040, 104 | "exclude": 1041, 105 | "exclusive": 1042, 106 | "icon": 1043, 107 | "sourcerpm": 1044, 108 | "fileverifyflags": 1045, 109 | "archivesize": 1046, 110 | "provides": 1047, 111 | "requireflags": 1048, 112 | "requirename": 1049, 113 | "requireversion": 1050, 114 | "nosource": 1051, 115 | "nopatch": 1052, 116 | "conflictflags": 1053, 117 | "conflictname": 1054, 118 | "conflictversion": 1055, 119 | "defaultprefix": 1056, 120 | "buildroot": 1057, 121 | "installprefix": 1058, 122 | "excludearch": 1059, 123 | "excludeos": 1060, 124 | "exclusivearch": 1061, 125 | "exclusiveos": 1062, 126 | "autoreqprov": 1063, 127 | "rpmversion": 1064, 128 | "triggerscripts": 1065, 129 | "triggername": 1066, 130 | "triggerversion": 1067, 131 | "triggerflags": 1068, 132 | "triggerindex": 1069, 133 | "verifyscript": 1079, 134 | "changelogtime": 1080, 135 | "authors": 1081, 136 | "comments": 1082, 137 | "prereq": 1084, 138 | "preinprog": 1085, 139 | "postinprog": 1086, 140 | "preunprog": 1087, 141 | "postunprog": 1088, 142 | "buildarchs": 1089, 143 | "obsoletes": 1090, 144 | "verifyscriptprog": 1091, 145 | "triggerscriptprog": 1092, 146 | "docdir": 1093, 147 | "cookie": 1094, 148 | "filedevices": 1095, 149 | "fileinodes": 1096, 150 | "filelangs": 1097, 151 | "prefixes": 1098, 152 | "instprefixes": 1099, 153 | "triggerin": 1100, 154 | "triggerun": 1101, 155 | "triggerpostun": 1102, 156 | "autoreq": 1103, 157 | "autoprov": 1104, 158 | "capability": 1105, 159 | "sourcepackage": 1106, 160 | "oldorigfilenames": 1107, 161 | "buildprereq": 1108, 162 | "buildrequires": 1109, 163 | "buildconflicts": 1110, 164 | "buildmacros": 1111, 165 | "provideflags": 1112, 166 | "provideversion": 1113, 167 | "obsoleteflags": 1114, 168 | "obsoleteversion": 1115, 169 | "dirindexes": 1116, 170 | "basenames": 1117, 171 | "dirnames": 1118, 172 | "origdirindexes": 1119, 173 | "origbasenames": 1120, 174 | "origdirnames": 1121, 175 | "optflags": 1122, 176 | "disturl": 1123, 177 | "archive_format": 1124, 178 | "archive_compression": 1125, 179 | "payloadflags": 1126, 180 | "installcolor": 1127, 181 | "installtid": 1128, 182 | "removetid": 1129, 183 | "rhnplatform": 1131, 184 | "target": 1132, 185 | "patchesname": 1133, 186 | "patchesflags": 1134, 187 | "patchesversion": 1135, 188 | "cachectime": 1136, 189 | "cachepkgpath": 1137, 190 | "cachepkgsize": 1138, 191 | "cachepkgmtime": 1139, 192 | "filecolors": 1140, 193 | "fileclass": 1141, 194 | "classdict": 1142, 195 | "filedependsx": 1143, 196 | "filedependsn": 1144, 197 | "dependsdict": 1145, 198 | "sourcepkgid": 1146, 199 | "filecontexts": 1147, 200 | "fscontexts": 1148, 201 | "recontexts": 1149, 202 | "policies": 1150, 203 | "pretrans": 1151, 204 | "posttrans": 1152, 205 | "pretransprog": 1153, 206 | "posttransprog": 1154, 207 | "disttag": 1155, 208 | "oldsuggestsname": 1156, 209 | "oldsuggestsversion": 1157, 210 | "oldsuggestsflags": 1158, 211 | "oldenhancesname": 1159, 212 | "oldenhancesversion": 1160, 213 | "oldenhancesflags": 1161, 214 | "priority": 1162, 215 | "cvsid": 1163, 216 | "blinkpkgid": 1164, 217 | "blinkhdrid": 1165, 218 | "blinknevra": 1166, 219 | "flinkpkgid": 1167, 220 | "flinkhdrid": 1168, 221 | "flinknevra": 1169, 222 | "packageorigin": 1170, 223 | "triggerprein": 1171, 224 | "buildsuggests": 1172, 225 | "buildenhances": 1173, 226 | "scriptstates": 1174, 227 | "scriptmetrics": 1175, 228 | "buildcpuclock": 1176, 229 | "filedigestalgos": 1177, 230 | "variants": 1178, 231 | "xmajor": 1179, 232 | "xminor": 1180, 233 | "repotag": 1181, 234 | "keywords": 1182, 235 | "buildplatforms": 1183, 236 | "packagecolor": 1184, 237 | "packageprefcolor": 1185, 238 | "xattrsdict": 1186, 239 | "filexattrsx": 1187, 240 | "depattrsdict": 1188, 241 | "conflictattrsx": 1189, 242 | "obsoleteattrsx": 1190, 243 | "provideattrsx": 1191, 244 | "requireattrsx": 1192, 245 | "buildprovides": 1193, 246 | "buildobsoletes": 1194, 247 | "dbinstance": 1195, 248 | "nvra": 1196, 249 | "filenames": 5000, 250 | "fileprovide": 5001, 251 | "filerequire": 5002, 252 | "fsnames": 5003, 253 | "fssizes": 5004, 254 | "triggerconds": 5005, 255 | "triggertype": 5006, 256 | "origfilenames": 5007, 257 | "longfilesizes": 5008, 258 | "longsize": 5009, 259 | "filecaps": 5010, 260 | "filedigestalgo": 5011, 261 | "bugurl": 5012, 262 | "evr": 5013, 263 | "nvr": 5014, 264 | "nevr": 5015, 265 | "nevra": 5016, 266 | "headercolor": 5017, 267 | "verbose": 5018, 268 | "epochnum": 5019, 269 | "preinflags": 5020, 270 | "postinflags": 5021, 271 | "preunflags": 5022, 272 | "postunflags": 5023, 273 | "pretransflags": 5024, 274 | "posttransflags": 5025, 275 | "verifyscriptflags": 5026, 276 | "triggerscriptflags": 5027, 277 | "collections": 5029, 278 | "policynames": 5030, 279 | "policytypes": 5031, 280 | "policytypesindexes": 5032, 281 | "policyflags": 5033, 282 | "vcs": 5034, 283 | "ordername": 5035, 284 | "orderversion": 5036, 285 | "orderflags": 5037, 286 | "mssfmanifest": 5038, 287 | "mssfdomain": 5039, 288 | "instfilenames": 5040, 289 | "requirenevrs": 5041, 290 | "providenevrs": 5042, 291 | "obsoletenevrs": 5043, 292 | "conflictnevrs": 5044, 293 | "filenlinks": 5045, 294 | "recommendname": 5046, 295 | "recommendversion": 5047, 296 | "recommendflags": 5048, 297 | "suggestname": 5049, 298 | "suggestversion": 5050, 299 | "suggestflags": 5051, 300 | "supplementname": 5052, 301 | "supplementversion": 5053, 302 | "supplementflags": 5054, 303 | "enhancename": 5055, 304 | "enhanceversion": 5056, 305 | "enhanceflags": 5057, 306 | "recommendnevrs": 5058, 307 | "suggestnevrs": 5059, 308 | "supplementnevrs": 5060, 309 | "enhancenevrs": 5061, 310 | "encoding": 5062, 311 | "filetriggerin": 5063, 312 | "filetriggerun": 5064, 313 | "filetriggerpostun": 5065, 314 | "filetriggerscripts": 5066, 315 | "filetriggerscriptprog": 5067, 316 | "filetriggerscriptflags": 5068, 317 | "filetriggername": 5069, 318 | "filetriggerindex": 5070, 319 | "filetriggerversion": 5071, 320 | "filetriggerflags": 5072, 321 | "transfiletriggerin": 5073, 322 | "transfiletriggerun": 5074, 323 | "transfiletriggerpostun": 5075, 324 | "transfiletriggerscripts": 5076, 325 | "transfiletriggerscriptprog": 5077, 326 | "transfiletriggerscriptflags": 5078, 327 | "transfiletriggername": 5079, 328 | "transfiletriggerindex": 5080, 329 | "transfiletriggerversion": 5081, 330 | "transfiletriggerflags": 5082, 331 | "removepathpostfixes": 5083, 332 | "filetriggerpriorities": 5084, 333 | "transfiletriggerpriorities": 5085, 334 | "filetriggerconds": 5086, 335 | "filetriggertype": 5087, 336 | "transfiletriggerconds": 5088, 337 | "transfiletriggertype": 5089, 338 | "filesignatures": 5090, 339 | "filesignaturelength": 5091, 340 | "payloaddigest": 5092, 341 | "payloaddigestalgo": 5093, 342 | "autoinstalled": 5094, 343 | "identity": 5095, 344 | "modularitylabel": 5096, 345 | "payloaddigestalt": 5097, 346 | "archsuffix": 5098, 347 | "spec": 5099, 348 | "translationurl": 5100, 349 | "upstreamreleases": 5101, 350 | "loaddigestalt": 5097, 351 | "archsuffix": 5098, 352 | "spec": 5099, 353 | "translationurl": 5100, 354 | "upstreamleases": 5101, 355 | "sourcelicense": 5102, 356 | "preuntrans": 5103, 357 | "postuntrans": 5104, 358 | "preuntransprog": 5105, 359 | "postuntransprog": 5106, 360 | "preuntransflags": 5107, 361 | "postuntransflags": 5108, 362 | "sysusers": 5109, 363 | } 364 | 365 | rtags = dict([(value, key) for (key, value) in tags.items()]) 366 | rsigtags = dict([(value, key) for (key, value) in sigtags.items()]) 367 | 368 | 369 | def extract_string(offset, count, store): 370 | if count > 1: 371 | return extract_array(offset, count, store) 372 | assert count == 1 373 | idx = store[offset:].index(b"\x00") 374 | return store[offset : offset + idx] 375 | 376 | 377 | def extract_i18nstring(offset, count, store): 378 | # rpm string header entries can have multiple versions, one for each locale. 379 | # the locale names are defined in the i18n table header entry. For the sake of 380 | # simplicity, take only one locale to use 381 | return store[offset:].split(b"\x00", count)[0] 382 | 383 | 384 | def extract_array(offset, count, store): 385 | return store[offset:].split(b"\x00", count)[:-1] 386 | 387 | 388 | def extract_bin(offset, count, store): 389 | return store[offset : offset + count] 390 | 391 | 392 | def extract_int32(offset, count, store): 393 | values = struct.unpack(b"!" + b"I" * count, store[offset : offset + 4 * count]) 394 | if count == 1: 395 | values = values[0] 396 | return values 397 | 398 | 399 | def extract_int16(offset, count, store): 400 | values = struct.unpack(b"!" + b"H" * count, store[offset : offset + 2 * count]) 401 | if count == 1: 402 | values = values[0] 403 | return values 404 | 405 | 406 | ty_map = { 407 | 3: extract_int16, 408 | 4: extract_int32, 409 | 6: extract_string, 410 | 7: extract_bin, 411 | 8: extract_array, 412 | 9: extract_i18nstring, 413 | } 414 | 415 | 416 | def extract_data(ty, offset, count, store): 417 | extract = ty_map.get(ty) 418 | if extract: 419 | return extract(offset, count, store) 420 | else: 421 | return "could not extract %s" % ty 422 | 423 | 424 | def _readheader(fileobj, is_signature): 425 | char = fileobj.read(1) 426 | while char != b"\x8e": 427 | char = fileobj.read(1) 428 | 429 | if char is None or char == b"": 430 | raise RPMError("reached end of file without finding magic char \x8e") 431 | 432 | magic = b"\x8e" + fileobj.read(2) 433 | from binascii import hexlify 434 | 435 | assert hexlify(magic) == b"8eade8", hexlify(magic) 436 | version = ord(fileobj.read(1)) 437 | 438 | header_start = fileobj.tell() - 4 # -4 for magic 439 | 440 | _ = fileobj.read(4) 441 | 442 | (num_entries,) = struct.unpack(b"!i", fileobj.read(4)) 443 | (header_structure_size,) = struct.unpack(b"!i", fileobj.read(4)) 444 | 445 | header = struct.Struct(b"!iiii") 446 | 447 | entries = [] 448 | for _ in range(num_entries): 449 | entry = header.unpack(fileobj.read(header.size)) 450 | entries.append(entry) 451 | 452 | store = fileobj.read(header_structure_size) 453 | 454 | headers = {} 455 | tagsdict = rsigtags if is_signature else rtags 456 | for tag, ty, offset, count in entries: 457 | key = tagsdict.get(tag, tag) 458 | value = extract_data(ty, offset, count, store) 459 | headers[key] = value 460 | header_end = fileobj.tell() 461 | return (header_start, header_end), headers 462 | 463 | 464 | def get_headers(fileobj): 465 | lead = struct.Struct(b"!4sBBhh66shh16s") 466 | data = fileobj.read(lead.size) 467 | value = lead.unpack(data) 468 | 469 | # signature header 470 | first_range, first_headers = _readheader(fileobj, True) 471 | # main header 472 | second_range, second_headers = _readheader(fileobj, False) 473 | 474 | first_headers.update(second_headers) 475 | 476 | return second_range, first_headers 477 | 478 | 479 | def main(): 480 | with open(sys.argv[1]) as fileobj: 481 | headers = get_headers(fileobj) 482 | pprint(headers) 483 | 484 | 485 | if __name__ == "__main__": 486 | main() 487 | -------------------------------------------------------------------------------- /rpmfile/cpiofile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python -3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright © 2011, 2013 K Richard Pixley 5 | # 6 | # See LICENSE for details. 7 | # 8 | # Time-stamp: <30-Jun-2013 19:07:22 PDT by rich@noir.com> 9 | 10 | """ 11 | Cpiofile is a library which reads and writes unix style 'cpio' format 12 | archives. 13 | 14 | .. todo:: open vs context manager 15 | .. todo:: make is_cpiofile work on fileobj 16 | """ 17 | 18 | from __future__ import unicode_literals, print_function 19 | 20 | __docformat__ = "restructuredtext en" 21 | 22 | __all__ = [ 23 | "CheckSumError", 24 | "CpioError", 25 | "CpioFile", 26 | "CpioMember", 27 | "HeaderError", 28 | "InvalidFileFormat", 29 | "InvalidFileFormatNull", 30 | "is_cpiofile", 31 | "valid_magic", 32 | ] 33 | 34 | import abc 35 | import io 36 | import mmap 37 | import os 38 | import struct 39 | 40 | 41 | class CpioError(Exception): 42 | """Base class for CpioFile exceptions""" 43 | 44 | pass 45 | 46 | 47 | class CheckSumError(CpioError): 48 | """Exception indicating a check sum error""" 49 | 50 | pass 51 | 52 | 53 | class InvalidFileFormat(CpioError): 54 | """Exception indicating a file format error""" 55 | 56 | pass 57 | 58 | 59 | class InvalidFileFormatNull(InvalidFileFormat): 60 | """Exception indicating a null file""" 61 | 62 | pass 63 | 64 | 65 | class HeaderError(CpioError): 66 | """Exception indicating a header error""" 67 | 68 | pass 69 | 70 | 71 | def valid_magic(block): 72 | """predicate indicating whether *block* includes a valid magic number""" 73 | return CpioMember.valid_magic(block) 74 | 75 | 76 | def is_cpiofile(name): 77 | """predicate indicating whether *name* is a valid cpiofile""" 78 | with io.open(name, "rb") as fff: 79 | return valid_magic(fff.read(16)) 80 | 81 | 82 | class StructBase(object): 83 | """ 84 | An abstract base class representing objects which are inherently 85 | based on a struct. 86 | """ 87 | 88 | __metaclass__ = abc.ABCMeta 89 | 90 | coder = None 91 | """ 92 | The :py:class:`struct.Struct` used to encode/decode this object 93 | into a block of memory. This is expected to be overridden by 94 | subclasses. 95 | """ # pylint: disable=W0105 96 | 97 | @property 98 | def size(self): 99 | """ 100 | Exact size in bytes of a block of memory into which is suitable 101 | for packing this instance. 102 | """ 103 | return self.coder.size 104 | 105 | def unpack(self, block): 106 | """convenience function for unpacking""" 107 | return self.unpack_from(block) 108 | 109 | @abc.abstractmethod 110 | def unpack_from(self, block, offset=0): 111 | """ 112 | Set the values of this instance from an in-memory 113 | representation of the struct. 114 | 115 | :param string block: block of memory from which to unpack 116 | :param int offset: optional offset into the memory block from 117 | which to start unpacking 118 | """ 119 | raise NotImplementedError 120 | 121 | def pack(self): 122 | """convenience function for packing""" 123 | block = bytearray(self.size) 124 | self.pack_into(block) 125 | return block 126 | 127 | @abc.abstractmethod 128 | def pack_into(self, block, offset=0): 129 | """ 130 | Store the values of this instance into an in-memory 131 | representation of the file. 132 | 133 | :param string block: block of memory into which to pack 134 | :param int offset: optional offset into the memory block into 135 | which to start packing 136 | """ 137 | raise NotImplementedError 138 | 139 | __hash__ = None 140 | 141 | def __eq__(self, other): 142 | raise NotImplementedError 143 | 144 | def __ne__(self, other): 145 | return not self.__eq__(other) 146 | 147 | def close_enough(self, other): 148 | """ 149 | This is a comparison similar to __eq__ except that here the 150 | goal is to determine whether two objects are "close enough" 151 | despite perhaps having been produced at different times in 152 | different locations in the file system. 153 | """ 154 | return self == other 155 | 156 | 157 | class CpioFile(StructBase): 158 | """Class representing an entire cpio file""" 159 | 160 | _members = [] 161 | 162 | def __init__(self): 163 | self._members = [] 164 | 165 | @property 166 | def members(self): 167 | """accessor for a list of the members of this cpio file""" 168 | return self._members 169 | 170 | @property 171 | def names(self): 172 | """accessor for a list of names of the members of this cpio file""" 173 | return [member.name for member in self.members] 174 | 175 | def __enter__(self): 176 | return self 177 | 178 | def __exit__(self, thingy, value, traceback): 179 | self.close() 180 | 181 | @classmethod 182 | def open(cls, name=None, mode=None): 183 | return cls._open(cls(), name) 184 | 185 | def _open(self, name=None, fileobj=None, mymap=None, block=None): 186 | """ 187 | The _open function takes some form of file identifier and creates 188 | an :py:class:`CpioFile` instance from it. 189 | 190 | :param :py:class:`str` name: a file name 191 | :param :py:class:`file` fileobj: if given, this overrides *name* 192 | :param :py:class:`mmap.mmap` mymap: if given, this overrides *fileobj* 193 | :param :py:class:`bytes` block: file contents in a block of memory, (if given, this overrides *mymap*) 194 | 195 | The file to be used can be specified in any of four different 196 | forms, (in reverse precedence): 197 | 198 | #. a file name 199 | #. :py:class:`file` object 200 | #. :py:mod:`mmap.mmap`, or 201 | #. a block of memory 202 | """ 203 | 204 | if block is not None: 205 | if not name: 206 | name = "" 207 | 208 | self.unpack_from(block) 209 | 210 | if fileobj: 211 | fileobj.close() 212 | 213 | return self 214 | 215 | if mymap is not None: 216 | block = mymap 217 | 218 | elif fileobj: 219 | try: 220 | mymap = mmap.mmap(fileobj.fileno(), 0, mmap.MAP_SHARED, mmap.PROT_READ) 221 | 222 | # pylint: disable=W0702 223 | except: 224 | mymap = 0 225 | block = fileobj.read() 226 | 227 | elif name: 228 | fileobj = io.open(os.path.normpath(os.path.expanduser(name)), "rb") 229 | 230 | else: 231 | assert False 232 | 233 | return self._open(name=name, fileobj=fileobj, mymap=mymap, block=block) 234 | 235 | def close(self): 236 | """noop - here for completeness""" 237 | pass 238 | 239 | def unpack_from(self, block, offset=0): 240 | pointer = offset 241 | print("unpack_from") 242 | while "TRAILER!!!" not in self.names: 243 | cmem = CpioMember.encoded_class(block, pointer)() 244 | print(type(cmem)) 245 | self.members.append(cmem.unpack_from(block, pointer)) 246 | pointer += cmem.size 247 | 248 | del self.members[-1] 249 | 250 | def pack_into(self, block, offset=0): 251 | pointer = offset 252 | 253 | for member in self.members: 254 | member.pack_into(block, pointer) 255 | pointer += member.size 256 | 257 | cmtype = type(self.members[0]) if self.members else CpioMemberNew 258 | cmt = cmtype() 259 | cmt.name = "TRAILER!!!" 260 | cmt.pack_into(block, pointer) 261 | 262 | def get_member(self, name): 263 | """return a member by *name*""" 264 | for member in self.members: 265 | if member.name == name: 266 | return member 267 | 268 | return None 269 | 270 | def __eq__(self, other): 271 | raise NotImplementedError 272 | 273 | 274 | class CpioMember(StructBase): 275 | """class representing a member of a cpio archive""" 276 | 277 | coder = None 278 | 279 | name = None 280 | magic = None 281 | devmajor = None 282 | devminor = None 283 | ino = None 284 | mode = None 285 | uid = None 286 | gid = None 287 | nlink = None 288 | rdevmajor = None 289 | rdevminor = None 290 | mtime = None 291 | filesize = None 292 | 293 | content = None 294 | 295 | @staticmethod 296 | def valid_magic(block, offset=0): 297 | """ 298 | predicate indicating whether a block of memory has a valid magic number 299 | """ 300 | try: 301 | return CpioMember.encoded_class(block, offset) 302 | except InvalidFileFormat: 303 | return False 304 | 305 | @staticmethod 306 | def encoded_class(block, offset=0): 307 | """ 308 | predicate indicating whether a block of memory includes a magic number 309 | """ 310 | if not block: 311 | raise InvalidFileFormatNull 312 | 313 | for key in __magicmap__: 314 | if block.find(key, offset, offset + len(key)) > -1: 315 | return __magicmap__[key] 316 | 317 | raise InvalidFileFormat 318 | 319 | def unpack_from(self, block, offset=0): 320 | ( 321 | self.magic, 322 | dev, 323 | self.ino, 324 | self.mode, 325 | self.uid, 326 | self.gid, 327 | self.nlink, 328 | rdev, 329 | mtimehigh, 330 | mtimelow, 331 | namesize, 332 | filesizehigh, 333 | filesizelow, 334 | ) = self.coder.unpack_from(block, offset) 335 | 336 | self.devmajor = os.major(dev) 337 | self.devminor = os.minor(dev) 338 | self.rdevmajor = os.major(rdev) 339 | self.rdevminor = os.minor(rdev) 340 | 341 | self.mtime = (mtimehigh << 16) | mtimelow 342 | self.filesize = (filesizehigh << 16) | filesizelow 343 | 344 | namestart = offset + self.coder.size 345 | datastart = namestart + namesize 346 | 347 | self.name = block[namestart : datastart - 1] # drop the null 348 | 349 | if isinstance(self, CpioMemberBin) and (namesize & 1): 350 | datastart += 1 # skip a pad byte if necessary 351 | 352 | self.content = block[datastart : datastart + self.filesize] 353 | 354 | return self 355 | 356 | def pack_into(self, block, offset=0): 357 | namesize = len(self.name) 358 | dev = os.makedev(self.devmajor, self.devminor) 359 | rdev = os.makedev(self.rdevmajor, self.rdevminor) 360 | 361 | mtimehigh = self.mtime >> 16 362 | mtimelow = self.mtime & 0xFFFF 363 | 364 | filesizehigh = self.filesize >> 16 365 | filesizelow = self.filesize & 0xFFFF 366 | 367 | self.coder.pack_into( 368 | block, 369 | offset, 370 | self.magic, 371 | dev, 372 | self.ino, 373 | self.mode, 374 | self.uid, 375 | self.gid, 376 | self.nlink, 377 | rdev, 378 | mtimehigh, 379 | mtimelow, 380 | namesize, 381 | filesizehigh, 382 | filesizelow, 383 | ) 384 | 385 | namestart = offset + self.coder.size 386 | datastart = namestart + namesize + 1 387 | 388 | block[namestart : datastart - 1] = self.name 389 | block[datastart - 1] = "\x00" 390 | 391 | if isinstance(self, CpioMemberBin) and (namesize & 1): 392 | datastart += 1 393 | block[datastart - 1] = "\x00" 394 | 395 | block[datastart : datastart + self.filesize] = self.content 396 | 397 | if isinstance(self, CpioMemberBin) and (self.filesize & 1): 398 | block[datastart + self.filesize] = "\x00" 399 | 400 | return self 401 | 402 | @property 403 | def size(self): 404 | return self.coder.size + len(self.name) + 1 + self.filesize 405 | 406 | def __repr__(self): 407 | return ( 408 | b"<{0}@{1}: coder={2}, name='{3}', magic='{4}'" 409 | + ", devmajor={5}, devminor={6}, ino={7}, mode={8}" 410 | + ", uid={9}, gid={10}, nlink={11}, rdevmajor={12}" 411 | + ", rdevmino={13}, mtime={14}, filesize={15}>".format( 412 | self.__class__.__name__, 413 | hex(id(self)), 414 | self.coder, 415 | self.name, 416 | self.magic, 417 | self.devmajor, 418 | self.devminor, 419 | self.ino, 420 | self.mode, 421 | self.uid, 422 | self.gid, 423 | self.nlink, 424 | self.rdevmajor, 425 | self.rdevminor, 426 | self.mtime, 427 | self.filesize, 428 | ) 429 | ) 430 | 431 | def __eq__(self, other): 432 | return ( 433 | isinstance(other, self.__class__) 434 | and self.coder == other.coder 435 | and self.magic == other.magic 436 | and self.devmajor == other.devmajor 437 | and self.devminor == other.devminor 438 | and self.ino == other.ino 439 | and self.mode == other.mode 440 | and self.uid == other.uid 441 | and self.gid == other.gid 442 | and self.nlink == other.nlink 443 | and self.rdevmajor == other.rdevmajor 444 | and self.rdevminor == other.rdevminor 445 | and self.mtime == other.mtime 446 | and self.filesize == other.filesize 447 | ) 448 | 449 | close_enough = __eq__ 450 | 451 | 452 | class CpioMemberBin(CpioMember): 453 | """intermediate class indicating binary members - for subclassing only""" 454 | 455 | @property 456 | def size(self): 457 | namesize = len(self.name) + 1 # add null 458 | 459 | retval = self.coder.size 460 | retval += namesize 461 | 462 | if isinstance(self, CpioMemberBin) and (namesize & 1): 463 | retval += 1 464 | 465 | retval += self.filesize 466 | 467 | if isinstance(self, CpioMemberBin) and (self.filesize & 1): 468 | retval += 1 469 | 470 | return retval 471 | 472 | 473 | class CpioMember32b(CpioMemberBin): 474 | """ 475 | .. todo:: need to pad after name and after content for old binary. 476 | """ 477 | 478 | coder = struct.Struct(b">2sHHHHHHHHHHHH") 479 | 480 | 481 | class CpioMember32l(CpioMemberBin): 482 | """class representing a 32bit little endian binary member""" 483 | 484 | coder = struct.Struct(b"<2sHHHHHHHHHHHH") 485 | 486 | 487 | class CpioMemberODC(CpioMember): 488 | """class representing an ODC member""" 489 | 490 | coder = struct.Struct(b"=6s6s6s6s6s6s6s6s11s6s11s") 491 | 492 | def unpack_from(self, block, offset=0): 493 | ( 494 | self.magic, 495 | dev, 496 | ino, 497 | mode, 498 | uid, 499 | gid, 500 | nlink, 501 | rdev, 502 | mtime, 503 | namesize, 504 | filesize, 505 | ) = self.coder.unpack_from(block, offset) 506 | _namesize = namesize 507 | self.ino = int(ino, 8) 508 | self.mode = int(mode, 8) 509 | self.uid = int(uid, 8) 510 | self.gid = int(gid, 8) 511 | self.nlink = int(nlink, 8) 512 | 513 | dev = int(dev, 8) 514 | rdev = int(rdev, 8) 515 | self.devmajor = os.major(dev) 516 | self.devminor = os.minor(dev) 517 | self.rdevmajor = os.major(rdev) 518 | self.rdevminor = os.minor(rdev) 519 | 520 | self.mtime = int(mtime, 8) 521 | namesize = int(namesize, 8) 522 | self.filesize = int(filesize, 8) 523 | 524 | namestart = offset + self.coder.size 525 | datastart = namestart + namesize 526 | 527 | self.name = block[namestart : datastart - 1] # drop the null 528 | print("+", _namesize, self.name) 529 | self.content = block[datastart : datastart + self.filesize] 530 | 531 | return self 532 | 533 | def pack_into(self, block, offset=0): 534 | dev = os.makedev(self.devmajor, self.devminor) 535 | ino = str(self.ino) 536 | mode = str(self.mode) 537 | uid = str(self.uid) 538 | gid = str(self.gid) 539 | nlink = str(self.nlink) 540 | rdev = os.makedev(self.rdevmajor, self.rdevminor) 541 | mtime = str(self.mtime) 542 | namesize = str(len(self.name) + 1) # add a null 543 | filesize = str(self.filesize) 544 | 545 | self.coder.pack_into( 546 | block, 547 | offset, 548 | self.magic, 549 | dev, 550 | ino, 551 | mode, 552 | uid, 553 | gid, 554 | nlink, 555 | rdev, 556 | mtime, 557 | namesize, 558 | filesize, 559 | ) 560 | 561 | namesize = len(self.name) + 1 562 | 563 | namestart = offset + self.coder.size 564 | datastart = namestart + namesize 565 | 566 | block[namestart : datastart - 2] = self.name 567 | block[datastart - 1] = "\x00" 568 | block[datastart : datastart + self.filesize] = self.content 569 | 570 | return self 571 | 572 | 573 | class CpioMemberNew(CpioMember): 574 | """class representing a new member""" 575 | 576 | coder = struct.Struct(b"6s8s8s8s8s8s8s8s8s8s8s8s8s8s") 577 | 578 | # pylint: disable=W0613 579 | @staticmethod 580 | def _checksum(block, offset, length): 581 | """return a checksum for *block* at *offset* and *length*""" 582 | return 0 583 | 584 | # pylint: enable=W0613 585 | 586 | def unpack_from(self, block, offset=0): 587 | unpacks = self.coder.unpack_from(block, offset) 588 | 589 | self.magic = unpacks[0] 590 | 591 | self.ino = int(unpacks[1], 16) 592 | self.mode = int(unpacks[2], 16) 593 | self.uid = int(unpacks[3], 16) 594 | self.gid = int(unpacks[4], 16) 595 | self.nlink = int(unpacks[5], 16) 596 | 597 | self.mtime = int(unpacks[6], 16) 598 | self.filesize = int(unpacks[7], 16) 599 | 600 | self.devmajor = int(unpacks[8], 16) 601 | self.devminor = int(unpacks[9], 16) 602 | self.rdevmajor = int(unpacks[10], 16) 603 | self.rdevminor = int(unpacks[11], 16) 604 | 605 | namesize = int(unpacks[12], 16) 606 | check = int(unpacks[13], 16) 607 | 608 | namestart = offset + self.coder.size 609 | nameend = namestart + namesize 610 | datastart = nameend + ((4 - (nameend % 4)) % 4) # pad 611 | dataend = datastart + self.filesize 612 | 613 | self.name = block[namestart : nameend - 1] # drop the null 614 | print("name", namesize, self.name) 615 | print("pad", ((4 - (nameend % 4)) % 4)) # pad 616 | self.content = block[datastart:dataend] 617 | 618 | if check != self._checksum(self.content, 0, self.filesize): 619 | raise CheckSumError 620 | 621 | return self 622 | 623 | def pack_into(self, block, offset=0): 624 | namesize = len(self.name) + 1 625 | # unused: rdev = os.makedev(self.rdevmajor, self.rdevminor) 626 | self.coder.pack_into( 627 | block, 628 | offset, 629 | self.magic, 630 | str(self.ino), 631 | str(self.mode), 632 | str(self.uid), 633 | str(self.gid), 634 | str(self.nlink), 635 | str(self.mtime), 636 | str(self.filesize), 637 | str(self.devmajor), 638 | str(self.devminor), 639 | str(self.rdevmajor), 640 | str(self.rdevminor), 641 | str(namesize), 642 | self._checksum(self.content, 0, self.filesize), 643 | ) 644 | 645 | namestart = offset + self.coder.size 646 | nameend = namestart + namesize 647 | datastart = nameend + ((4 - (nameend % 4)) % 4) # pad 648 | dataend = datastart + self.filesize 649 | 650 | block[namestart:nameend] = self.name 651 | 652 | for i in range(nameend, datastart): 653 | block[i] = "\x00" 654 | 655 | block[datastart:dataend] = self.content 656 | 657 | padend = dataend + ((4 - (datastart % 4)) % 4) # pad 658 | for i in range(dataend, padend): 659 | block[i] = "\x00" 660 | 661 | return self 662 | 663 | @property 664 | def size(self): 665 | retval = self.coder.size 666 | retval += len(self.name) + 1 667 | retval += (4 - (retval % 4)) % 4 668 | retval += self.filesize 669 | retval += (4 - (retval % 4)) % 4 670 | return retval 671 | 672 | 673 | class CpioMemberCRC(CpioMemberNew): 674 | """class representing a cpio archive member with a CRC""" 675 | 676 | @staticmethod 677 | def _checksum(block, offset, length): 678 | csum = 0 679 | 680 | for i in range(length): 681 | csum += ord(block[offset + i]) 682 | 683 | return csum & 0xFFFFFFFF 684 | 685 | 686 | __magicmap__ = { 687 | b"\x71\xc7": CpioMember32b, 688 | b"\xc7\x71": CpioMember32l, 689 | b"070707": CpioMemberODC, 690 | b"070701": CpioMemberNew, 691 | b"070702": CpioMemberCRC, 692 | } 693 | --------------------------------------------------------------------------------