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