Repository: srossross/rpmfile

Issue:
LZMA RPM support
Can you please add support for LZMA RPMs? It seems like Python 3's `lzma` module and Python 2's `backports.lzma` support the format fine. However, the library currently assumes that the RPM is gzip.


Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 3072e6f..d2a551d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 *.pyc
-*.pyo
\ No newline at end of file
+*.pyor
+*.egg-info/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..eaec3ae
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,8 @@
+language: python
+python:
+  - "2.7"
+  - "3.4"
+  - "3.5"
+  - "3.6"
+script:
+  - python setup.py test
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..67f043d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2015 Sean Ross-Ross
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 9adcc09..780801c 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,34 @@
-rpmfile
-========
-
-Tools for inspecting RPM files in python. This module is modeled after the tarfile module.
-
-## Example 
-
-    import rpmfile
-    
-    with rpmfile.open('file.rpm') as rpm:
-    
-        #Inspect the RPM headers
-        print rpm.headers.keys()
-        print rpm.headers.get('arch', 'noarch')
-        
-        #Extract a fileobject from the archive
-        fd = rpm.extractfile('./usr/bin/script')
-        print fd.read() 
-        
-        for member in rpm.getmembers():
-            print member
-        
+# rpmfile
 
-## Classes
-    
-    * rpmfile.RPMFile: The RPMFile object provides an interface to a RPM archive
-    * rpmfile.RPMInfo: An RPMInfo object represents one member in a RPMFile.
+Tools for inspecting RPM files in python. This module is modeled after the
+tarfile module.
 
+## Example
 
-## Code in this module was borrowed from:
+```python
+import rpmfile
+
+with rpmfile.open('file.rpm') as rpm:
+
+    # Inspect the RPM headers
+    print(rpm.headers.keys())
+    print(rpm.headers.get('arch', 'noarch'))
+
+    # Extract a fileobject from the archive
+    fd = rpm.extractfile('./usr/bin/script')
+    print(fd.read())
 
-    * https://bitbucket.org/krp/cpiofile
-    * https://github.com/mjvm/pyrpm
+    for member in rpm.getmembers():
+        print(member)
+```
 
 
+## Classes
+
+* rpmfile.RPMFile: The RPMFile object provides an interface to a RPM archive
+* rpmfile.RPMInfo: An RPMInfo object represents one member in a RPMFile.
+
+## Code in this module was borrowed from:
 
+* https://bitbucket.org/krp/cpiofile
+* https://github.com/mjvm/pyrpm
diff --git a/rpmfile/__init__.py b/rpmfile/__init__.py
index b90280d..fe8477c 100644
--- a/rpmfile/__init__.py
+++ b/rpmfile/__init__.py
@@ -4,6 +4,10 @@ from .headers import get_headers
 import sys
 import io
 import gzip
+try:
+    import lzma
+except ImportError:
+    pass
 import struct
 from rpmfile import cpiofile
 from functools import wraps
@@ -11,6 +15,9 @@ from rpmfile.io_extra import _SubFile
 
 pad = lambda fileobj: (4 - (fileobj.tell() % 4)) % 4
 
+class NoLZMAModuleError(NotImplementedError):
+    pass
+
 class RPMInfo(object):
     '''
     Informational class which holds the details about an
@@ -38,7 +45,7 @@ class RPMInfo(object):
 
     @classmethod
     def _read(cls, magic, fileobj):
-        if magic == '070701':
+        if magic == b'070701':
             return cls._read_new(fileobj, magic=magic)
         else:
             raise Exception('bad magic number %r' % magic)
@@ -51,7 +58,7 @@ class RPMInfo(object):
         d = coder.unpack_from(fileobj.read(coder.size))
 
         namesize = int(d[11], 16)
-        name = fileobj.read(namesize)[:-1]
+        name = fileobj.read(namesize)[:-1].decode('utf-8')
         fileobj.seek(pad(fileobj), 1)
         file_start = fileobj.tell()
         file_size = int(d[6], 16)
@@ -108,10 +115,10 @@ class RPMFile(object):
         '''
         if self._members is None:
             self._members = _members = []
-            g = self.gzip_file
+            g = self.data_file
             magic = g.read(2)
             while magic:
-                if magic == '07':
+                if magic == b'07':
                     magic += g.read(4)
                     member = RPMInfo._read(magic, g)
 
@@ -148,17 +155,25 @@ class RPMFile(object):
         '''
         if not isinstance(member, RPMInfo):
             member = self.getmember(member)
-        return _SubFile(self.gzip_file, member.file_start, member.size)
+        return _SubFile(self.data_file, member.file_start, member.size)
 
-    _gzip_file = None
+    _data_file = None
 
     @property
-    def gzip_file(self):
-        'Return the uncompressed raw CPIO data of the RPM archive'
-        if self._gzip_file is None:
+    def data_file(self):
+        """Return the uncompressed raw CPIO data of the RPM archive."""
+
+        if self._data_file is None:
             fileobj = _SubFile(self._fileobj, self.data_offset)
-            self._gzip_file = gzip.GzipFile(fileobj=fileobj)
-        return self._gzip_file
+
+            if self.headers["archive_compression"] == b"xz":
+                if not getattr(sys.modules[__name__], 'lzma', False):
+                    raise NoLZMAModuleError('lzma module not present')
+                self._data_file = lzma.LZMAFile(fileobj)
+            else:
+                self._data_file = gzip.GzipFile(fileobj=fileobj)
+
+        return self._data_file
 
 def open(name=None, mode='rb', fileobj=None):
     '''
diff --git a/rpmfile/headers.py b/rpmfile/headers.py
index 4a9194c..f4bee2e 100644
--- a/rpmfile/headers.py
+++ b/rpmfile/headers.py
@@ -103,13 +103,7 @@ def extract_string(offset, count, store):
     return store[offset:offset + idx]
 
 def extract_array(offset, count, store):
-    a = []
-    for _ in range(count):
-        idx = store[offset:].index(b'\x00')
-        value = store[offset:offset + idx]
-        a.append(value)
-        offset = offset + idx + 1
-    return a
+    return store[offset:].split(b'\x00', count)[:-1]
 
 def extract_bin(offset, count, store):
     return store[offset:offset + count]
@@ -148,7 +142,7 @@ def _readheader(fileobj):
     while char != b'\x8e':
         char = fileobj.read(1)
 
-        if char is None:
+        if char is None or char == '':
             raise RPMError("reached end of file without finding magic char \x8e")
 
     magic = b'\x8e' + fileobj.read(2)
diff --git a/rpmfile/rpmdefs.py b/rpmfile/rpmdefs.py
index df3d453..5d39ee5 100644
--- a/rpmfile/rpmdefs.py
+++ b/rpmfile/rpmdefs.py
@@ -4,7 +4,7 @@
 '''
 rpm definitions
 
-$Id$
+Mario Morgado (BSD) https://github.com/mjvm/pyrpm
 '''
 __revision__ = '$Rev$'[6:-2]
 
diff --git a/setup.py b/setup.py
index 851277e..185e319 100644
--- a/setup.py
+++ b/setup.py
@@ -6,18 +6,20 @@ from setuptools import setup, find_packages
 from os.path import isfile
 
 if isfile('README.md'):
-    long_description = open('README.md').read()
+    with open('README.md') as readme:
+        long_description = readme.read()
 else:
     long_description = '???'
 
 setup(
     name='rpmfile',
-    description='Read rmp archive files',
-    version="0.1.4",
+    description='Read rpm archive files',
+    version="0.1.5",
     author='Sean Ross-Ross',
     author_email='srossross@gmail.com',
     url='https://github.com/srossross/rpmfile',
     license='MIT',
     long_description=long_description,
+    long_description_content_type='text/markdown',
     packages=find_packages(),
 )


Test patch:
diff --git a/rpmfile/tests/__init__.py b/tests/__init__.py
similarity index 100%
rename from rpmfile/tests/__init__.py
rename to tests/__init__.py
diff --git a/tests/test_extract.py b/tests/test_extract.py
new file mode 100644
index 0000000..efeafcb
--- /dev/null
+++ b/tests/test_extract.py
@@ -0,0 +1,60 @@
+import os
+import sys
+import shutil
+import hashlib
+import tempfile
+import unittest
+from functools import wraps
+try:
+    from urllib2 import urlopen
+except ImportError:
+    from urllib.request import urlopen
+
+import rpmfile
+
+def download(url, rpmname):
+    def _downloader(func):
+        @wraps(func)
+        def wrapper(*args, **kwds):
+            args = list(args)
+            rpmpath = os.path.join(args[0].tempdir, rpmname)
+            args.append(rpmpath)
+            download = urlopen(url)
+            with open(rpmpath, 'wb') as target_file:
+                target_file.write(download.read())
+            download.close()
+            return func(*args, **kwds)
+        return wrapper
+    return _downloader
+
+class TempDirTest(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.prevdir = os.getcwd()
+        cls.tempdir = tempfile.mkdtemp()
+        os.chdir(cls.tempdir)
+
+    @classmethod
+    def tearDownClass(cls):
+        shutil.rmtree(cls.tempdir)
+        os.chdir(cls.prevdir)
+
+    @unittest.skipUnless(sys.version_info.major >= 3 \
+                            and sys.version_info.minor >= 3,
+                         'Need lzma module')
+    @download('https://download.clearlinux.org/releases/10540/clear/x86_64/os/Packages/sudo-setuid-1.8.17p1-34.x86_64.rpm', 'sudo.rpm')
+    def test_lzma_sudo(self, rpmpath):
+        with rpmfile.open(rpmpath) as rpm:
+
+            # Inspect the RPM headers
+            self.assertIn('name', rpm.headers.keys())
+            self.assertEqual(rpm.headers.get('arch', 'noarch'), b'x86_64')
+
+            members = list(rpm.getmembers())
+            self.assertEqual(len(members), 1)
+
+            fd = rpm.extractfile('./usr/bin/sudo')
+
+            calculated = hashlib.md5(fd.read()).hexdigest()
+            self.assertEqual(calculated, 'a208f3d9170ecfa69a0f4ccc78d2f8f6')