1919
2020import gdaltest
2121import pytest
22+ import webserver
2223from test_py_scripts import samples_path
2324
2425from osgeo import gdal , ogr , osr
@@ -1549,6 +1550,45 @@ def test_jp2grok_multitile_overview_decode():
15491550 gdal .Unlink (fname )
15501551
15511552
1553+ ###############################################################################
1554+ # Test reading a remote JP2 via /vsicurl/ (handled natively by Grok's libcurl
1555+ # backend when available). Uses a real URL, so it is only run when slow
1556+ # tests are enabled.
1557+
1558+
1559+ def test_jp2grok_vsicurl_remote ():
1560+
1561+ if not gdaltest .run_slow_tests ():
1562+ pytest .skip ("GDAL_RUN_SLOW_TESTS not set" )
1563+ if "CURL_ENABLED=YES" not in gdal .VersionInfo ("BUILD_INFO" ):
1564+ pytest .skip ("curl not enabled in this GDAL build" )
1565+
1566+ url = (
1567+ "/vsicurl/https://www.opengeodata.nrw.de/produkte/geobasis/lusat/"
1568+ "akt/dop/dop_jp2_f10/dop10rgbi_32_280_5653_1_nw_2025.jp2"
1569+ )
1570+
1571+ gdal .VSICurlClearCache ()
1572+ try :
1573+ ds = gdal .Open (url )
1574+ if ds is None :
1575+ pytest .skip ("remote host unreachable: " + gdal .GetLastErrorMsg ())
1576+ assert ds .RasterXSize > 0
1577+ assert ds .RasterYSize > 0
1578+ assert ds .RasterCount >= 1
1579+ # Read a small window from an overview (if any) or from the full-res
1580+ # upper-left corner to exercise the fetch path without pulling
1581+ # too much data.
1582+ band = ds .GetRasterBand (1 )
1583+ w = min (64 , ds .RasterXSize )
1584+ h = min (64 , ds .RasterYSize )
1585+ data = band .ReadRaster (0 , 0 , w , h , w , h )
1586+ assert data is not None and len (data ) > 0
1587+ ds = None
1588+ finally :
1589+ gdal .VSICurlClearCache ()
1590+
1591+
15521592###############################################################################
15531593# Test driver metadata
15541594
@@ -1562,3 +1602,169 @@ def test_jp2grok_driver_metadata():
15621602 assert drv .GetMetadataItem (gdal .DCAP_CREATECOPY ) == "YES"
15631603 assert "jp2" in drv .GetMetadataItem (gdal .DMD_EXTENSIONS )
15641604 assert "j2k" in drv .GetMetadataItem (gdal .DMD_EXTENSIONS )
1605+
1606+
1607+ ###############################################################################
1608+ # Webserver fixture for HTTP tests
1609+
1610+
1611+ @pytest .fixture (scope = "module" )
1612+ def server ():
1613+
1614+ process , port = webserver .launch (handler = webserver .DispatcherHttpHandler )
1615+ if port == 0 :
1616+ pytest .skip ("cannot start HTTP server" )
1617+
1618+ import collections
1619+
1620+ WebServer = collections .namedtuple ("WebServer" , "process port" )
1621+
1622+ yield WebServer (process , port )
1623+
1624+ gdal .VSICurlClearCache ()
1625+ webserver .server_stop (process , port )
1626+
1627+
1628+ ###############################################################################
1629+ # Test: blocklisted HTTP settings force VSILFILE fallback.
1630+ #
1631+ # When an unsupported GDAL HTTP config option is set, GrokCanRead() should
1632+ # return false for /vsicurl/ paths, causing the driver to use GDAL's VSILFILE
1633+ # callbacks instead of Grok's native libcurl I/O. The dataset should still
1634+ # open successfully — just via the fallback path.
1635+
1636+
1637+ # Each entry is (config_option, value) that should trigger VSILFILE fallback.
1638+ _BLOCKLIST_CASES = [
1639+ ("GDAL_HTTP_AUTH" , "NTLM" ),
1640+ ("GDAL_HTTP_AUTH" , "NEGOTIATE" ),
1641+ ("GDAL_HTTP_SSLCERT" , "/path/to/cert.pem" ),
1642+ ("GDAL_HTTP_SSLKEY" , "/path/to/key.pem" ),
1643+ ("GDAL_HTTP_SSLCERTTYPE" , "PEM" ),
1644+ ("GDAL_HTTP_KEYPASSWD" , "secret" ),
1645+ ("GDAL_HTTP_SSL_VERIFYSTATUS" , "YES" ),
1646+ ("GDAL_CURL_CA_BUNDLE" , "/path/to/ca-bundle.crt" ),
1647+ ("GDAL_HTTP_CAPATH" , "/etc/ssl/certs" ),
1648+ ("GDAL_HTTP_HEADER_FILE" , "/tmp/headers.txt" ),
1649+ ("GDAL_HTTPS_PROXY" , "http://proxy:8443" ),
1650+ ("GDAL_PROXY_AUTH" , "NTLM" ),
1651+ ("GDAL_HTTP_LOW_SPEED_TIME" , "30" ),
1652+ ("GDAL_HTTP_LOW_SPEED_LIMIT" , "1024" ),
1653+ ("GDAL_GSSAPI_DELEGATION" , "POLICY" ),
1654+ ]
1655+
1656+
1657+ @pytest .mark .require_curl ()
1658+ @pytest .mark .parametrize (
1659+ "option,value" , _BLOCKLIST_CASES , ids = [c [0 ] for c in _BLOCKLIST_CASES ]
1660+ )
1661+ def test_jp2grok_blocklist_fallback (server , option , value , tmp_path ):
1662+ """Blocklisted HTTP settings should trigger VSILFILE fallback while still
1663+ allowing the dataset to open successfully via GDAL's VSI layer."""
1664+
1665+ # GDAL_HTTP_HEADER_FILE requires the file to actually exist, otherwise
1666+ # GDAL logs an error when it tries to read headers from it.
1667+ if option == "GDAL_HTTP_HEADER_FILE" :
1668+ header_file = tmp_path / "headers.txt"
1669+ header_file .write_text ("X-Test: FallbackValue\n " )
1670+ value = str (header_file )
1671+
1672+ jp2_data = open ("data/jpeg2000/byte.jp2" , "rb" ).read ()
1673+ gdal .VSICurlClearCache ()
1674+
1675+ handler = webserver .FileHandler ({"/byte.jp2" : jp2_data })
1676+ url = "/vsicurl/http://localhost:%d/byte.jp2" % server .port
1677+
1678+ with gdal .config_option (option , value ):
1679+ with webserver .install_http_handler (handler ):
1680+ ds = gdal .Open (url )
1681+ # The dataset should open successfully via the VSILFILE callback
1682+ # path — GDAL's own curl handles the request.
1683+ assert ds is not None
1684+ assert ds .RasterXSize == 100
1685+ assert ds .RasterYSize == 100
1686+ ds = None
1687+
1688+ gdal .VSICurlClearCache ()
1689+
1690+
1691+ ###############################################################################
1692+ # Test: BASIC and BEARER auth should NOT trigger fallback (Grok handles these).
1693+
1694+
1695+ @pytest .mark .require_curl ()
1696+ @pytest .mark .parametrize ("auth_scheme" , ["BASIC" , "BEARER" ])
1697+ def test_jp2grok_supported_auth_no_fallback (server , auth_scheme ):
1698+ """BASIC and BEARER auth are handled by Grok natively and should not
1699+ trigger the VSILFILE fallback path."""
1700+
1701+ jp2_data = open ("data/jpeg2000/byte.jp2" , "rb" ).read ()
1702+ gdal .VSICurlClearCache ()
1703+
1704+ handler = webserver .FileHandler ({"/byte.jp2" : jp2_data })
1705+ url = "/vsicurl/http://localhost:%d/byte.jp2" % server .port
1706+
1707+ with gdal .config_option ("GDAL_HTTP_AUTH" , auth_scheme ):
1708+ with webserver .install_http_handler (handler ):
1709+ ds = gdal .Open (url )
1710+ assert ds is not None
1711+ assert ds .RasterXSize == 100
1712+ assert ds .RasterYSize == 100
1713+ ds = None
1714+
1715+ gdal .VSICurlClearCache ()
1716+
1717+
1718+ ###############################################################################
1719+ # Test: GDAL_HTTP_HEADERS with a single custom header should be forwarded
1720+ # to Grok's native I/O (no fallback).
1721+
1722+
1723+ @pytest .mark .require_curl ()
1724+ def test_jp2grok_single_custom_header (server ):
1725+ """A single GDAL_HTTP_HEADERS entry should be forwarded to Grok's
1726+ custom_headers[] without triggering VSILFILE fallback."""
1727+
1728+ jp2_data = open ("data/jpeg2000/byte.jp2" , "rb" ).read ()
1729+ gdal .VSICurlClearCache ()
1730+
1731+ handler = webserver .FileHandler ({"/byte.jp2" : jp2_data })
1732+ url = "/vsicurl/http://localhost:%d/byte.jp2" % server .port
1733+
1734+ with gdal .config_option ("GDAL_HTTP_HEADERS" , "X-Custom: TestValue" ):
1735+ with webserver .install_http_handler (handler ):
1736+ ds = gdal .Open (url )
1737+ assert ds is not None
1738+ assert ds .RasterXSize == 100
1739+ assert ds .RasterYSize == 100
1740+ ds = None
1741+
1742+ gdal .VSICurlClearCache ()
1743+
1744+
1745+ ###############################################################################
1746+ # Test: GDAL_HTTP_HEADERS with multiple headers should still work
1747+ # (forwarded to Grok's custom_headers[] array, up to GRK_MAX_CUSTOM_HEADERS).
1748+
1749+
1750+ @pytest .mark .require_curl ()
1751+ def test_jp2grok_multiple_custom_headers (server ):
1752+ """Multiple GDAL_HTTP_HEADERS entries should be forwarded to Grok's
1753+ custom_headers[] array."""
1754+
1755+ jp2_data = open ("data/jpeg2000/byte.jp2" , "rb" ).read ()
1756+ gdal .VSICurlClearCache ()
1757+
1758+ handler = webserver .FileHandler ({"/byte.jp2" : jp2_data })
1759+ url = "/vsicurl/http://localhost:%d/byte.jp2" % server .port
1760+
1761+ headers = "X-Custom1: Value1, X-Custom2: Value2, X-Custom3: Value3"
1762+ with gdal .config_option ("GDAL_HTTP_HEADERS" , headers ):
1763+ with webserver .install_http_handler (handler ):
1764+ ds = gdal .Open (url )
1765+ assert ds is not None
1766+ assert ds .RasterXSize == 100
1767+ assert ds .RasterYSize == 100
1768+ ds = None
1769+
1770+ gdal .VSICurlClearCache ()
0 commit comments