Repository: mhe/pynrrd

Issue:
read_data(_, file) closes file prematurely
I'm receiving NRRD files over a network. I want to use pynrrd to parse and validate those files while they're in memory before they've been saved to disk.

I am passing in a file object to `nrrd.read_data()`. I want to keep using that file object afterwards but `nrrd.read_data(_, file)` closes the file. I can't reopen the file after passing it to `nrrd.read_data()` because I haven't yet persisted it to disk. Once the file has been closed, it's gone.

Minimal reproduction using Flask:

```python3
from flask import Flask, request
import nrrd

app = Flask(__name__)

@app.route("/process", methods=["POST"])
def process_file():
    file = request.files["file"].stream

    is_file_ok = check_nrrd(file)

    if not is_file_ok:
        return "some error", 400

    file.seek(0)
    do_something_with_file(file)

    return "uploaded"

def check_nrrd(file):
    """Check that the NRRD conforms to our expectations"""
    header = nrrd.read_header(file)
    data = nrrd.read_data(header, file)

    # perform some checks
    file_is_good = ...
    
    return file_is_good

def do_something_with_file(file):
    print(file.read())
```

Tested with:
```
$ curl -F 'file=@test.nrrd' http://localhost:5000/process
```

Log:
```
127.0.0.1 - - [17/Jan/2025 17:23:11] "POST /process HTTP/1.1" 500 -
[2025-01-17 17:28:39,601] ERROR in app: Exception on /process [POST]
Traceback (most recent call last):
  File "/Users/JackRiccomini/.pyenv/versions/3.12.7/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/JackRiccomini/.pyenv/versions/3.12.7/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
    rv = self.handle_user_exception(e)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/JackRiccomini/.pyenv/versions/3.12.7/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/JackRiccomini/.pyenv/versions/3.12.7/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/private/tmp/min.py", line 15, in process_file
    file.seek(0)
  File "/Users/JackRiccomini/.pyenv/versions/3.12.7/lib/python3.12/tempfile.py", line 828, in seek
    return self._file.seek(*args)
           ^^^^^^^^^^^^^^^^^^^^^^
ValueError: I/O operation on closed file.
```

Discussion:

Solution patch:
diff --git a/nrrd/reader.py b/nrrd/reader.py
index 4be406b..6692f5b 100644
--- a/nrrd/reader.py
+++ b/nrrd/reader.py
@@ -387,7 +387,8 @@ def read_data(header: NRRDHeader, fh: Optional[IO] = None, filename: Optional[st
     else:
         # Must close the file because if the file was opened above from detached filename, there is no "with" block to
         # close it for us
-        fh.close()
+        if data_filename is not None:
+            fh.close()
 
         raise NRRDError('Invalid lineskip, allowed values are greater than or equal to 0')
 
@@ -395,7 +396,8 @@ def read_data(header: NRRDHeader, fh: Optional[IO] = None, filename: Optional[st
     if byte_skip < -1:
         # Must close the file because if the file was opened above from detached filename, there is no "with" block to
         # close it for us
-        fh.close()
+        if data_filename is not None:
+            fh.close()
 
         raise NRRDError('Invalid byteskip, allowed values are greater than or equal to -1')
     elif byte_skip >= 0:
@@ -428,7 +430,8 @@ def read_data(header: NRRDHeader, fh: Optional[IO] = None, filename: Optional[st
         else:
             # Must close the file because if the file was opened above from detached filename, there is no "with" block
             # to close it for us
-            fh.close()
+            if data_filename is not None:
+                fh.close()
 
             raise NRRDError(f'Unsupported encoding: {header["encoding"]}')
 
@@ -461,8 +464,9 @@ def read_data(header: NRRDHeader, fh: Optional[IO] = None, filename: Optional[st
         # NumPy
         data = np.frombuffer(decompressed_data[byte_skip:], dtype)
 
-    # Close the file, even if opened using "with" block, closing it manually does not hurt
-    fh.close()
+    # Close the file if we opened it
+    if data_filename is not None:
+        fh.close()
 
     if total_data_points != data.size:
         raise NRRDError(f'Size of the data does not equal the product of all the dimensions: '


Test patch:
diff --git a/nrrd/tests/test_reading.py b/nrrd/tests/test_reading.py
index ba3b9da..4721458 100644
--- a/nrrd/tests/test_reading.py
+++ b/nrrd/tests/test_reading.py
@@ -517,6 +517,12 @@ def test_read_space_directions_list(self):
             finally:
                 nrrd.SPACE_DIRECTIONS_TYPE = 'double matrix'
 
+        def test_file_parameter_not_closed(self):
+            with open(RAW_NRRD_FILE_PATH, 'rb') as fh:
+                header = nrrd.read_header(fh)
+                nrrd.read_data(header, fh)
+                self.assertFalse(fh.closed)
+
 
 class TestReadingFunctionsFortran(Abstract.TestReadingFunctions):
     index_order = 'F'