Skip to content

Commit 02c5ea9

Browse files
authored
Fix remote dask orientation guard (#1794) (#1802)
1 parent 56cc261 commit 02c5ea9

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

xrspatial/geotiff/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2186,6 +2186,13 @@ def read_geotiff_dask(source: str, *,
21862186
finally:
21872187
_src.close()
21882188
http_meta = (http_header, http_ifd)
2189+
if http_ifd.orientation != 1:
2190+
raise ValueError(
2191+
f"Orientation tag (274) is {http_ifd.orientation}; "
2192+
f"dask-chunked reads (chunks=...) are not supported for "
2193+
f"non-default orientation on remote GeoTIFF sources. Read "
2194+
f"the full array first, then slice/chunk it."
2195+
)
21892196
# Wrap the parsed metadata in a single dask Delayed so every
21902197
# window task takes it as a graph input, not a Python closure.
21912198
# Without this, the (TIFFHeader, IFD) pair -- which can carry
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Remote dask reads must not bypass TIFF Orientation handling (#1794)."""
2+
from __future__ import annotations
3+
4+
import http.server
5+
import socketserver
6+
import threading
7+
8+
import numpy as np
9+
import pytest
10+
11+
from xrspatial.geotiff import open_geotiff
12+
13+
tifffile = pytest.importorskip("tifffile")
14+
15+
16+
def _write_with_orientation(path, arr, orientation):
17+
tifffile.imwrite(
18+
str(path),
19+
arr,
20+
extratags=[(274, 'H', 1, orientation, True)],
21+
)
22+
23+
24+
class _RangeHandler(http.server.BaseHTTPRequestHandler):
25+
payload: bytes = b''
26+
27+
def do_GET(self): # noqa: N802
28+
rng = self.headers.get('Range')
29+
if rng and rng.startswith('bytes='):
30+
spec = rng[len('bytes='):]
31+
start_s, _, end_s = spec.partition('-')
32+
start = int(start_s)
33+
end = int(end_s) if end_s else len(self.payload) - 1
34+
chunk = self.payload[start:end + 1]
35+
self.send_response(206)
36+
self.send_header('Content-Type', 'application/octet-stream')
37+
self.send_header(
38+
'Content-Range',
39+
f'bytes {start}-{start + len(chunk) - 1}/{len(self.payload)}',
40+
)
41+
self.send_header('Content-Length', str(len(chunk)))
42+
self.end_headers()
43+
self.wfile.write(chunk)
44+
return
45+
self.send_response(200)
46+
self.send_header('Content-Type', 'application/octet-stream')
47+
self.send_header('Content-Length', str(len(self.payload)))
48+
self.end_headers()
49+
self.wfile.write(self.payload)
50+
51+
def log_message(self, *_args, **_kwargs):
52+
pass
53+
54+
55+
def _serve(payload: bytes):
56+
handler_cls = type(
57+
'RangeHandler1794', (_RangeHandler,), {'payload': payload}
58+
)
59+
httpd = socketserver.TCPServer(('127.0.0.1', 0), handler_cls)
60+
port = httpd.server_address[1]
61+
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
62+
thread.start()
63+
return httpd, port
64+
65+
66+
def test_http_dask_read_rejects_non_default_orientation(tmp_path, monkeypatch):
67+
monkeypatch.setenv('XRSPATIAL_GEOTIFF_ALLOW_PRIVATE_HOSTS', '1')
68+
arr = np.arange(64, dtype=np.uint8).reshape(8, 8)
69+
path = tmp_path / "tmp_1794_orientation_2.tif"
70+
_write_with_orientation(path, arr, 2)
71+
72+
payload = path.read_bytes()
73+
httpd, port = _serve(payload)
74+
try:
75+
url = f'http://127.0.0.1:{port}/tmp_1794_orientation_2.tif'
76+
with pytest.raises(ValueError, match="Orientation tag"):
77+
open_geotiff(url, chunks=4)
78+
finally:
79+
httpd.shutdown()
80+
httpd.server_close()
81+

0 commit comments

Comments
 (0)