|
| 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