├── 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 | [](https://travis-ci.org/srossross/rpmfile)
4 | [](https://github.com/srossross/rpmfile/actions)
5 | [](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 |
--------------------------------------------------------------------------------