1919
2020import gdaltest
2121import pytest
22+ import webserver
2223from test_py_scripts import samples_path
2324
2425from osgeo import gdal , ogr , osr
@@ -1416,6 +1417,45 @@ def test_jp2grok_overview_decode():
14161417 gdal .Unlink (fname )
14171418
14181419
1420+ ###############################################################################
1421+ # Test reading a remote JP2 via /vsicurl/ (handled natively by Grok's libcurl
1422+ # backend when available). Uses a real URL, so it is only run when slow
1423+ # tests are enabled.
1424+
1425+
1426+ def test_jp2grok_vsicurl_remote ():
1427+
1428+ if not gdaltest .run_slow_tests ():
1429+ pytest .skip ("GDAL_RUN_SLOW_TESTS not set" )
1430+ if "CURL_ENABLED=YES" not in gdal .VersionInfo ("BUILD_INFO" ):
1431+ pytest .skip ("curl not enabled in this GDAL build" )
1432+
1433+ url = (
1434+ "/vsicurl/https://www.opengeodata.nrw.de/produkte/geobasis/lusat/"
1435+ "akt/dop/dop_jp2_f10/dop10rgbi_32_280_5653_1_nw_2025.jp2"
1436+ )
1437+
1438+ gdal .VSICurlClearCache ()
1439+ try :
1440+ ds = gdal .Open (url )
1441+ if ds is None :
1442+ pytest .skip ("remote host unreachable: " + gdal .GetLastErrorMsg ())
1443+ assert ds .RasterXSize > 0
1444+ assert ds .RasterYSize > 0
1445+ assert ds .RasterCount >= 1
1446+ # Read a small window from an overview (if any) or from the full-res
1447+ # upper-left corner to exercise the fetch path without pulling
1448+ # too much data.
1449+ band = ds .GetRasterBand (1 )
1450+ w = min (64 , ds .RasterXSize )
1451+ h = min (64 , ds .RasterYSize )
1452+ data = band .ReadRaster (0 , 0 , w , h , w , h )
1453+ assert data is not None and len (data ) > 0
1454+ ds = None
1455+ finally :
1456+ gdal .VSICurlClearCache ()
1457+
1458+
14191459###############################################################################
14201460# Test driver metadata
14211461
@@ -1429,3 +1469,169 @@ def test_jp2grok_driver_metadata():
14291469 assert drv .GetMetadataItem (gdal .DCAP_CREATECOPY ) == "YES"
14301470 assert "jp2" in drv .GetMetadataItem (gdal .DMD_EXTENSIONS )
14311471 assert "j2k" in drv .GetMetadataItem (gdal .DMD_EXTENSIONS )
1472+
1473+
1474+ ###############################################################################
1475+ # Webserver fixture for HTTP tests
1476+
1477+
1478+ @pytest .fixture (scope = "module" )
1479+ def server ():
1480+
1481+ process , port = webserver .launch (handler = webserver .DispatcherHttpHandler )
1482+ if port == 0 :
1483+ pytest .skip ("cannot start HTTP server" )
1484+
1485+ import collections
1486+
1487+ WebServer = collections .namedtuple ("WebServer" , "process port" )
1488+
1489+ yield WebServer (process , port )
1490+
1491+ gdal .VSICurlClearCache ()
1492+ webserver .server_stop (process , port )
1493+
1494+
1495+ ###############################################################################
1496+ # Test: blocklisted HTTP settings force VSILFILE fallback.
1497+ #
1498+ # When an unsupported GDAL HTTP config option is set, GrokCanRead() should
1499+ # return false for /vsicurl/ paths, causing the driver to use GDAL's VSILFILE
1500+ # callbacks instead of Grok's native libcurl I/O. The dataset should still
1501+ # open successfully — just via the fallback path.
1502+
1503+
1504+ # Each entry is (config_option, value) that should trigger VSILFILE fallback.
1505+ _BLOCKLIST_CASES = [
1506+ ("GDAL_HTTP_AUTH" , "NTLM" ),
1507+ ("GDAL_HTTP_AUTH" , "NEGOTIATE" ),
1508+ ("GDAL_HTTP_SSLCERT" , "/path/to/cert.pem" ),
1509+ ("GDAL_HTTP_SSLKEY" , "/path/to/key.pem" ),
1510+ ("GDAL_HTTP_SSLCERTTYPE" , "PEM" ),
1511+ ("GDAL_HTTP_KEYPASSWD" , "secret" ),
1512+ ("GDAL_HTTP_SSL_VERIFYSTATUS" , "YES" ),
1513+ ("GDAL_CURL_CA_BUNDLE" , "/path/to/ca-bundle.crt" ),
1514+ ("GDAL_HTTP_CAPATH" , "/etc/ssl/certs" ),
1515+ ("GDAL_HTTP_HEADER_FILE" , "/tmp/headers.txt" ),
1516+ ("GDAL_HTTPS_PROXY" , "http://proxy:8443" ),
1517+ ("GDAL_PROXY_AUTH" , "NTLM" ),
1518+ ("GDAL_HTTP_LOW_SPEED_TIME" , "30" ),
1519+ ("GDAL_HTTP_LOW_SPEED_LIMIT" , "1024" ),
1520+ ("GDAL_GSSAPI_DELEGATION" , "POLICY" ),
1521+ ]
1522+
1523+
1524+ @pytest .mark .require_curl ()
1525+ @pytest .mark .parametrize (
1526+ "option,value" , _BLOCKLIST_CASES , ids = [c [0 ] for c in _BLOCKLIST_CASES ]
1527+ )
1528+ def test_jp2grok_blocklist_fallback (server , option , value , tmp_path ):
1529+ """Blocklisted HTTP settings should trigger VSILFILE fallback while still
1530+ allowing the dataset to open successfully via GDAL's VSI layer."""
1531+
1532+ # GDAL_HTTP_HEADER_FILE requires the file to actually exist, otherwise
1533+ # GDAL logs an error when it tries to read headers from it.
1534+ if option == "GDAL_HTTP_HEADER_FILE" :
1535+ header_file = tmp_path / "headers.txt"
1536+ header_file .write_text ("X-Test: FallbackValue\n " )
1537+ value = str (header_file )
1538+
1539+ jp2_data = open ("data/jpeg2000/byte.jp2" , "rb" ).read ()
1540+ gdal .VSICurlClearCache ()
1541+
1542+ handler = webserver .FileHandler ({"/byte.jp2" : jp2_data })
1543+ url = "/vsicurl/http://localhost:%d/byte.jp2" % server .port
1544+
1545+ with gdal .config_option (option , value ):
1546+ with webserver .install_http_handler (handler ):
1547+ ds = gdal .Open (url )
1548+ # The dataset should open successfully via the VSILFILE callback
1549+ # path — GDAL's own curl handles the request.
1550+ assert ds is not None
1551+ assert ds .RasterXSize == 100
1552+ assert ds .RasterYSize == 100
1553+ ds = None
1554+
1555+ gdal .VSICurlClearCache ()
1556+
1557+
1558+ ###############################################################################
1559+ # Test: BASIC and BEARER auth should NOT trigger fallback (Grok handles these).
1560+
1561+
1562+ @pytest .mark .require_curl ()
1563+ @pytest .mark .parametrize ("auth_scheme" , ["BASIC" , "BEARER" ])
1564+ def test_jp2grok_supported_auth_no_fallback (server , auth_scheme ):
1565+ """BASIC and BEARER auth are handled by Grok natively and should not
1566+ trigger the VSILFILE fallback path."""
1567+
1568+ jp2_data = open ("data/jpeg2000/byte.jp2" , "rb" ).read ()
1569+ gdal .VSICurlClearCache ()
1570+
1571+ handler = webserver .FileHandler ({"/byte.jp2" : jp2_data })
1572+ url = "/vsicurl/http://localhost:%d/byte.jp2" % server .port
1573+
1574+ with gdal .config_option ("GDAL_HTTP_AUTH" , auth_scheme ):
1575+ with webserver .install_http_handler (handler ):
1576+ ds = gdal .Open (url )
1577+ assert ds is not None
1578+ assert ds .RasterXSize == 100
1579+ assert ds .RasterYSize == 100
1580+ ds = None
1581+
1582+ gdal .VSICurlClearCache ()
1583+
1584+
1585+ ###############################################################################
1586+ # Test: GDAL_HTTP_HEADERS with a single custom header should be forwarded
1587+ # to Grok's native I/O (no fallback).
1588+
1589+
1590+ @pytest .mark .require_curl ()
1591+ def test_jp2grok_single_custom_header (server ):
1592+ """A single GDAL_HTTP_HEADERS entry should be forwarded to Grok's
1593+ custom_headers[] without triggering VSILFILE fallback."""
1594+
1595+ jp2_data = open ("data/jpeg2000/byte.jp2" , "rb" ).read ()
1596+ gdal .VSICurlClearCache ()
1597+
1598+ handler = webserver .FileHandler ({"/byte.jp2" : jp2_data })
1599+ url = "/vsicurl/http://localhost:%d/byte.jp2" % server .port
1600+
1601+ with gdal .config_option ("GDAL_HTTP_HEADERS" , "X-Custom: TestValue" ):
1602+ with webserver .install_http_handler (handler ):
1603+ ds = gdal .Open (url )
1604+ assert ds is not None
1605+ assert ds .RasterXSize == 100
1606+ assert ds .RasterYSize == 100
1607+ ds = None
1608+
1609+ gdal .VSICurlClearCache ()
1610+
1611+
1612+ ###############################################################################
1613+ # Test: GDAL_HTTP_HEADERS with multiple headers should still work
1614+ # (forwarded to Grok's custom_headers[] array, up to GRK_MAX_CUSTOM_HEADERS).
1615+
1616+
1617+ @pytest .mark .require_curl ()
1618+ def test_jp2grok_multiple_custom_headers (server ):
1619+ """Multiple GDAL_HTTP_HEADERS entries should be forwarded to Grok's
1620+ custom_headers[] array."""
1621+
1622+ jp2_data = open ("data/jpeg2000/byte.jp2" , "rb" ).read ()
1623+ gdal .VSICurlClearCache ()
1624+
1625+ handler = webserver .FileHandler ({"/byte.jp2" : jp2_data })
1626+ url = "/vsicurl/http://localhost:%d/byte.jp2" % server .port
1627+
1628+ headers = "X-Custom1: Value1, X-Custom2: Value2, X-Custom3: Value3"
1629+ with gdal .config_option ("GDAL_HTTP_HEADERS" , headers ):
1630+ with webserver .install_http_handler (handler ):
1631+ ds = gdal .Open (url )
1632+ assert ds is not None
1633+ assert ds .RasterXSize == 100
1634+ assert ds .RasterYSize == 100
1635+ ds = None
1636+
1637+ gdal .VSICurlClearCache ()
0 commit comments