1919
2020import gdaltest
2121import pytest
22+ import webserver
2223from test_py_scripts import samples_path
2324
2425from osgeo import gdal , ogr , osr
@@ -1350,6 +1351,45 @@ def test_jp2grok_transcode_ignored_options(tmp_path):
13501351 ds = None
13511352
13521353
1354+ ###############################################################################
1355+ # Test reading a remote JP2 via /vsicurl/ (handled natively by Grok's libcurl
1356+ # backend when available). Uses a real URL, so it is only run when slow
1357+ # tests are enabled.
1358+
1359+
1360+ def test_jp2grok_vsicurl_remote ():
1361+
1362+ if not gdaltest .run_slow_tests ():
1363+ pytest .skip ("GDAL_RUN_SLOW_TESTS not set" )
1364+ if "CURL_ENABLED=YES" not in gdal .VersionInfo ("BUILD_INFO" ):
1365+ pytest .skip ("curl not enabled in this GDAL build" )
1366+
1367+ url = (
1368+ "/vsicurl/https://www.opengeodata.nrw.de/produkte/geobasis/lusat/"
1369+ "akt/dop/dop_jp2_f10/dop10rgbi_32_280_5653_1_nw_2025.jp2"
1370+ )
1371+
1372+ gdal .VSICurlClearCache ()
1373+ try :
1374+ ds = gdal .Open (url )
1375+ if ds is None :
1376+ pytest .skip ("remote host unreachable: " + gdal .GetLastErrorMsg ())
1377+ assert ds .RasterXSize > 0
1378+ assert ds .RasterYSize > 0
1379+ assert ds .RasterCount >= 1
1380+ # Read a small window from an overview (if any) or from the full-res
1381+ # upper-left corner to exercise the fetch path without pulling
1382+ # too much data.
1383+ band = ds .GetRasterBand (1 )
1384+ w = min (64 , ds .RasterXSize )
1385+ h = min (64 , ds .RasterYSize )
1386+ data = band .ReadRaster (0 , 0 , w , h , w , h )
1387+ assert data is not None and len (data ) > 0
1388+ ds = None
1389+ finally :
1390+ gdal .VSICurlClearCache ()
1391+
1392+
13531393###############################################################################
13541394# Test driver metadata
13551395
@@ -1363,3 +1403,169 @@ def test_jp2grok_driver_metadata():
13631403 assert drv .GetMetadataItem (gdal .DCAP_CREATECOPY ) == "YES"
13641404 assert "jp2" in drv .GetMetadataItem (gdal .DMD_EXTENSIONS )
13651405 assert "j2k" in drv .GetMetadataItem (gdal .DMD_EXTENSIONS )
1406+
1407+
1408+ ###############################################################################
1409+ # Webserver fixture for HTTP tests
1410+
1411+
1412+ @pytest .fixture (scope = "module" )
1413+ def server ():
1414+
1415+ process , port = webserver .launch (handler = webserver .DispatcherHttpHandler )
1416+ if port == 0 :
1417+ pytest .skip ("cannot start HTTP server" )
1418+
1419+ import collections
1420+
1421+ WebServer = collections .namedtuple ("WebServer" , "process port" )
1422+
1423+ yield WebServer (process , port )
1424+
1425+ gdal .VSICurlClearCache ()
1426+ webserver .server_stop (process , port )
1427+
1428+
1429+ ###############################################################################
1430+ # Test: blocklisted HTTP settings force VSILFILE fallback.
1431+ #
1432+ # When an unsupported GDAL HTTP config option is set, GrokCanRead() should
1433+ # return false for /vsicurl/ paths, causing the driver to use GDAL's VSILFILE
1434+ # callbacks instead of Grok's native libcurl I/O. The dataset should still
1435+ # open successfully — just via the fallback path.
1436+
1437+
1438+ # Each entry is (config_option, value) that should trigger VSILFILE fallback.
1439+ _BLOCKLIST_CASES = [
1440+ ("GDAL_HTTP_AUTH" , "NTLM" ),
1441+ ("GDAL_HTTP_AUTH" , "NEGOTIATE" ),
1442+ ("GDAL_HTTP_SSLCERT" , "/path/to/cert.pem" ),
1443+ ("GDAL_HTTP_SSLKEY" , "/path/to/key.pem" ),
1444+ ("GDAL_HTTP_SSLCERTTYPE" , "PEM" ),
1445+ ("GDAL_HTTP_KEYPASSWD" , "secret" ),
1446+ ("GDAL_HTTP_SSL_VERIFYSTATUS" , "YES" ),
1447+ ("GDAL_CURL_CA_BUNDLE" , "/path/to/ca-bundle.crt" ),
1448+ ("GDAL_HTTP_CAPATH" , "/etc/ssl/certs" ),
1449+ ("GDAL_HTTP_HEADER_FILE" , "/tmp/headers.txt" ),
1450+ ("GDAL_HTTPS_PROXY" , "http://proxy:8443" ),
1451+ ("GDAL_PROXY_AUTH" , "NTLM" ),
1452+ ("GDAL_HTTP_LOW_SPEED_TIME" , "30" ),
1453+ ("GDAL_HTTP_LOW_SPEED_LIMIT" , "1024" ),
1454+ ("GDAL_GSSAPI_DELEGATION" , "POLICY" ),
1455+ ]
1456+
1457+
1458+ @pytest .mark .require_curl ()
1459+ @pytest .mark .parametrize (
1460+ "option,value" , _BLOCKLIST_CASES , ids = [c [0 ] for c in _BLOCKLIST_CASES ]
1461+ )
1462+ def test_jp2grok_blocklist_fallback (server , option , value , tmp_path ):
1463+ """Blocklisted HTTP settings should trigger VSILFILE fallback while still
1464+ allowing the dataset to open successfully via GDAL's VSI layer."""
1465+
1466+ # GDAL_HTTP_HEADER_FILE requires the file to actually exist, otherwise
1467+ # GDAL logs an error when it tries to read headers from it.
1468+ if option == "GDAL_HTTP_HEADER_FILE" :
1469+ header_file = tmp_path / "headers.txt"
1470+ header_file .write_text ("X-Test: FallbackValue\n " )
1471+ value = str (header_file )
1472+
1473+ jp2_data = open ("data/jpeg2000/byte.jp2" , "rb" ).read ()
1474+ gdal .VSICurlClearCache ()
1475+
1476+ handler = webserver .FileHandler ({"/byte.jp2" : jp2_data })
1477+ url = "/vsicurl/http://localhost:%d/byte.jp2" % server .port
1478+
1479+ with gdal .config_option (option , value ):
1480+ with webserver .install_http_handler (handler ):
1481+ ds = gdal .Open (url )
1482+ # The dataset should open successfully via the VSILFILE callback
1483+ # path — GDAL's own curl handles the request.
1484+ assert ds is not None
1485+ assert ds .RasterXSize == 100
1486+ assert ds .RasterYSize == 100
1487+ ds = None
1488+
1489+ gdal .VSICurlClearCache ()
1490+
1491+
1492+ ###############################################################################
1493+ # Test: BASIC and BEARER auth should NOT trigger fallback (Grok handles these).
1494+
1495+
1496+ @pytest .mark .require_curl ()
1497+ @pytest .mark .parametrize ("auth_scheme" , ["BASIC" , "BEARER" ])
1498+ def test_jp2grok_supported_auth_no_fallback (server , auth_scheme ):
1499+ """BASIC and BEARER auth are handled by Grok natively and should not
1500+ trigger the VSILFILE fallback path."""
1501+
1502+ jp2_data = open ("data/jpeg2000/byte.jp2" , "rb" ).read ()
1503+ gdal .VSICurlClearCache ()
1504+
1505+ handler = webserver .FileHandler ({"/byte.jp2" : jp2_data })
1506+ url = "/vsicurl/http://localhost:%d/byte.jp2" % server .port
1507+
1508+ with gdal .config_option ("GDAL_HTTP_AUTH" , auth_scheme ):
1509+ with webserver .install_http_handler (handler ):
1510+ ds = gdal .Open (url )
1511+ assert ds is not None
1512+ assert ds .RasterXSize == 100
1513+ assert ds .RasterYSize == 100
1514+ ds = None
1515+
1516+ gdal .VSICurlClearCache ()
1517+
1518+
1519+ ###############################################################################
1520+ # Test: GDAL_HTTP_HEADERS with a single custom header should be forwarded
1521+ # to Grok's native I/O (no fallback).
1522+
1523+
1524+ @pytest .mark .require_curl ()
1525+ def test_jp2grok_single_custom_header (server ):
1526+ """A single GDAL_HTTP_HEADERS entry should be forwarded to Grok's
1527+ custom_headers[] without triggering VSILFILE fallback."""
1528+
1529+ jp2_data = open ("data/jpeg2000/byte.jp2" , "rb" ).read ()
1530+ gdal .VSICurlClearCache ()
1531+
1532+ handler = webserver .FileHandler ({"/byte.jp2" : jp2_data })
1533+ url = "/vsicurl/http://localhost:%d/byte.jp2" % server .port
1534+
1535+ with gdal .config_option ("GDAL_HTTP_HEADERS" , "X-Custom: TestValue" ):
1536+ with webserver .install_http_handler (handler ):
1537+ ds = gdal .Open (url )
1538+ assert ds is not None
1539+ assert ds .RasterXSize == 100
1540+ assert ds .RasterYSize == 100
1541+ ds = None
1542+
1543+ gdal .VSICurlClearCache ()
1544+
1545+
1546+ ###############################################################################
1547+ # Test: GDAL_HTTP_HEADERS with multiple headers should still work
1548+ # (forwarded to Grok's custom_headers[] array, up to GRK_MAX_CUSTOM_HEADERS).
1549+
1550+
1551+ @pytest .mark .require_curl ()
1552+ def test_jp2grok_multiple_custom_headers (server ):
1553+ """Multiple GDAL_HTTP_HEADERS entries should be forwarded to Grok's
1554+ custom_headers[] array."""
1555+
1556+ jp2_data = open ("data/jpeg2000/byte.jp2" , "rb" ).read ()
1557+ gdal .VSICurlClearCache ()
1558+
1559+ handler = webserver .FileHandler ({"/byte.jp2" : jp2_data })
1560+ url = "/vsicurl/http://localhost:%d/byte.jp2" % server .port
1561+
1562+ headers = "X-Custom1: Value1, X-Custom2: Value2, X-Custom3: Value3"
1563+ with gdal .config_option ("GDAL_HTTP_HEADERS" , headers ):
1564+ with webserver .install_http_handler (handler ):
1565+ ds = gdal .Open (url )
1566+ assert ds is not None
1567+ assert ds .RasterXSize == 100
1568+ assert ds .RasterYSize == 100
1569+ ds = None
1570+
1571+ gdal .VSICurlClearCache ()
0 commit comments