Repository: pydicom/pylibjpeg-rle (language: python)

Issue:
Error decoding segment with excess padding
See https://github.com/pydicom/pydicom/issues/1438

```
Traceback (most recent call last):
  File ".../rle/utils.py", line 45, in decode_pixel_data
    decode_frame(src, ds.Rows * ds.Columns, ds.BitsAllocated, byteorder),
ValueError: The end of the frame was reached before the segment was completely decoded
```


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/release-wheels.yml b/.github/workflows/release-wheels.yml
index cb3b784..6d448db 100644
--- a/.github/workflows/release-wheels.yml
+++ b/.github/workflows/release-wheels.yml
@@ -9,12 +9,12 @@ jobs:
     name: Build wheels for ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     env:
-      CIBW_SKIP: "cp35-*"
+      CIBW_SKIP: "cp36-*"
     strategy:
       fail-fast: false
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
-        python-version: [3.9]
+        python-version: ["3.10"]
 
     steps:
     - uses: actions/checkout@v2
@@ -35,7 +35,7 @@ jobs:
     - name: Install requirements
       run: |
         pip install -U pip
-        pip install cibuildwheel==1.10.0
+        pip install cibuildwheel==2.3.1
         pip install setuptools-rust
 
     - name: Build sdist
diff --git a/README.md b/README.md
index 6387e62..4ae1fd6 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 
 ## pylibjpeg-rle
 
-A fast DICOM ([PackBits](https://en.wikipedia.org/wiki/PackBits)) RLE plugin for [pylibjpeg](https://github.com/pydicom/pylibjpeg), written in Rust with a Python 3.6+ wrapper.
+A fast DICOM ([PackBits](https://en.wikipedia.org/wiki/PackBits)) RLE plugin for [pylibjpeg](https://github.com/pydicom/pylibjpeg), written in Rust with a Python 3.7+ wrapper.
 
 Linux, MacOS and Windows are all supported.
 
diff --git a/docs/release_notes/v1.2.0.rst b/docs/release_notes/v1.2.0.rst
new file mode 100644
index 0000000..a437946
--- /dev/null
+++ b/docs/release_notes/v1.2.0.rst
@@ -0,0 +1,14 @@
+.. _v1.2.0:
+
+1.2.0
+=====
+
+Enhancements
+............
+
+* Support decoding segments with non-conformant padding (:issue:`7`)
+
+Changes
+.......
+
+* Support for Python 3.6 has been dropped, while Python 3.10 is now supported
diff --git a/rle/_version.py b/rle/_version.py
index b5cccd7..7d11fa0 100644
--- a/rle/_version.py
+++ b/rle/_version.py
@@ -3,7 +3,7 @@
 import re
 
 
-__version__ = '1.1.0'
+__version__ = '1.2.0'
 
 
 VERSION_PATTERN = r"""
diff --git a/setup.py b/setup.py
index 1a678c1..76458da 100644
--- a/setup.py
+++ b/setup.py
@@ -1,21 +1,15 @@
 
-import os
-import sys
+from pathlib import Path
 from setuptools import setup, find_packages
 from setuptools_rust import Binding, RustExtension
 
-import subprocess
-from distutils.command.build import build as build_orig
-import distutils.sysconfig
 
+VERSION_FILE = Path(__file__).parent / "rle" / '_version.py'
+with open(VERSION_FILE) as f:
+    exec(f.read())
 
-
-VERSION_FILE = os.path.join('rle', '_version.py')
-with open(VERSION_FILE) as fp:
-    exec(fp.read())
-
-with open('README.md', 'r') as fp:
-    long_description = fp.read()
+with open('README.md', 'r') as f:
+    long_description = f.read()
 
 setup(
     name = 'pylibjpeg-rle',
@@ -39,15 +33,13 @@
         "Intended Audience :: Developers",
         "Intended Audience :: Healthcare Industry",
         "Intended Audience :: Science/Research",
-        #"Development Status :: 3 - Alpha",
-        #"Development Status :: 4 - Beta",
         "Development Status :: 5 - Production/Stable",
         "Natural Language :: English",
         "Programming Language :: Rust",
-        "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
         "Operating System :: MacOS :: MacOS X",
         "Operating System :: POSIX :: Linux",
         "Operating System :: Microsoft :: Windows",
@@ -58,7 +50,7 @@
     package_data = {'': ['*.txt', '*.rs', '*.pyx']},
     include_package_data = True,
     zip_safe = False,
-    python_requires = ">=3.6",
+    python_requires = ">=3.7",
     setup_requires = ['setuptools>=18.0', 'setuptools-rust'],
     install_requires = ["numpy"],
     extras_require = {
diff --git a/src/lib.rs b/src/lib.rs
index d6e099f..cc34561 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -312,12 +312,12 @@ fn _decode_segment_into_frame(
             completely decoded"
         ).into()
     );
-    let err_eof = Err(
-        String::from(
-            "The end of the frame was reached before the segment was \
-            completely decoded"
-        ).into()
-    );
+    // let err_eof = Err(
+    //     String::from(
+    //         "The end of the frame was reached before the segment was \
+    //         completely decoded"
+    //     ).into()
+    // );
 
     loop {
         // `header_byte` is equivalent to N in the DICOM Standard
@@ -329,12 +329,21 @@ fn _decode_segment_into_frame(
             // however since using uint8 instead of int8 this will be
             // (256 - N + 1) times
             op_len = 257 - header_byte;
-            // Check we have enough encoded data and remaining frame
-            if (pos > max_offset) || (idx + op_len) > max_frame {
-                match pos > max_offset {
-                    true => return err_eod,
-                    false => return err_eof
+
+            // Check we have enough encoded data
+            if pos > max_offset {
+                return err_eod
+            }
+
+            // Check segment for excess padding
+            if (idx + op_len) > max_frame {
+                // Only copy until we reach the end of frame
+                for _ in 0..(max_frame - idx) {
+                    dst[idx] = src[pos];
+                    idx += bpp;
                 }
+
+                return Ok((idx - initial_offset) / bpp)
             }
 
             for _ in 0..op_len {
@@ -345,12 +354,21 @@ fn _decode_segment_into_frame(
         } else if header_byte < 128 {
             // Extend by literally copying the next (N + 1) bytes
             op_len = header_byte + 1;
-            // Check we have enough encoded data and remaining frame
-            if ((pos + header_byte) > max_offset) || (idx + op_len > max_frame) {
-                match (pos + header_byte) > max_offset {
-                    true => return err_eod,
-                    false => return err_eof
+
+            // Check we have enough encoded data
+            if (pos + header_byte) > max_offset {
+                return err_eod
+            }
+
+            // Check segment for excess padding
+            if (idx + op_len) > max_frame {
+                // Only extend until the end of frame
+                for ii in pos..pos + (max_frame - idx) {
+                    dst[idx] = src[ii];
+                    idx += bpp;
                 }
+
+                return Ok((idx - initial_offset) / bpp)
             }
 
             for ii in pos..pos + op_len {


Test patch:
diff --git a/.github/workflows/pytest-builds.yml b/.github/workflows/pytest-builds.yml
index 0d10e57..83eaa6d 100644
--- a/.github/workflows/pytest-builds.yml
+++ b/.github/workflows/pytest-builds.yml
@@ -13,7 +13,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9]
+        python-version: [3.7, 3.8, 3.9, "3.10"]
         os: [ubuntu-latest, windows-latest, macos-latest]
 
     steps:
diff --git a/rle/tests/test_decode.py b/rle/tests/test_decode.py
index 944cd6b..0a2f905 100644
--- a/rle/tests/test_decode.py
+++ b/rle/tests/test_decode.py
@@ -136,25 +136,15 @@ def test_invalid_samples_px_raises(self):
         with pytest.raises(ValueError, match=msg):
             decode_frame(d + b'\x00' * 8, 1, 8, '<')
 
-    def test_insufficient_frame_literal_raises(self):
-        """Test exception if frame not large enough to hold segment on lit."""
-        msg = (
-            r"The end of the frame was reached before the segment was "
-            r"completely decoded"
-        )
+    def test_insufficient_frame_literal(self):
+        """Test segment with excess padding on lit."""
         d = self.as_bytes([64])
-        with pytest.raises(ValueError, match=msg):
-            decode_frame(d + b'\x00' * 8, 1, 8, '<')
+        assert decode_frame(d + b'\x00' * 8, 1, 8, '<') == b"\x00"
 
-    def test_insufficient_frame_copy_raises(self):
-        """Test exception if frame not large enough to hold segment on copy."""
-        msg = (
-            r"The end of the frame was reached before the segment was "
-            r"completely decoded"
-        )
+    def test_insufficient_frame_copy(self):
+        """Test segment withe excess padding on copy."""
         d = self.as_bytes([64])
-        with pytest.raises(ValueError, match=msg):
-            decode_frame(d + b'\xff\x00\x00', 1, 8, '<')
+        assert decode_frame(d + b'\xff\x00\x00', 1, 8, '<') == b"\x00"
 
     def test_insufficient_segment_copy_raises(self):
         """Test exception if insufficient segment data on copy."""
@@ -844,3 +834,12 @@ def test_generator(self):
         assert (600, 800) == arr.shape
         with pytest.raises(StopIteration):
             next(gen)
+
+    def test_multi_sample(self):
+        ds = deepcopy(INDEX["SC_rgb_rle_16bit.dcm"]['ds'])
+
+        gen = generate_frames(ds, reshape=True)
+        arr = next(gen)
+        assert (100, 100, 3) == arr.shape
+        with pytest.raises(StopIteration):
+            next(gen)
diff --git a/rle/tests/test_encode.py b/rle/tests/test_encode.py
index 258ed02..fc25acf 100644
--- a/rle/tests/test_encode.py
+++ b/rle/tests/test_encode.py
@@ -245,8 +245,7 @@ def test_cycle(self, _, ds, nr_frames):
         encoded = encode_frame(ds.PixelData, *params, '<')
         decoded = _rle_decode_frame(encoded, *params)
 
-        dtype = pixel_dtype(ds).newbyteorder('>')
-        arr = np.frombuffer(decoded, dtype)
+        arr = np.frombuffer(decoded, pixel_dtype(ds))
 
         if ds.SamplesPerPixel == 1:
             arr = arr.reshape(ds.Rows, ds.Columns)
diff --git a/rle/tests/test_utils.py b/rle/tests/test_utils.py
index 1d9e6a5..b88ff47 100644
--- a/rle/tests/test_utils.py
+++ b/rle/tests/test_utils.py
@@ -153,6 +153,13 @@ def test_bad_byteorder_raises(self):
         with pytest.raises(ValueError, match=msg):
             encode_pixel_data(b'', **kwargs)
 
+    def test_encode_using_dataset(self):
+        """Test encoding using a dataset"""
+        ds = INDEX_LEE["SC_rgb_32bit_2frame.dcm"]['ds']
+        src = ds.pixel_array[0].tobytes()
+        enc = encode_pixel_data(src, ds, "<")
+        assert enc[:10] == b"\x0C\x00\x00\x00\x40\x00\x00\x00\x08\x01"
+
     def test_no_byteorder_u8(self):
         """Test exception raised if invalid byteorder."""
         kwargs = {