diff options
| author | Even Rouault <even.rouault@spatialys.com> | 2020-01-22 14:09:14 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-01-22 14:09:14 +0100 |
| commit | c5fb54168665d41503ef3a08f0534da58949b632 (patch) | |
| tree | 19e2adc8809290881a6a13ec6001013d03170c7d | |
| parent | a6390b59ae2bad2a763e7ab3341ee4c80e708b3d (diff) | |
| parent | 66fd99a8831955034cb25c8468ecfe1f9d3a7d62 (diff) | |
| download | PROJ-c5fb54168665d41503ef3a08f0534da58949b632.tar.gz PROJ-c5fb54168665d41503ef3a08f0534da58949b632.zip | |
Merge pull request #1839 from rouault/rfc4_utf8
[RFC4_dev] Use Win32 Unicode APIs and expect all strings to be UTF-8 (fixes #1765)
49 files changed, 4682 insertions, 3030 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d5524db..41c0c0c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,8 +84,6 @@ elseif("${CMAKE_C_COMPILER_ID}" STREQUAL "Intel") endif() endif() -add_definitions(-DNOMINMAX) - set(PROJ_C_WARN_FLAGS "${PROJ_C_WARN_FLAGS}" CACHE STRING "C flags used to compile PROJ targets") set(PROJ_CXX_WARN_FLAGS "${PROJ_CXX_WARN_FLAGS}" @@ -772,7 +772,7 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = src/iso19111 include/proj src/proj.h src/proj_experimental.h src/general_doc.dox src/filemanager.cpp +INPUT = src/iso19111 include/proj src/proj.h src/proj_experimental.h src/general_doc.dox src/filemanager.cpp src/networkfilemanager.cpp # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses diff --git a/cmake/ProjTest.cmake b/cmake/ProjTest.cmake index a016cd9a..ad612970 100644 --- a/cmake/ProjTest.cmake +++ b/cmake/ProjTest.cmake @@ -2,6 +2,16 @@ # add test with sh script # +function(proj_test_set_properties TESTNAME) + if(MSVC) + set_tests_properties( ${TESTNAME} + PROPERTIES ENVIRONMENT "PROJ_IGNORE_USER_WRITABLE_DIRECTORY=YES;PROJ_LIB=${PROJECT_BINARY_DIR}/data\\;${PROJECT_SOURCE_DIR}/data") + else() + set_tests_properties( ${TESTNAME} + PROPERTIES ENVIRONMENT "PROJ_IGNORE_USER_WRITABLE_DIRECTORY=YES;PROJ_LIB=${PROJECT_BINARY_DIR}/data:${PROJECT_SOURCE_DIR}/data") + endif() +endfunction() + function(proj_add_test_script_sh SH_NAME BIN_USE) if(UNIX) get_filename_component(testname ${SH_NAME} NAME_WE) @@ -26,13 +36,7 @@ function(proj_add_test_script_sh SH_NAME BIN_USE) COMMAND ${PROJECT_SOURCE_DIR}/test/cli/${SH_NAME} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${${BIN_USE}} ) - if(MSVC) - set_tests_properties( ${testname} - PROPERTIES ENVIRONMENT "PROJ_IGNORE_USER_WRITABLE_DIRECTORY=YES;PROJ_LIB=${PROJECT_BINARY_DIR}/data\\;${PROJECT_SOURCE_DIR}/data") - else() - set_tests_properties( ${testname} - PROPERTIES ENVIRONMENT "PROJ_IGNORE_USER_WRITABLE_DIRECTORY=YES;PROJ_LIB=${PROJECT_BINARY_DIR}/data:${PROJECT_SOURCE_DIR}/data") - endif() + proj_test_set_properties(${testname}) endif() endif() @@ -49,13 +53,6 @@ function(proj_add_gie_test TESTNAME TESTCASE) ${TESTFILE} ) - if(MSVC) - set_tests_properties( ${TESTNAME} - PROPERTIES ENVIRONMENT "PROJ_IGNORE_USER_WRITABLE_DIRECTORY=YES;PROJ_LIB=${PROJECT_BINARY_DIR}/data\\;${PROJECT_SOURCE_DIR}/data") - else() - set_tests_properties( ${TESTNAME} - PROPERTIES ENVIRONMENT "PROJ_IGNORE_USER_WRITABLE_DIRECTORY=YES;PROJ_LIB=${PROJECT_BINARY_DIR}/data:${PROJECT_SOURCE_DIR}/data") - endif() - + proj_test_set_properties(${TESTNAME}) endfunction() diff --git a/configure.ac b/configure.ac index afde2712..2bdcafb8 100644 --- a/configure.ac +++ b/configure.ac @@ -168,8 +168,17 @@ AC_SUBST(C_WFLAGS,$C_WFLAGS) AC_SUBST(CXX_WFLAGS,$CXX_WFLAGS) AC_SUBST(NO_ZERO_AS_NULL_POINTER_CONSTANT_FLAG,$NO_ZERO_AS_NULL_POINTER_CONSTANT_FLAG) -CFLAGS="${CFLAGS} -fvisibility=hidden -DNOMINMAX" -CXXFLAGS="${CXXFLAGS} -fvisibility=hidden -DNOMINMAX" +CFLAGS="${CFLAGS} -fvisibility=hidden" +CXXFLAGS="${CXXFLAGS} -fvisibility=hidden" + +case "${host_os}" in + cygwin* | mingw32* | pw32* | beos* | darwin*) + CFLAGS="${CFLAGS} -DNOMINMAX" + CXXFLAGS="${CXXFLAGS} -DNOMINMAX" + ;; + *) + ;; +esac dnl Checks for libraries. save_CFLAGS="$CFLAGS" diff --git a/data/Makefile.am b/data/Makefile.am index 4bba2649..d8abfe32 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -85,6 +85,7 @@ EXTRA_DIST = proj.ini GL27 nad.lst nad27 nad83 \ tests/nkgrf03vel_realigned_extract.tif \ tests/nkgrf03vel_realigned_xy_extract.ct2 \ tests/nkgrf03vel_realigned_z_extract.gtx \ + tests/test_hgrid_with_two_level_of_subgrids_no_grid_name.tif \ null \ generate_all_sql_in.cmake sql_filelist.cmake \ $(SQL_ORDERED_LIST) diff --git a/data/proj.ini b/data/proj.ini index 2146ce41..0ae33249 100644 --- a/data/proj.ini +++ b/data/proj.ini @@ -11,6 +11,6 @@ cdn_endpoint = https://cdn.proj.org cache_enabled = on -cache_size_MB = 100 +cache_size_MB = 300 cache_ttl_sec = 86400 diff --git a/data/tests/test_hgrid_with_two_level_of_subgrids_no_grid_name.tif b/data/tests/test_hgrid_with_two_level_of_subgrids_no_grid_name.tif Binary files differnew file mode 100644 index 00000000..2abb3226 --- /dev/null +++ b/data/tests/test_hgrid_with_two_level_of_subgrids_no_grid_name.tif diff --git a/docs/source/apps/cct.rst b/docs/source/apps/cct.rst index 33c716fe..c290e1b2 100644 --- a/docs/source/apps/cct.rst +++ b/docs/source/apps/cct.rst @@ -91,6 +91,15 @@ cartesian coordinates) and *Coordinate Transformations*, which are coordinate operations where input and output datums differ (e.g. change of reference frame). +Use of remote grids +******************* + +.. versionadded:: 7.0.0 + +If the :envvar:`PROJ_NETWORK` environment variable is set to ``ON``, +:program:`cct` will attempt to use remote grids stored on CDN (Content +Delivery Network) storage, when they are not available locally. + Examples ******** diff --git a/docs/source/conf.py b/docs/source/conf.py index b9e1fa8a..c2142667 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -376,7 +376,7 @@ texinfo_documents = [ #texinfo_no_detailmenu = False breathe_projects = { - "cpp_stuff":"../build/xml/", + "doxygen_api":"../build/xml/", } import redirects diff --git a/docs/source/development/reference/cpp/common.rst b/docs/source/development/reference/cpp/common.rst index c1a28d37..4e92f6b2 100644 --- a/docs/source/development/reference/cpp/common.rst +++ b/docs/source/development/reference/cpp/common.rst @@ -4,5 +4,5 @@ common namespace ---------------- .. doxygennamespace:: osgeo::proj::common - :project: cpp_stuff + :project: doxygen_api :members: diff --git a/docs/source/development/reference/cpp/cpp_general.rst b/docs/source/development/reference/cpp/cpp_general.rst index 3b47d01e..d54daaa8 100644 --- a/docs/source/development/reference/cpp/cpp_general.rst +++ b/docs/source/development/reference/cpp/cpp_general.rst @@ -4,4 +4,4 @@ General documentation --------------------- .. doxygenfile:: general_doc.dox.reworked.h - :project: cpp_stuff + :project: doxygen_api diff --git a/docs/source/development/reference/cpp/crs.rst b/docs/source/development/reference/cpp/crs.rst index 2abea378..f8df118e 100644 --- a/docs/source/development/reference/cpp/crs.rst +++ b/docs/source/development/reference/cpp/crs.rst @@ -4,5 +4,5 @@ crs namespace ------------- .. doxygennamespace:: osgeo::proj::crs - :project: cpp_stuff + :project: doxygen_api :members: diff --git a/docs/source/development/reference/cpp/cs.rst b/docs/source/development/reference/cpp/cs.rst index b168213f..376ae6a6 100644 --- a/docs/source/development/reference/cpp/cs.rst +++ b/docs/source/development/reference/cpp/cs.rst @@ -4,5 +4,5 @@ cs namespace ------------ .. doxygennamespace:: osgeo::proj::cs - :project: cpp_stuff + :project: doxygen_api :members: diff --git a/docs/source/development/reference/cpp/datum.rst b/docs/source/development/reference/cpp/datum.rst index 1fee5f8a..0a545a68 100644 --- a/docs/source/development/reference/cpp/datum.rst +++ b/docs/source/development/reference/cpp/datum.rst @@ -4,5 +4,5 @@ datum namespace --------------- .. doxygennamespace:: osgeo::proj::datum - :project: cpp_stuff + :project: doxygen_api :members: diff --git a/docs/source/development/reference/cpp/io.rst b/docs/source/development/reference/cpp/io.rst index 9da0f680..9729af90 100644 --- a/docs/source/development/reference/cpp/io.rst +++ b/docs/source/development/reference/cpp/io.rst @@ -4,5 +4,5 @@ io namespace ------------ .. doxygennamespace:: osgeo::proj::io - :project: cpp_stuff + :project: doxygen_api :members: diff --git a/docs/source/development/reference/cpp/metadata.rst b/docs/source/development/reference/cpp/metadata.rst index b3960122..348627e8 100644 --- a/docs/source/development/reference/cpp/metadata.rst +++ b/docs/source/development/reference/cpp/metadata.rst @@ -4,5 +4,5 @@ metadata namespace ------------------ .. doxygennamespace:: osgeo::proj::metadata - :project: cpp_stuff + :project: doxygen_api :members: diff --git a/docs/source/development/reference/cpp/operation.rst b/docs/source/development/reference/cpp/operation.rst index bc3f201d..88096cb8 100644 --- a/docs/source/development/reference/cpp/operation.rst +++ b/docs/source/development/reference/cpp/operation.rst @@ -4,5 +4,5 @@ operation namespace ------------------- .. doxygennamespace:: osgeo::proj::operation - :project: cpp_stuff + :project: doxygen_api :members: diff --git a/docs/source/development/reference/cpp/util.rst b/docs/source/development/reference/cpp/util.rst index 1aba2954..bc9676a4 100644 --- a/docs/source/development/reference/cpp/util.rst +++ b/docs/source/development/reference/cpp/util.rst @@ -4,5 +4,5 @@ util namespace -------------- .. doxygennamespace:: osgeo::proj::util - :project: cpp_stuff + :project: doxygen_api :members: diff --git a/docs/source/development/reference/datatypes.rst b/docs/source/development/reference/datatypes.rst index 18bd7efd..7ec17eb1 100644 --- a/docs/source/development/reference/datatypes.rst +++ b/docs/source/development/reference/datatypes.rst @@ -797,11 +797,48 @@ Logging .. versionadded:: 5.1.0 +Setting custom I/O functions +------------------------------------------------------------------------------- + +.. versionadded:: 7.0.0 + +.. doxygenstruct:: PROJ_FILE_API + :project: doxygen_api + :members: + +.. doxygentypedef:: PROJ_FILE_HANDLE + :project: doxygen_api + +.. doxygenenum:: PROJ_OPEN_ACCESS + :project: doxygen_api + + +Network related functionality +------------------------------------------------------------------------------- + +.. versionadded:: 7.0.0 + +.. doxygentypedef:: PROJ_NETWORK_HANDLE + :project: doxygen_api + +.. doxygentypedef:: proj_network_open_cbk_type + :project: doxygen_api + +.. doxygentypedef:: proj_network_close_cbk_type + :project: doxygen_api + +.. doxygentypedef:: proj_network_get_header_value_cbk_type + :project: doxygen_api + +.. doxygentypedef:: proj_network_read_range_type + :project: doxygen_api + + C API for ISO-19111 functionality ------------------------------------------------------------------------------- .. doxygengroup:: iso19111_types - :project: cpp_stuff + :project: doxygen_api :content-only: diff --git a/docs/source/development/reference/functions.rst b/docs/source/development/reference/functions.rst index c010fd13..a2a0c3ae 100644 --- a/docs/source/development/reference/functions.rst +++ b/docs/source/development/reference/functions.rst @@ -697,6 +697,54 @@ Various :returns: :c:type:`int` 1 if output units is expected in radians, otherwise 0 +Setting custom I/O functions +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. versionadded:: 7.0.0 + +.. doxygenfunction:: proj_context_set_fileapi + :project: doxygen_api + +.. doxygenfunction:: proj_context_set_sqlite3_vfs_name + :project: doxygen_api + + +Network related functionality +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. versionadded:: 7.0.0 + +.. doxygenfunction:: proj_context_set_network_callbacks + :project: doxygen_api + +.. doxygenfunction:: proj_context_set_enable_network + :project: doxygen_api + +.. doxygenfunction:: proj_context_set_url_endpoint + :project: doxygen_api + +.. doxygenfunction:: proj_grid_cache_set_enable + :project: doxygen_api + +.. doxygenfunction:: proj_grid_cache_set_filename + :project: doxygen_api + +.. doxygenfunction:: proj_grid_cache_set_max_size + :project: doxygen_api + +.. doxygenfunction:: proj_grid_cache_set_ttl + :project: doxygen_api + +.. doxygenfunction:: proj_grid_cache_clear + :project: doxygen_api + +.. doxygenfunction:: proj_is_download_needed + :project: doxygen_api + +.. doxygenfunction:: proj_download_file + :project: doxygen_api + + Cleanup ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -727,6 +775,6 @@ which are not of type CRS (can be tested with :c:func:`proj_is_crs`), will return an error when used with functions of this section. .. doxygengroup:: iso19111_functions - :project: cpp_stuff + :project: doxygen_api :content-only: diff --git a/docs/source/operations/transformations/deformation.rst b/docs/source/operations/transformations/deformation.rst index aefda3a6..02924a25 100644 --- a/docs/source/operations/transformations/deformation.rst +++ b/docs/source/operations/transformations/deformation.rst @@ -99,6 +99,8 @@ Parameters Grids for the horizontal component of a deformation model is expected to be in CTable2 format. + .. note:: :option:`+xy_grids` is mutually exclusive with :option:`+grids` + .. option:: +z_grids=<list> Comma-separated list of grids to load. If a grid is prefixed by an `@` the @@ -108,6 +110,8 @@ Parameters Grids for the vertical component of a deformation model is expected to be in either GTX format. + .. note:: :option:`+z_grids` is mutually exclusive with :option:`+grids` + .. option:: +grids=<list> .. versionadded:: 7.0.0 diff --git a/docs/source/resource_files.rst b/docs/source/resource_files.rst index 3684a948..a07d6d6a 100644 --- a/docs/source/resource_files.rst +++ b/docs/source/resource_files.rst @@ -67,6 +67,8 @@ A proj installation includes a SQLite database of transformation information that must be accessible for the library to work properly. The library will print an error if the database can't be found. +.. _proj-ini: + proj.ini ------------------------------------------------------------------------------- @@ -92,7 +94,7 @@ Its default content is: cache_enabled = on - cache_size_MB = 100 + cache_size_MB = 300 cache_ttl_sec = 86400 diff --git a/docs/source/usage/differences.rst b/docs/source/usage/differences.rst index 4d05543a..8fcf51d6 100644 --- a/docs/source/usage/differences.rst +++ b/docs/source/usage/differences.rst @@ -114,3 +114,9 @@ exclusive with :option:`+t_epoch`. :option:`+dt` is used when deformation for a set amount of time is needed and :option:`+t_epoch` is used (in conjunction with the observation time of the input coordinate) when deformation from a specific epoch to the observation time is needed. + +Version 7.0.0 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +The value of all path, filenames passed to PROJ through function calls, PROJ +strings or environment variables should be encoded in UTF-8. diff --git a/scripts/doxygen.sh b/scripts/doxygen.sh index 9646eed8..380afcfa 100755 --- a/scripts/doxygen.sh +++ b/scripts/doxygen.sh @@ -34,7 +34,11 @@ fi mkdir -p docs/build/tmp_breathe python scripts/generate_breathe_friendly_general_doc.py rm -rf docs/build/xml/ -(cat Doxyfile; printf "GENERATE_HTML=NO\nGENERATE_XML=YES\nINPUT= src/iso19111 include/proj src/proj.h src/filemanager.cpp docs/build/tmp_breathe/general_doc.dox.reworked.h") | doxygen - > docs/build/tmp_breathe/docs_log.txt 2>&1 + +# Ugly hack to workaround a bug of Doxygen 1.8.17 that erroneously detect proj_network_get_header_value_cbk_type/ as a variable +sed "s/const char\* (\*proj_network_get_header_value_cbk_type/CONST_CHAR\* (\*proj_network_get_header_value_cbk_type/" < src/proj.h > docs/build/tmp_breathe/proj.h + +(cat Doxyfile; printf "GENERATE_HTML=NO\nGENERATE_XML=YES\nINPUT= src/iso19111 include/proj docs/build/tmp_breathe/proj.h src/filemanager.cpp src/networkfilemanager.cpp docs/build/tmp_breathe/general_doc.dox.reworked.h") | doxygen - > docs/build/tmp_breathe/docs_log.txt 2>&1 if grep -i warning docs/build/tmp_breathe/docs_log.txt; then echo "Doxygen warnings found" && cat docs/build/tmp_breathe/docs_log.txt && /bin/false; else @@ -56,5 +60,9 @@ done sed "s/Convention/Convention_/g" < ${TOPDIR}/docs/build/xml/classosgeo_1_1proj_1_1io_1_1WKTFormatter.xml | sed "s/WKT2_2018/_WKT2_2018/g" | sed "s/WKT2_2019/_WKT2_2019/g" | sed "s/WKT2_2015/_WKT2_2015/g" | sed "s/WKT1_GDAL/_WKT1_GDAL/g" | sed "s/WKT1_ESRI/_WKT1_ESRI/g" > ${TOPDIR}/docs/build/xml/classosgeo_1_1proj_1_1io_1_1WKTFormatter.xml.tmp mv ${TOPDIR}/docs/build/xml/classosgeo_1_1proj_1_1io_1_1WKTFormatter.xml.tmp ${TOPDIR}/docs/build/xml/classosgeo_1_1proj_1_1io_1_1WKTFormatter.xml +# Ugly hack to workaround a bug of Doxygen 1.8.17 that erroneously detect proj_network_get_header_value_cbk_type/ as a variable +sed "s/CONST_CHAR/const char/" < ${TOPDIR}/docs/build/xml/proj_8h.xml > ${TOPDIR}/docs/build/xml/proj_8h.xml.tmp +mv ${TOPDIR}/docs/build/xml/proj_8h.xml.tmp ${TOPDIR}/docs/build/xml/proj_8h.xml + popd > /dev/null || exit diff --git a/scripts/reference_exported_symbols.txt b/scripts/reference_exported_symbols.txt index d10f3bc1..1774b79b 100644 --- a/scripts/reference_exported_symbols.txt +++ b/scripts/reference_exported_symbols.txt @@ -279,6 +279,26 @@ osgeo::proj::datum::TemporalDatum::temporalOrigin() const osgeo::proj::datum::VerticalReferenceFrame::create(osgeo::proj::util::PropertyMap const&, osgeo::proj::util::optional<std::string> const&, osgeo::proj::util::optional<osgeo::proj::datum::RealizationMethod> const&) osgeo::proj::datum::VerticalReferenceFrame::realizationMethod() const osgeo::proj::datum::VerticalReferenceFrame::~VerticalReferenceFrame() +osgeo::proj::GenericShiftGrid::~GenericShiftGrid() +osgeo::proj::GenericShiftGrid::GenericShiftGrid(std::string const&, int, int, osgeo::proj::ExtentAndRes const&) +osgeo::proj::GenericShiftGrid::gridAt(double, double) const +osgeo::proj::GenericShiftGridSet::~GenericShiftGridSet() +osgeo::proj::GenericShiftGridSet::GenericShiftGridSet() +osgeo::proj::GenericShiftGridSet::gridAt(double, double) const +osgeo::proj::GenericShiftGridSet::open(projCtx_t*, std::string const&) +osgeo::proj::GenericShiftGridSet::reassign_context(projCtx_t*) +osgeo::proj::GenericShiftGridSet::reopen(projCtx_t*) +osgeo::proj::Grid::~Grid() +osgeo::proj::Grid::Grid(std::string const&, int, int, osgeo::proj::ExtentAndRes const&) +osgeo::proj::HorizontalShiftGrid::gridAt(double, double) const +osgeo::proj::HorizontalShiftGrid::~HorizontalShiftGrid() +osgeo::proj::HorizontalShiftGrid::HorizontalShiftGrid(std::string const&, int, int, osgeo::proj::ExtentAndRes const&) +osgeo::proj::HorizontalShiftGridSet::gridAt(double, double) const +osgeo::proj::HorizontalShiftGridSet::~HorizontalShiftGridSet() +osgeo::proj::HorizontalShiftGridSet::HorizontalShiftGridSet() +osgeo::proj::HorizontalShiftGridSet::open(projCtx_t*, std::string const&) +osgeo::proj::HorizontalShiftGridSet::reassign_context(projCtx_t*) +osgeo::proj::HorizontalShiftGridSet::reopen(projCtx_t*) osgeo::proj::internal::ci_equal(std::string const&, char const*) osgeo::proj::internal::ci_equal(std::string const&, std::string const&) osgeo::proj::internal::ci_find(std::string const&, char const*) @@ -696,6 +716,15 @@ osgeo::proj::util::PropertyMap::set(std::string const&, std::string const&) osgeo::proj::util::PropertyMap::set(std::string const&, std::vector<std::string, std::allocator<std::string> > const&) osgeo::proj::util::UnsupportedOperationException::~UnsupportedOperationException() osgeo::proj::util::UnsupportedOperationException::UnsupportedOperationException(osgeo::proj::util::UnsupportedOperationException const&) +osgeo::proj::VerticalShiftGrid::gridAt(double, double) const +osgeo::proj::VerticalShiftGridSet::gridAt(double, double) const +osgeo::proj::VerticalShiftGridSet::open(projCtx_t*, std::string const&) +osgeo::proj::VerticalShiftGridSet::reassign_context(projCtx_t*) +osgeo::proj::VerticalShiftGridSet::reopen(projCtx_t*) +osgeo::proj::VerticalShiftGridSet::~VerticalShiftGridSet() +osgeo::proj::VerticalShiftGridSet::VerticalShiftGridSet() +osgeo::proj::VerticalShiftGrid::~VerticalShiftGrid() +osgeo::proj::VerticalShiftGrid::VerticalShiftGrid(std::string const&, int, int, osgeo::proj::ExtentAndRes const&) pj_acquire_lock pj_add_type_crs_if_needed(std::string const&) pj_apply_gridshift @@ -798,10 +827,12 @@ proj_context_guess_wkt_dialect proj_context_set_autoclose_database proj_context_set_database_path proj_context_set_enable_network +proj_context_set_fileapi proj_context_set_file_finder proj_context_set_network_callbacks proj_context_set(PJconsts*, projCtx_t*) proj_context_set_search_paths +proj_context_set_sqlite3_vfs_name proj_context_set_url_endpoint proj_context_use_proj4_init_rules proj_convert_conversion_to_other_method diff --git a/src/4D_api.cpp b/src/4D_api.cpp index 9107723d..6b53363a 100644 --- a/src/4D_api.cpp +++ b/src/4D_api.cpp @@ -1461,10 +1461,10 @@ PJ_INFO proj_info (void) { pj_context_get_user_writable_directory(ctx, false).c_str(), &buf_size); } - const char *envPROJ_LIB = getenv("PROJ_LIB"); - buf = path_append(buf, envPROJ_LIB, &buf_size); + const std::string envPROJ_LIB = NS_PROJ::FileManager::getProjLibEnvVar(ctx); + buf = path_append(buf, envPROJ_LIB.empty() ? nullptr : envPROJ_LIB.c_str(), &buf_size); #ifdef PROJ_LIB - if (envPROJ_LIB == nullptr) { + if (envPROJ_LIB.empty()) { buf = path_append(buf, PROJ_LIB, &buf_size); } #endif @@ -1770,3 +1770,4 @@ PJ_FACTORS proj_factors(PJ *P, PJ_COORD lp) { return factors; } + diff --git a/src/Makefile.am b/src/Makefile.am index 39667509..9b513fb3 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -190,7 +190,7 @@ libproj_la_SOURCES = \ deriv.cpp ell_set.cpp ellps.cpp errno.cpp \ factors.cpp fwd.cpp init.cpp inv.cpp \ list.cpp malloc.cpp mlfn.cpp msfn.cpp proj_mdist.cpp \ - open_lib.cpp param.cpp phi2.cpp pr_list.cpp \ + param.cpp phi2.cpp pr_list.cpp \ qsfn.cpp strerrno.cpp \ tsfn.cpp units.cpp ctx.cpp log.cpp zpoly1.cpp rtodms.cpp \ release.cpp gauss.cpp \ @@ -218,8 +218,9 @@ libproj_la_SOURCES = \ grids.cpp \ filemanager.hpp \ filemanager.cpp \ - sqlite3.hpp \ - sqlite3.cpp + networkfilemanager.cpp \ + sqlite3_utils.hpp \ + sqlite3_utils.cpp # The sed hack is to please MSVC diff --git a/src/filemanager.cpp b/src/filemanager.cpp index 9acea83e..592bada4 100644 --- a/src/filemanager.cpp +++ b/src/filemanager.cpp @@ -30,42 +30,17 @@ #endif #define LRU11_DO_NOT_DEFINE_OUT_OF_CLASS_METHODS +#include <errno.h> #include <stdlib.h> #include <algorithm> -#include <codecvt> -#include <functional> #include <limits> -#include <locale> #include <string> #include "filemanager.hpp" #include "proj.h" #include "proj/internal/internal.hpp" -#include "proj/internal/lru_cache.hpp" #include "proj_internal.h" -#include "sqlite3.hpp" - -#ifdef __MINGW32__ -// mingw32-win32 doesn't implement std::mutex -namespace { -class MyMutex { - public: - // cppcheck-suppress functionStatic - void lock() { pj_acquire_lock(); } - // cppcheck-suppress functionStatic - void unlock() { pj_release_lock(); } -}; -} -#else -#include <mutex> -#define MyMutex std::mutex -#endif - -#ifdef CURL_ENABLED -#include <curl/curl.h> -#include <sqlite3.h> // for sqlite3_snprintf -#endif #include <sys/stat.h> @@ -76,17 +51,6 @@ class MyMutex { #include <unistd.h> #endif -#if defined(_WIN32) -#include <windows.h> -#elif defined(__MACH__) && defined(__APPLE__) -#include <mach-o/dyld.h> -#elif defined(__FreeBSD__) -#include <sys/sysctl.h> -#include <sys/types.h> -#endif - -#include <time.h> - //! @cond Doxygen_Suppress #define STR_HELPER(x) #x @@ -98,1531 +62,823 @@ NS_PROJ_START // --------------------------------------------------------------------------- -static void proj_sleep_ms(int ms) { -#ifdef _WIN32 - Sleep(ms); -#else - usleep(ms * 1000); -#endif -} - -// --------------------------------------------------------------------------- - File::File(const std::string &name) : name_(name) {} // --------------------------------------------------------------------------- File::~File() = default; -// --------------------------------------------------------------------------- - -class FileStdio : public File { - PJ_CONTEXT *m_ctx; - FILE *m_fp; +#ifdef _WIN32 - FileStdio(const FileStdio &) = delete; - FileStdio &operator=(const FileStdio &) = delete; +/* The bulk of utf8towc()/utf8fromwc() is derived from the utf.c module from + * FLTK. It was originally downloaded from: + * http://svn.easysw.com/public/fltk/fltk/trunk/src/utf.c + * And already used by GDAL + */ +/************************************************************************/ +/* ==================================================================== */ +/* UTF.C code from FLTK with some modifications. */ +/* ==================================================================== */ +/************************************************************************/ + +/* Set to 1 to turn bad UTF8 bytes into ISO-8859-1. If this is to zero + they are instead turned into the Unicode REPLACEMENT CHARACTER, of + value 0xfffd. + If this is on utf8decode will correctly map most (perhaps all) + human-readable text that is in ISO-8859-1. This may allow you + to completely ignore character sets in your code because virtually + everything is either ISO-8859-1 or UTF-8. +*/ +#define ERRORS_TO_ISO8859_1 1 - protected: - FileStdio(const std::string &name, PJ_CONTEXT *ctx, FILE *fp) - : File(name), m_ctx(ctx), m_fp(fp) {} +/* Set to 1 to turn bad UTF8 bytes in the 0x80-0x9f range into the + Unicode index for Microsoft's CP1252 character set. You should + also set ERRORS_TO_ISO8859_1. With this a huge amount of more + available text (such as all web pages) are correctly converted + to Unicode. +*/ +#define ERRORS_TO_CP1252 1 - public: - ~FileStdio() override; +/* A number of Unicode code points are in fact illegal and should not + be produced by a UTF-8 converter. Turn this on will replace the + bytes in those encodings with errors. If you do this then converting + arbitrary 16-bit data to UTF-8 and then back is not an identity, + which will probably break a lot of software. +*/ +#define STRICT_RFC3629 0 + +#if ERRORS_TO_CP1252 +// Codes 0x80..0x9f from the Microsoft CP1252 character set, translated +// to Unicode: +constexpr unsigned short cp1252[32] = { + 0x20ac, 0x0081, 0x201a, 0x0192, 0x201e, 0x2026, 0x2020, 0x2021, + 0x02c6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008d, 0x017d, 0x008f, + 0x0090, 0x2018, 0x2019, 0x201c, 0x201d, 0x2022, 0x2013, 0x2014, + 0x02dc, 0x2122, 0x0161, 0x203a, 0x0153, 0x009d, 0x017e, 0x0178}; +#endif - size_t read(void *buffer, size_t sizeBytes) override; - bool seek(unsigned long long offset, int whence = SEEK_SET) override; - unsigned long long tell() override; - void reassign_context(PJ_CONTEXT *ctx) override { m_ctx = ctx; } +/************************************************************************/ +/* utf8decode() */ +/************************************************************************/ + +/* + Decode a single UTF-8 encoded character starting at \e p. The + resulting Unicode value (in the range 0-0x10ffff) is returned, + and \e len is set the number of bytes in the UTF-8 encoding + (adding \e len to \e p will point at the next character). + + If \a p points at an illegal UTF-8 encoding, including one that + would go past \e end, or where a code is uses more bytes than + necessary, then *reinterpret_cast<const unsigned char*>(p) is translated as +though it is + in the Microsoft CP1252 character set and \e len is set to 1. + Treating errors this way allows this to decode almost any + ISO-8859-1 or CP1252 text that has been mistakenly placed where + UTF-8 is expected, and has proven very useful. + + If you want errors to be converted to error characters (as the + standards recommend), adding a test to see if the length is + unexpectedly 1 will work: + +\code + if( *p & 0x80 ) + { // What should be a multibyte encoding. + code = utf8decode(p, end, &len); + if( len<2 ) code = 0xFFFD; // Turn errors into REPLACEMENT CHARACTER. + } + else + { // Handle the 1-byte utf8 encoding: + code = *p; + len = 1; + } +\endcode - // We may lie, but the real use case is only for network files - bool hasChanged() const override { return false; } + Direct testing for the 1-byte case (as shown above) will also + speed up the scanning of strings where the majority of characters + are ASCII. +*/ +static unsigned utf8decode(const char *p, const char *end, int *len) { + unsigned char c = *reinterpret_cast<const unsigned char *>(p); + if (c < 0x80) { + *len = 1; + return c; +#if ERRORS_TO_CP1252 + } else if (c < 0xa0) { + *len = 1; + return cp1252[c - 0x80]; +#endif + } else if (c < 0xc2) { + goto FAIL; + } + if (p + 1 >= end || (p[1] & 0xc0) != 0x80) + goto FAIL; + if (c < 0xe0) { + *len = 2; + return ((p[0] & 0x1f) << 6) + ((p[1] & 0x3f)); + } else if (c == 0xe0) { + if ((reinterpret_cast<const unsigned char *>(p))[1] < 0xa0) + goto FAIL; + goto UTF8_3; +#if STRICT_RFC3629 + } else if (c == 0xed) { + // RFC 3629 says surrogate chars are illegal. + if ((reinterpret_cast<const unsigned char *>(p))[1] >= 0xa0) + goto FAIL; + goto UTF8_3; + } else if (c == 0xef) { + // 0xfffe and 0xffff are also illegal characters. + if ((reinterpret_cast<const unsigned char *>(p))[1] == 0xbf && + (reinterpret_cast<const unsigned char *>(p))[2] >= 0xbe) + goto FAIL; + goto UTF8_3; +#endif + } else if (c < 0xf0) { + UTF8_3: + if (p + 2 >= end || (p[2] & 0xc0) != 0x80) + goto FAIL; + *len = 3; + return ((p[0] & 0x0f) << 12) + ((p[1] & 0x3f) << 6) + ((p[2] & 0x3f)); + } else if (c == 0xf0) { + if ((reinterpret_cast<const unsigned char *>(p))[1] < 0x90) + goto FAIL; + goto UTF8_4; + } else if (c < 0xf4) { + UTF8_4: + if (p + 3 >= end || (p[2] & 0xc0) != 0x80 || (p[3] & 0xc0) != 0x80) + goto FAIL; + *len = 4; +#if STRICT_RFC3629 + // RFC 3629 says all codes ending in fffe or ffff are illegal: + if ((p[1] & 0xf) == 0xf && + (reinterpret_cast<const unsigned char *>(p))[2] == 0xbf && + (reinterpret_cast<const unsigned char *>(p))[3] >= 0xbe) + goto FAIL; +#endif + return ((p[0] & 0x07) << 18) + ((p[1] & 0x3f) << 12) + + ((p[2] & 0x3f) << 6) + ((p[3] & 0x3f)); + } else if (c == 0xf4) { + if ((reinterpret_cast<const unsigned char *>(p))[1] > 0x8f) + goto FAIL; // After 0x10ffff. + goto UTF8_4; + } else { + FAIL: + *len = 1; +#if ERRORS_TO_ISO8859_1 + return c; +#else + return 0xfffd; // Unicode REPLACEMENT CHARACTER +#endif + } +} - static std::unique_ptr<File> open(PJ_CONTEXT *ctx, const char *filename); -}; +/************************************************************************/ +/* utf8towc() */ +/************************************************************************/ -// --------------------------------------------------------------------------- +/* Convert a UTF-8 sequence into an array of wchar_t. These + are used by some system calls, especially on Windows. -FileStdio::~FileStdio() { fclose(m_fp); } + \a src points at the UTF-8, and \a srclen is the number of bytes to + convert. -// --------------------------------------------------------------------------- + \a dst points at an array to write, and \a dstlen is the number of + locations in this array. At most \a dstlen-1 words will be + written there, plus a 0 terminating word. Thus this function + will never overwrite the buffer and will always return a + zero-terminated string. If \a dstlen is zero then \a dst can be + null and no data is written, but the length is returned. -size_t FileStdio::read(void *buffer, size_t sizeBytes) { - return fread(buffer, 1, sizeBytes, m_fp); -} + The return value is the number of words that \e would be written + to \a dst if it were long enough, not counting the terminating + zero. If the return value is greater or equal to \a dstlen it + indicates truncation, you can then allocate a new array of size + return+1 and call this again. -// --------------------------------------------------------------------------- + Errors in the UTF-8 are converted as though each byte in the + erroneous string is in the Microsoft CP1252 encoding. This allows + ISO-8859-1 text mistakenly identified as UTF-8 to be printed + correctly. -bool FileStdio::seek(unsigned long long offset, int whence) { - // TODO one day: use 64-bit offset compatible API - if (offset != static_cast<unsigned long long>(static_cast<long>(offset))) { - pj_log(m_ctx, PJ_LOG_ERROR, - "Attempt at seeking to a 64 bit offset. Not supported yet"); - return false; + Notice that sizeof(wchar_t) is 2 on Windows and is 4 on Linux + and most other systems. Where wchar_t is 16 bits, Unicode + characters in the range 0x10000 to 0x10ffff are converted to + "surrogate pairs" which take two words each (this is called UTF-16 + encoding). If wchar_t is 32 bits this rather nasty problem is + avoided. +*/ +static unsigned utf8towc(const char *src, unsigned srclen, wchar_t *dst, + unsigned dstlen) { + const char *p = src; + const char *e = src + srclen; + unsigned count = 0; + if (dstlen) + while (true) { + if (p >= e) { + dst[count] = 0; + return count; + } + if (!(*p & 0x80)) { + // ASCII + dst[count] = *p++; + } else { + int len = 0; + unsigned ucs = utf8decode(p, e, &len); + p += len; +#ifdef _WIN32 + if (ucs < 0x10000) { + dst[count] = static_cast<wchar_t>(ucs); + } else { + // Make a surrogate pair: + if (count + 2 >= dstlen) { + dst[count] = 0; + count += 2; + break; + } + dst[count] = static_cast<wchar_t>( + (((ucs - 0x10000u) >> 10) & 0x3ff) | 0xd800); + dst[++count] = static_cast<wchar_t>((ucs & 0x3ff) | 0xdc00); + } +#else + dst[count] = static_cast<wchar_t>(ucs); +#endif + } + if (++count == dstlen) { + dst[count - 1] = 0; + break; + } + } + // We filled dst, measure the rest: + while (p < e) { + if (!(*p & 0x80)) { + p++; + } else { + int len = 0; +#ifdef _WIN32 + const unsigned ucs = utf8decode(p, e, &len); + p += len; + if (ucs >= 0x10000) + ++count; +#else + utf8decode(p, e, &len); + p += len; +#endif + } + ++count; } - return fseek(m_fp, static_cast<long>(offset), whence) == 0; -} -// --------------------------------------------------------------------------- - -unsigned long long FileStdio::tell() { - // TODO one day: use 64-bit offset compatible API - return ftell(m_fp); + return count; } // --------------------------------------------------------------------------- -std::unique_ptr<File> FileStdio::open(PJ_CONTEXT *ctx, const char *filename) { - auto fp = fopen(filename, "rb"); - return std::unique_ptr<File>(fp ? new FileStdio(filename, ctx, fp) - : nullptr); +struct NonValidUTF8Exception : public std::exception {}; + +// May throw exceptions +static std::wstring UTF8ToWString(const std::string &str) { + std::wstring wstr; + wstr.resize(str.size()); + wstr.resize(utf8towc(str.data(), static_cast<unsigned>(str.size()), + &wstr[0], static_cast<unsigned>(wstr.size()) + 1)); + for (const auto ch : wstr) { + if (ch == 0xfffd) { + throw NonValidUTF8Exception(); + } + } + return wstr; } // --------------------------------------------------------------------------- -#ifndef REMOVE_LEGACY_SUPPORT - -class FileLegacyAdapter : public File { - PJ_CONTEXT *m_ctx; - PAFile m_fp; - - FileLegacyAdapter(const FileLegacyAdapter &) = delete; - FileLegacyAdapter &operator=(const FileLegacyAdapter &) = delete; - - protected: - FileLegacyAdapter(const std::string &name, PJ_CONTEXT *ctx, PAFile fp) - : File(name), m_ctx(ctx), m_fp(fp) {} - - public: - ~FileLegacyAdapter() override; - - size_t read(void *buffer, size_t sizeBytes) override; - bool seek(unsigned long long offset, int whence = SEEK_SET) override; - unsigned long long tell() override; - void reassign_context(PJ_CONTEXT *ctx) override { m_ctx = ctx; } - - // We may lie, but the real use case is only for network files - bool hasChanged() const override { return false; } +/************************************************************************/ +/* utf8fromwc() */ +/************************************************************************/ +/* Turn "wide characters" as returned by some system calls + (especially on Windows) into UTF-8. - static std::unique_ptr<File> open(PJ_CONTEXT *ctx, const char *filename); -}; + Up to \a dstlen bytes are written to \a dst, including a null + terminator. The return value is the number of bytes that would be + written, not counting the null terminator. If greater or equal to + \a dstlen then if you malloc a new array of size n+1 you will have + the space needed for the entire string. If \a dstlen is zero then + nothing is written and this call just measures the storage space + needed. -// --------------------------------------------------------------------------- + \a srclen is the number of words in \a src to convert. On Windows + this is not necessarily the number of characters, due to there + possibly being "surrogate pairs" in the UTF-16 encoding used. + On Unix wchar_t is 32 bits and each location is a character. -FileLegacyAdapter::~FileLegacyAdapter() { pj_ctx_fclose(m_ctx, m_fp); } + On Unix if a src word is greater than 0x10ffff then this is an + illegal character according to RFC 3629. These are converted as + though they are 0xFFFD (REPLACEMENT CHARACTER). Characters in the + range 0xd800 to 0xdfff, or ending with 0xfffe or 0xffff are also + illegal according to RFC 3629. However I encode these as though + they are legal, so that utf8towc will return the original data. -// --------------------------------------------------------------------------- - -size_t FileLegacyAdapter::read(void *buffer, size_t sizeBytes) { - return pj_ctx_fread(m_ctx, buffer, 1, sizeBytes, m_fp); -} - -// --------------------------------------------------------------------------- + On Windows "surrogate pairs" are converted to a single character + and UTF-8 encoded (as 4 bytes). Mismatched halves of surrogate + pairs are converted as though they are individual characters. +*/ +static unsigned int utf8fromwc(char *dst, unsigned dstlen, const wchar_t *src, + unsigned srclen) { + unsigned int i = 0; + unsigned int count = 0; + if (dstlen) + while (true) { + if (i >= srclen) { + dst[count] = 0; + return count; + } + unsigned int ucs = src[i++]; + if (ucs < 0x80U) { + dst[count++] = static_cast<char>(ucs); + if (count >= dstlen) { + dst[count - 1] = 0; + break; + } + } else if (ucs < 0x800U) { + // 2 bytes. + if (count + 2 >= dstlen) { + dst[count] = 0; + count += 2; + break; + } + dst[count++] = 0xc0 | static_cast<char>(ucs >> 6); + dst[count++] = 0x80 | static_cast<char>(ucs & 0x3F); +#ifdef _WIN32 + } else if (ucs >= 0xd800 && ucs <= 0xdbff && i < srclen && + src[i] >= 0xdc00 && src[i] <= 0xdfff) { + // Surrogate pair. + unsigned int ucs2 = src[i++]; + ucs = 0x10000U + ((ucs & 0x3ff) << 10) + (ucs2 & 0x3ff); +// All surrogate pairs turn into 4-byte utf8. +#else + } else if (ucs >= 0x10000) { + if (ucs > 0x10ffff) { + ucs = 0xfffd; + goto J1; + } +#endif + if (count + 4 >= dstlen) { + dst[count] = 0; + count += 4; + break; + } + dst[count++] = 0xf0 | static_cast<char>(ucs >> 18); + dst[count++] = 0x80 | static_cast<char>((ucs >> 12) & 0x3F); + dst[count++] = 0x80 | static_cast<char>((ucs >> 6) & 0x3F); + dst[count++] = 0x80 | static_cast<char>(ucs & 0x3F); + } else { +#ifndef _WIN32 + J1: +#endif + // All others are 3 bytes: + if (count + 3 >= dstlen) { + dst[count] = 0; + count += 3; + break; + } + dst[count++] = 0xe0 | static_cast<char>(ucs >> 12); + dst[count++] = 0x80 | static_cast<char>((ucs >> 6) & 0x3F); + dst[count++] = 0x80 | static_cast<char>(ucs & 0x3F); + } + } -bool FileLegacyAdapter::seek(unsigned long long offset, int whence) { - if (offset != static_cast<unsigned long long>(static_cast<long>(offset))) { - pj_log(m_ctx, PJ_LOG_ERROR, - "Attempt at seeking to a 64 bit offset. Not supported yet"); - return false; + // We filled dst, measure the rest: + while (i < srclen) { + unsigned int ucs = src[i++]; + if (ucs < 0x80U) { + count++; + } else if (ucs < 0x800U) { + // 2 bytes. + count += 2; +#ifdef _WIN32 + } else if (ucs >= 0xd800 && ucs <= 0xdbff && i < srclen - 1 && + src[i + 1] >= 0xdc00 && src[i + 1] <= 0xdfff) { + // Surrogate pair. + ++i; +#else + } else if (ucs >= 0x10000 && ucs <= 0x10ffff) { +#endif + count += 4; + } else { + count += 3; + } } - return pj_ctx_fseek(m_ctx, m_fp, static_cast<long>(offset), whence) == 0; + return count; } // --------------------------------------------------------------------------- -unsigned long long FileLegacyAdapter::tell() { - return pj_ctx_ftell(m_ctx, m_fp); -} - -// --------------------------------------------------------------------------- - -std::unique_ptr<File> FileLegacyAdapter::open(PJ_CONTEXT *ctx, - const char *filename) { - auto fid = pj_ctx_fopen(ctx, filename, "rb"); - return std::unique_ptr<File>(fid ? new FileLegacyAdapter(filename, ctx, fid) - : nullptr); +static std::string WStringToUTF8(const std::wstring &wstr) { + std::string str; + str.resize(wstr.size()); + str.resize(utf8fromwc(&str[0], static_cast<unsigned>(str.size() + 1), + wstr.data(), static_cast<unsigned>(wstr.size()))); + return str; } -#endif // REMOVE_LEGACY_SUPPORT - // --------------------------------------------------------------------------- -constexpr size_t DOWNLOAD_CHUNK_SIZE = 16 * 1024; -constexpr int MAX_CHUNKS = 64; - -struct FileProperties { - unsigned long long size = 0; - time_t lastChecked = 0; - std::string lastModified{}; - std::string etag{}; -}; - -class NetworkChunkCache { - public: - void insert(PJ_CONTEXT *ctx, const std::string &url, - unsigned long long chunkIdx, std::vector<unsigned char> &&data); - - std::shared_ptr<std::vector<unsigned char>> - get(PJ_CONTEXT *ctx, const std::string &url, unsigned long long chunkIdx); +static std::string Win32Recode(const char *src, unsigned src_code_page, + unsigned dst_code_page) { + // Convert from source code page to Unicode. - std::shared_ptr<std::vector<unsigned char>> get(PJ_CONTEXT *ctx, - const std::string &url, - unsigned long long chunkIdx, - FileProperties &props); + // Compute the length in wide characters. + int wlen = MultiByteToWideChar(src_code_page, MB_ERR_INVALID_CHARS, src, -1, + nullptr, 0); + if (wlen == 0 && GetLastError() == ERROR_NO_UNICODE_TRANSLATION) { + return std::string(); + } - void clearMemoryCache(); + // Do the actual conversion. + std::wstring wbuf; + wbuf.resize(wlen); + MultiByteToWideChar(src_code_page, 0, src, -1, &wbuf[0], wlen); - static void clearDiskChunkCache(PJ_CONTEXT *ctx); + // Convert from Unicode to destination code page. - private: - struct Key { - std::string url; - unsigned long long chunkIdx; + // Compute the length in chars. + int len = WideCharToMultiByte(dst_code_page, 0, &wbuf[0], -1, nullptr, 0, + nullptr, nullptr); - Key(const std::string &urlIn, unsigned long long chunkIdxIn) - : url(urlIn), chunkIdx(chunkIdxIn) {} - bool operator==(const Key &other) const { - return url == other.url && chunkIdx == other.chunkIdx; - } - }; + // Do the actual conversion. + std::string out; + out.resize(len); + WideCharToMultiByte(dst_code_page, 0, &wbuf[0], -1, &out[0], len, nullptr, + nullptr); + out.resize(strlen(out.c_str())); - struct KeyHasher { - std::size_t operator()(const Key &k) const { - return std::hash<std::string>{}(k.url) ^ - (std::hash<unsigned long long>{}(k.chunkIdx) << 1); - } - }; - - lru11::Cache< - Key, std::shared_ptr<std::vector<unsigned char>>, MyMutex, - std::unordered_map< - Key, - typename std::list<lru11::KeyValuePair< - Key, std::shared_ptr<std::vector<unsigned char>>>>::iterator, - KeyHasher>> - cache_{MAX_CHUNKS}; -}; + return out; +} // --------------------------------------------------------------------------- -static NetworkChunkCache gNetworkChunkCache{}; +class FileWin32 : public File { + PJ_CONTEXT *m_ctx; + HANDLE m_handle; -// --------------------------------------------------------------------------- + FileWin32(const FileWin32 &) = delete; + FileWin32 &operator=(const FileWin32 &) = delete; + + protected: + FileWin32(const std::string &name, PJ_CONTEXT *ctx, HANDLE handle) + : File(name), m_ctx(ctx), m_handle(handle) {} -class NetworkFilePropertiesCache { public: - void insert(PJ_CONTEXT *ctx, const std::string &url, FileProperties &props); + ~FileWin32() override; - bool tryGet(PJ_CONTEXT *ctx, const std::string &url, FileProperties &props); + size_t read(void *buffer, size_t sizeBytes) override; + size_t write(const void *buffer, size_t sizeBytes) override; + bool seek(unsigned long long offset, int whence = SEEK_SET) override; + unsigned long long tell() override; + void reassign_context(PJ_CONTEXT *ctx) override { m_ctx = ctx; } - void clearMemoryCache(); + // We may lie, but the real use case is only for network files + bool hasChanged() const override { return false; } - private: - lru11::Cache<std::string, FileProperties, MyMutex> cache_{}; + static std::unique_ptr<File> open(PJ_CONTEXT *ctx, const char *filename, + FileAccess access); }; // --------------------------------------------------------------------------- -static NetworkFilePropertiesCache gNetworkFileProperties{}; +FileWin32::~FileWin32() { CloseHandle(m_handle); } // --------------------------------------------------------------------------- -class DiskChunkCache { - PJ_CONTEXT *ctx_ = nullptr; - std::string path_{}; - sqlite3 *hDB_ = nullptr; - std::string thisNamePtr_{}; - std::unique_ptr<SQLite3VFS> vfs_{}; - - explicit DiskChunkCache(PJ_CONTEXT *ctx, const std::string &path); - - bool createDBStructure(); - bool checkConsistency(); - bool get_links(sqlite3_int64 chunk_id, sqlite3_int64 &link_id, - sqlite3_int64 &prev, sqlite3_int64 &next, - sqlite3_int64 &head, sqlite3_int64 &tail); - bool update_links_of_prev_and_next_links(sqlite3_int64 prev, - sqlite3_int64 next); - bool update_linked_chunks(sqlite3_int64 link_id, sqlite3_int64 prev, - sqlite3_int64 next); - bool update_linked_chunks_head_tail(sqlite3_int64 head, sqlite3_int64 tail); - - DiskChunkCache(const DiskChunkCache &) = delete; - DiskChunkCache &operator=(const DiskChunkCache &) = delete; - - public: - static std::unique_ptr<DiskChunkCache> open(PJ_CONTEXT *ctx); - ~DiskChunkCache(); - - sqlite3 *handle() { return hDB_; } - std::unique_ptr<SQLiteStatement> prepare(const char *sql); - bool move_to_head(sqlite3_int64 chunk_id); - bool move_to_tail(sqlite3_int64 chunk_id); - void closeAndUnlink(); -}; +size_t FileWin32::read(void *buffer, size_t sizeBytes) { + DWORD dwSizeRead = 0; + size_t nResult = 0; -// --------------------------------------------------------------------------- + if (!ReadFile(m_handle, buffer, static_cast<DWORD>(sizeBytes), &dwSizeRead, + nullptr)) + nResult = 0; + else + nResult = dwSizeRead; -static bool pj_context_get_grid_cache_is_enabled(PJ_CONTEXT *ctx) { - pj_load_ini(ctx); - return ctx->gridChunkCache.enabled; + return nResult; } // --------------------------------------------------------------------------- -static long long pj_context_get_grid_cache_max_size(PJ_CONTEXT *ctx) { - pj_load_ini(ctx); - return ctx->gridChunkCache.max_size; -} +size_t FileWin32::write(const void *buffer, size_t sizeBytes) { + DWORD dwSizeWritten = 0; + size_t nResult = 0; -// --------------------------------------------------------------------------- + if (!WriteFile(m_handle, buffer, static_cast<DWORD>(sizeBytes), + &dwSizeWritten, nullptr)) + nResult = 0; + else + nResult = dwSizeWritten; -static int pj_context_get_grid_cache_ttl(PJ_CONTEXT *ctx) { - pj_load_ini(ctx); - return ctx->gridChunkCache.ttl; + return nResult; } // --------------------------------------------------------------------------- -std::unique_ptr<DiskChunkCache> DiskChunkCache::open(PJ_CONTEXT *ctx) { - if (!pj_context_get_grid_cache_is_enabled(ctx)) { - return nullptr; - } - const auto cachePath = pj_context_get_grid_cache_filename(ctx); - if (cachePath.empty()) { - return nullptr; +bool FileWin32::seek(unsigned long long offset, int whence) { + LONG dwMoveMethod, dwMoveHigh; + uint32_t nMoveLow; + LARGE_INTEGER li; + + switch (whence) { + case SEEK_CUR: + dwMoveMethod = FILE_CURRENT; + break; + case SEEK_END: + dwMoveMethod = FILE_END; + break; + case SEEK_SET: + default: + dwMoveMethod = FILE_BEGIN; + break; } - auto diskCache = - std::unique_ptr<DiskChunkCache>(new DiskChunkCache(ctx, cachePath)); - if (!diskCache->hDB_) - diskCache.reset(); - return diskCache; + li.QuadPart = offset; + nMoveLow = li.LowPart; + dwMoveHigh = li.HighPart; + + SetLastError(0); + SetFilePointer(m_handle, nMoveLow, &dwMoveHigh, dwMoveMethod); + + return GetLastError() == NO_ERROR; } // --------------------------------------------------------------------------- -DiskChunkCache::DiskChunkCache(PJ_CONTEXT *ctx, const std::string &path) - : ctx_(ctx), path_(path), vfs_(SQLite3VFS::create(true, false, false)) { - if (vfs_ == nullptr) { - return; - } - sqlite3_open_v2(path.c_str(), &hDB_, - SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, vfs_->name()); - if (!hDB_) { - return; - } - for (int i = 0;; i++) { - int ret = - sqlite3_exec(hDB_, "BEGIN EXCLUSIVE", nullptr, nullptr, nullptr); - if (ret == SQLITE_OK) { - break; - } - if (ret != SQLITE_BUSY) { - pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); - sqlite3_close(hDB_); - hDB_ = nullptr; - return; - } - const char *max_iters = getenv("PROJ_LOCK_MAX_ITERS"); - if (i >= (max_iters && max_iters[0] ? atoi(max_iters) - : 30)) { // A bit more than 1 second - pj_log(ctx_, PJ_LOG_ERROR, "Cannot take exclusive lock on %s", - path.c_str()); - sqlite3_close(hDB_); - hDB_ = nullptr; - return; - } - pj_log(ctx, PJ_LOG_TRACE, "Lock taken on cache. Waiting a bit..."); - // Retry every 5 ms for 50 ms, then every 10 ms for 100 ms, then - // every 100 ms - proj_sleep_ms(i < 10 ? 5 : i < 20 ? 10 : 100); - } - char **pasResult = nullptr; - int nRows = 0; - int nCols = 0; - sqlite3_get_table(hDB_, - "SELECT 1 FROM sqlite_master WHERE name = 'properties'", - &pasResult, &nRows, &nCols, nullptr); - sqlite3_free_table(pasResult); - if (nRows == 0) { - if (!createDBStructure()) { - sqlite3_close(hDB_); - hDB_ = nullptr; - return; - } - } +unsigned long long FileWin32::tell() { + LARGE_INTEGER li; - if (getenv("PROJ_CHECK_CACHE_CONSISTENCY")) { - checkConsistency(); - } -} + li.HighPart = 0; + li.LowPart = SetFilePointer(m_handle, 0, &(li.HighPart), FILE_CURRENT); + return static_cast<unsigned long long>(li.QuadPart); +} // --------------------------------------------------------------------------- -static const char *cache_db_structure_sql = - "CREATE TABLE properties(" - " url TEXT PRIMARY KEY NOT NULL," - " lastChecked TIMESTAMP NOT NULL," - " fileSize INTEGER NOT NULL," - " lastModified TEXT," - " etag TEXT" - ");" - "CREATE TABLE downloaded_file_properties(" - " url TEXT PRIMARY KEY NOT NULL," - " lastChecked TIMESTAMP NOT NULL," - " fileSize INTEGER NOT NULL," - " lastModified TEXT," - " etag TEXT" - ");" - "CREATE TABLE chunk_data(" - " id INTEGER PRIMARY KEY AUTOINCREMENT CHECK (id > 0)," - " data BLOB NOT NULL" - ");" - "CREATE TABLE chunks(" - " id INTEGER PRIMARY KEY AUTOINCREMENT CHECK (id > 0)," - " url TEXT NOT NULL," - " offset INTEGER NOT NULL," - " data_id INTEGER NOT NULL," - " data_size INTEGER NOT NULL," - " CONSTRAINT fk_chunks_url FOREIGN KEY (url) REFERENCES properties(url)," - " CONSTRAINT fk_chunks_data FOREIGN KEY (data_id) REFERENCES chunk_data(id)" - ");" - "CREATE INDEX idx_chunks ON chunks(url, offset);" - "CREATE TABLE linked_chunks(" - " id INTEGER PRIMARY KEY AUTOINCREMENT CHECK (id > 0)," - " chunk_id INTEGER NOT NULL," - " prev INTEGER," - " next INTEGER," - " CONSTRAINT fk_links_chunkid FOREIGN KEY (chunk_id) REFERENCES chunks(id)," - " CONSTRAINT fk_links_prev FOREIGN KEY (prev) REFERENCES linked_chunks(id)," - " CONSTRAINT fk_links_next FOREIGN KEY (next) REFERENCES linked_chunks(id)" - ");" - "CREATE INDEX idx_linked_chunks_chunk_id ON linked_chunks(chunk_id);" - "CREATE TABLE linked_chunks_head_tail(" - " head INTEGER," - " tail INTEGER," - " CONSTRAINT lht_head FOREIGN KEY (head) REFERENCES linked_chunks(id)," - " CONSTRAINT lht_tail FOREIGN KEY (tail) REFERENCES linked_chunks(id)" - ");" - "INSERT INTO linked_chunks_head_tail VALUES (NULL, NULL);"; - -bool DiskChunkCache::createDBStructure() { - - pj_log(ctx_, PJ_LOG_TRACE, "Creating cache DB structure"); - if (sqlite3_exec(hDB_, cache_db_structure_sql, nullptr, nullptr, nullptr) != - SQLITE_OK) { - pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); - return false; +std::unique_ptr<File> FileWin32::open(PJ_CONTEXT *ctx, const char *filename, + FileAccess access) { + DWORD dwDesiredAccess = access == FileAccess::READ_ONLY + ? GENERIC_READ + : GENERIC_READ | GENERIC_WRITE; + DWORD dwCreationDisposition = + access == FileAccess::CREATE ? CREATE_ALWAYS : OPEN_EXISTING; + DWORD dwFlagsAndAttributes = (dwDesiredAccess == GENERIC_READ) + ? FILE_ATTRIBUTE_READONLY + : FILE_ATTRIBUTE_NORMAL; + try { + HANDLE hFile = CreateFileW( + UTF8ToWString(std::string(filename)).c_str(), dwDesiredAccess, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + dwCreationDisposition, dwFlagsAndAttributes, nullptr); + return std::unique_ptr<File>(hFile != INVALID_HANDLE_VALUE + ? new FileWin32(filename, ctx, hFile) + : nullptr); + } catch (const std::exception &e) { + pj_log(ctx, PJ_LOG_DEBUG, "%s", e.what()); + return nullptr; } - return true; } +#else // --------------------------------------------------------------------------- -#define INVALIDATED_SQL_LITERAL "'invalidated'" - -bool DiskChunkCache::checkConsistency() { - - auto stmt = prepare("SELECT * FROM chunk_data WHERE id NOT IN (SELECT " - "data_id FROM chunks)"); - if (!stmt) { - return false; - } - if (stmt->execute() != SQLITE_DONE) { - fprintf(stderr, "Rows in chunk_data not referenced by chunks.\n"); - return false; - } +class FileStdio : public File { + PJ_CONTEXT *m_ctx; + FILE *m_fp; - stmt = prepare("SELECT * FROM chunks WHERE id NOT IN (SELECT chunk_id FROM " - "linked_chunks)"); - if (!stmt) { - return false; - } - if (stmt->execute() != SQLITE_DONE) { - fprintf(stderr, "Rows in chunks not referenced by linked_chunks.\n"); - return false; - } + FileStdio(const FileStdio &) = delete; + FileStdio &operator=(const FileStdio &) = delete; - stmt = prepare("SELECT * FROM chunks WHERE url <> " INVALIDATED_SQL_LITERAL - " AND url " - "NOT IN (SELECT url FROM properties)"); - if (!stmt) { - return false; - } - if (stmt->execute() != SQLITE_DONE) { - fprintf(stderr, "url values in chunks not referenced by properties.\n"); - return false; - } + protected: + FileStdio(const std::string &name, PJ_CONTEXT *ctx, FILE *fp) + : File(name), m_ctx(ctx), m_fp(fp) {} - stmt = prepare("SELECT head, tail FROM linked_chunks_head_tail"); - if (!stmt) { - return false; - } - if (stmt->execute() != SQLITE_ROW) { - fprintf(stderr, "linked_chunks_head_tail empty.\n"); - return false; - } - const auto head = stmt->getInt64(); - const auto tail = stmt->getInt64(); - if (stmt->execute() != SQLITE_DONE) { - fprintf(stderr, "linked_chunks_head_tail has more than one row.\n"); - return false; - } + public: + ~FileStdio() override; - stmt = prepare("SELECT COUNT(*) FROM linked_chunks"); - if (!stmt) { - return false; - } - if (stmt->execute() != SQLITE_ROW) { - fprintf(stderr, "linked_chunks_head_tail empty.\n"); - return false; - } - const auto count_linked_chunks = stmt->getInt64(); - - if (head) { - auto id = head; - std::set<sqlite3_int64> visitedIds; - stmt = prepare("SELECT next FROM linked_chunks WHERE id = ?"); - if (!stmt) { - return false; - } - while (true) { - visitedIds.insert(id); - stmt->reset(); - stmt->bindInt64(id); - if (stmt->execute() != SQLITE_ROW) { - fprintf(stderr, "cannot find linked_chunks.id = %d.\n", - static_cast<int>(id)); - return false; - } - auto next = stmt->getInt64(); - if (next == 0) { - if (id != tail) { - fprintf(stderr, - "last item when following next is not tail.\n"); - return false; - } - break; - } - if (visitedIds.find(next) != visitedIds.end()) { - fprintf(stderr, "found cycle on linked_chunks.next = %d.\n", - static_cast<int>(next)); - return false; - } - id = next; - } - if (visitedIds.size() != static_cast<size_t>(count_linked_chunks)) { - fprintf(stderr, - "ghost items in linked_chunks when following next.\n"); - return false; - } - } else if (count_linked_chunks) { - fprintf(stderr, "linked_chunks_head_tail.head = NULL but linked_chunks " - "not empty.\n"); - return false; - } + size_t read(void *buffer, size_t sizeBytes) override; + size_t write(const void *buffer, size_t sizeBytes) override; + bool seek(unsigned long long offset, int whence = SEEK_SET) override; + unsigned long long tell() override; + void reassign_context(PJ_CONTEXT *ctx) override { m_ctx = ctx; } - if (tail) { - auto id = tail; - std::set<sqlite3_int64> visitedIds; - stmt = prepare("SELECT prev FROM linked_chunks WHERE id = ?"); - if (!stmt) { - return false; - } - while (true) { - visitedIds.insert(id); - stmt->reset(); - stmt->bindInt64(id); - if (stmt->execute() != SQLITE_ROW) { - fprintf(stderr, "cannot find linked_chunks.id = %d.\n", - static_cast<int>(id)); - return false; - } - auto prev = stmt->getInt64(); - if (prev == 0) { - if (id != head) { - fprintf(stderr, - "last item when following prev is not head.\n"); - return false; - } - break; - } - if (visitedIds.find(prev) != visitedIds.end()) { - fprintf(stderr, "found cycle on linked_chunks.prev = %d.\n", - static_cast<int>(prev)); - return false; - } - id = prev; - } - if (visitedIds.size() != static_cast<size_t>(count_linked_chunks)) { - fprintf(stderr, - "ghost items in linked_chunks when following prev.\n"); - return false; - } - } else if (count_linked_chunks) { - fprintf(stderr, "linked_chunks_head_tail.tail = NULL but linked_chunks " - "not empty.\n"); - return false; - } + // We may lie, but the real use case is only for network files + bool hasChanged() const override { return false; } - fprintf(stderr, "check ok\n"); - return true; -} + static std::unique_ptr<File> open(PJ_CONTEXT *ctx, const char *filename, + FileAccess access); +}; // --------------------------------------------------------------------------- -DiskChunkCache::~DiskChunkCache() { - if (hDB_) { - sqlite3_exec(hDB_, "COMMIT", nullptr, nullptr, nullptr); - sqlite3_close(hDB_); - } -} +FileStdio::~FileStdio() { fclose(m_fp); } // --------------------------------------------------------------------------- -void DiskChunkCache::closeAndUnlink() { - if (hDB_) { - sqlite3_exec(hDB_, "COMMIT", nullptr, nullptr, nullptr); - sqlite3_close(hDB_); - hDB_ = nullptr; - } - if (vfs_) { - vfs_->raw()->xDelete(vfs_->raw(), path_.c_str(), 0); - } +size_t FileStdio::read(void *buffer, size_t sizeBytes) { + return fread(buffer, 1, sizeBytes, m_fp); } // --------------------------------------------------------------------------- -std::unique_ptr<SQLiteStatement> DiskChunkCache::prepare(const char *sql) { - sqlite3_stmt *hStmt = nullptr; - sqlite3_prepare_v2(hDB_, sql, -1, &hStmt, nullptr); - if (!hStmt) { - pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); - return nullptr; - } - return std::unique_ptr<SQLiteStatement>(new SQLiteStatement(hStmt)); +size_t FileStdio::write(const void *buffer, size_t sizeBytes) { + return fwrite(buffer, 1, sizeBytes, m_fp); } // --------------------------------------------------------------------------- -bool DiskChunkCache::get_links(sqlite3_int64 chunk_id, sqlite3_int64 &link_id, - sqlite3_int64 &prev, sqlite3_int64 &next, - sqlite3_int64 &head, sqlite3_int64 &tail) { - auto stmt = - prepare("SELECT id, prev, next FROM linked_chunks WHERE chunk_id = ?"); - if (!stmt) +bool FileStdio::seek(unsigned long long offset, int whence) { + // TODO one day: use 64-bit offset compatible API + if (offset != static_cast<unsigned long long>(static_cast<long>(offset))) { + pj_log(m_ctx, PJ_LOG_ERROR, + "Attempt at seeking to a 64 bit offset. Not supported yet"); return false; - stmt->bindInt64(chunk_id); - { - const auto ret = stmt->execute(); - if (ret != SQLITE_ROW) { - pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); - return false; - } } - link_id = stmt->getInt64(); - prev = stmt->getInt64(); - next = stmt->getInt64(); - - stmt = prepare("SELECT head, tail FROM linked_chunks_head_tail"); - { - const auto ret = stmt->execute(); - if (ret != SQLITE_ROW) { - pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); - return false; - } - } - head = stmt->getInt64(); - tail = stmt->getInt64(); - return true; + return fseek(m_fp, static_cast<long>(offset), whence) == 0; } // --------------------------------------------------------------------------- -bool DiskChunkCache::update_links_of_prev_and_next_links(sqlite3_int64 prev, - sqlite3_int64 next) { - if (prev) { - auto stmt = prepare("UPDATE linked_chunks SET next = ? WHERE id = ?"); - if (!stmt) - return false; - if (next) - stmt->bindInt64(next); - else - stmt->bindNull(); - stmt->bindInt64(prev); - const auto ret = stmt->execute(); - if (ret != SQLITE_DONE) { - pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); - return false; - } - } - - if (next) { - auto stmt = prepare("UPDATE linked_chunks SET prev = ? WHERE id = ?"); - if (!stmt) - return false; - if (prev) - stmt->bindInt64(prev); - else - stmt->bindNull(); - stmt->bindInt64(next); - const auto ret = stmt->execute(); - if (ret != SQLITE_DONE) { - pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); - return false; - } - } - return true; +unsigned long long FileStdio::tell() { + // TODO one day: use 64-bit offset compatible API + return ftell(m_fp); } // --------------------------------------------------------------------------- -bool DiskChunkCache::update_linked_chunks(sqlite3_int64 link_id, - sqlite3_int64 prev, - sqlite3_int64 next) { - auto stmt = - prepare("UPDATE linked_chunks SET prev = ?, next = ? WHERE id = ?"); - if (!stmt) - return false; - if (prev) - stmt->bindInt64(prev); - else - stmt->bindNull(); - if (next) - stmt->bindInt64(next); - else - stmt->bindNull(); - stmt->bindInt64(link_id); - const auto ret = stmt->execute(); - if (ret != SQLITE_DONE) { - pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); - return false; - } - return true; +std::unique_ptr<File> FileStdio::open(PJ_CONTEXT *ctx, const char *filename, + FileAccess access) { + auto fp = fopen(filename, + access == FileAccess::READ_ONLY + ? "rb" + : access == FileAccess::READ_UPDATE ? "r+b" : "w+b"); + return std::unique_ptr<File>(fp ? new FileStdio(filename, ctx, fp) + : nullptr); } -// --------------------------------------------------------------------------- - -bool DiskChunkCache::update_linked_chunks_head_tail(sqlite3_int64 head, - sqlite3_int64 tail) { - auto stmt = - prepare("UPDATE linked_chunks_head_tail SET head = ?, tail = ?"); - if (!stmt) - return false; - if (head) - stmt->bindInt64(head); - else - stmt->bindNull(); // shouldn't happen normally - if (tail) - stmt->bindInt64(tail); - else - stmt->bindNull(); // shouldn't happen normally - const auto ret = stmt->execute(); - if (ret != SQLITE_DONE) { - pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); - return false; - } - return true; -} +#endif // _WIN32 // --------------------------------------------------------------------------- -bool DiskChunkCache::move_to_head(sqlite3_int64 chunk_id) { - - sqlite3_int64 link_id = 0; - sqlite3_int64 prev = 0; - sqlite3_int64 next = 0; - sqlite3_int64 head = 0; - sqlite3_int64 tail = 0; - if (!get_links(chunk_id, link_id, prev, next, head, tail)) { - return false; - } - - if (link_id == head) { - return true; - } - - if (!update_links_of_prev_and_next_links(prev, next)) { - return false; - } - - if (head) { - auto stmt = prepare("UPDATE linked_chunks SET prev = ? WHERE id = ?"); - if (!stmt) - return false; - stmt->bindInt64(link_id); - stmt->bindInt64(head); - const auto ret = stmt->execute(); - if (ret != SQLITE_DONE) { - pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); - return false; - } - } +#ifndef REMOVE_LEGACY_SUPPORT - return update_linked_chunks(link_id, 0, head) && - update_linked_chunks_head_tail(link_id, - (link_id == tail) ? prev : tail); -} +class FileLegacyAdapter : public File { + PJ_CONTEXT *m_ctx; + PAFile m_fp; -// --------------------------------------------------------------------------- + FileLegacyAdapter(const FileLegacyAdapter &) = delete; + FileLegacyAdapter &operator=(const FileLegacyAdapter &) = delete; -bool DiskChunkCache::move_to_tail(sqlite3_int64 chunk_id) { - sqlite3_int64 link_id = 0; - sqlite3_int64 prev = 0; - sqlite3_int64 next = 0; - sqlite3_int64 head = 0; - sqlite3_int64 tail = 0; - if (!get_links(chunk_id, link_id, prev, next, head, tail)) { - return false; - } + protected: + FileLegacyAdapter(const std::string &name, PJ_CONTEXT *ctx, PAFile fp) + : File(name), m_ctx(ctx), m_fp(fp) {} - if (link_id == tail) { - return true; - } + public: + ~FileLegacyAdapter() override; - if (!update_links_of_prev_and_next_links(prev, next)) { - return false; - } + size_t read(void *buffer, size_t sizeBytes) override; + size_t write(const void *, size_t) override { return 0; } + bool seek(unsigned long long offset, int whence = SEEK_SET) override; + unsigned long long tell() override; + void reassign_context(PJ_CONTEXT *ctx) override { m_ctx = ctx; } - if (tail) { - auto stmt = prepare("UPDATE linked_chunks SET next = ? WHERE id = ?"); - if (!stmt) - return false; - stmt->bindInt64(link_id); - stmt->bindInt64(tail); - const auto ret = stmt->execute(); - if (ret != SQLITE_DONE) { - pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); - return false; - } - } + // We may lie, but the real use case is only for network files + bool hasChanged() const override { return false; } - return update_linked_chunks(link_id, tail, 0) && - update_linked_chunks_head_tail((link_id == head) ? next : head, - link_id); -} + static std::unique_ptr<File> open(PJ_CONTEXT *ctx, const char *filename, + FileAccess access); +}; // --------------------------------------------------------------------------- -void NetworkChunkCache::insert(PJ_CONTEXT *ctx, const std::string &url, - unsigned long long chunkIdx, - std::vector<unsigned char> &&data) { - auto dataPtr(std::make_shared<std::vector<unsigned char>>(std::move(data))); - cache_.insert(Key(url, chunkIdx), dataPtr); - - auto diskCache = DiskChunkCache::open(ctx); - if (!diskCache) - return; - auto hDB = diskCache->handle(); - - // Always insert DOWNLOAD_CHUNK_SIZE bytes to avoid fragmentation - std::vector<unsigned char> blob(*dataPtr); - assert(blob.size() <= DOWNLOAD_CHUNK_SIZE); - blob.resize(DOWNLOAD_CHUNK_SIZE); - - // Check if there is an existing entry for that URL and offset - auto stmt = diskCache->prepare( - "SELECT id, data_id FROM chunks WHERE url = ? AND offset = ?"); - if (!stmt) - return; - stmt->bindText(url.c_str()); - stmt->bindInt64(chunkIdx * DOWNLOAD_CHUNK_SIZE); - - const auto mainRet = stmt->execute(); - if (mainRet == SQLITE_ROW) { - const auto chunk_id = stmt->getInt64(); - const auto data_id = stmt->getInt64(); - stmt = - diskCache->prepare("UPDATE chunk_data SET data = ? WHERE id = ?"); - if (!stmt) - return; - stmt->bindBlob(blob.data(), blob.size()); - stmt->bindInt64(data_id); - { - const auto ret = stmt->execute(); - if (ret != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - } - - diskCache->move_to_head(chunk_id); - - return; - } else if (mainRet != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - - // Lambda to recycle an existing entry that was either invalidated, or - // least recently used. - const auto reuseExistingEntry = [ctx, &blob, &diskCache, hDB, &url, - chunkIdx, &dataPtr]( - std::unique_ptr<SQLiteStatement> &stmtIn) { - const auto chunk_id = stmtIn->getInt64(); - const auto data_id = stmtIn->getInt64(); - if (data_id <= 0) { - pj_log(ctx, PJ_LOG_ERROR, "data_id <= 0"); - return; - } - - auto l_stmt = - diskCache->prepare("UPDATE chunk_data SET data = ? WHERE id = ?"); - if (!l_stmt) - return; - l_stmt->bindBlob(blob.data(), blob.size()); - l_stmt->bindInt64(data_id); - { - const auto ret2 = l_stmt->execute(); - if (ret2 != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - } - - l_stmt = diskCache->prepare("UPDATE chunks SET url = ?, " - "offset = ?, data_size = ?, data_id = ? " - "WHERE id = ?"); - if (!l_stmt) - return; - l_stmt->bindText(url.c_str()); - l_stmt->bindInt64(chunkIdx * DOWNLOAD_CHUNK_SIZE); - l_stmt->bindInt64(dataPtr->size()); - l_stmt->bindInt64(data_id); - l_stmt->bindInt64(chunk_id); - { - const auto ret2 = l_stmt->execute(); - if (ret2 != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - } - - diskCache->move_to_head(chunk_id); - }; - - // Find if there is an invalidated chunk we can reuse - stmt = diskCache->prepare( - "SELECT id, data_id FROM chunks " - "WHERE id = (SELECT tail FROM linked_chunks_head_tail) AND " - "url = " INVALIDATED_SQL_LITERAL); - if (!stmt) - return; - { - const auto ret = stmt->execute(); - if (ret == SQLITE_ROW) { - reuseExistingEntry(stmt); - return; - } else if (ret != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - } - - // Check if we have not reached the max size of the cache - stmt = diskCache->prepare("SELECT COUNT(*) FROM chunks"); - if (!stmt) - return; - { - const auto ret = stmt->execute(); - if (ret != SQLITE_ROW) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - } - - const auto max_size = pj_context_get_grid_cache_max_size(ctx); - if (max_size > 0 && - static_cast<long long>(stmt->getInt64() * DOWNLOAD_CHUNK_SIZE) >= - max_size) { - stmt = diskCache->prepare( - "SELECT id, data_id FROM chunks " - "WHERE id = (SELECT tail FROM linked_chunks_head_tail)"); - if (!stmt) - return; - - const auto ret = stmt->execute(); - if (ret != SQLITE_ROW) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - reuseExistingEntry(stmt); - return; - } - - // Otherwise just append a new entry - stmt = diskCache->prepare("INSERT INTO chunk_data(data) VALUES (?)"); - if (!stmt) - return; - stmt->bindBlob(blob.data(), blob.size()); - { - const auto ret = stmt->execute(); - if (ret != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - } - - const auto chunk_data_id = sqlite3_last_insert_rowid(hDB); - - stmt = diskCache->prepare("INSERT INTO chunks(url, offset, data_id, " - "data_size) VALUES (?,?,?,?)"); - if (!stmt) - return; - stmt->bindText(url.c_str()); - stmt->bindInt64(chunkIdx * DOWNLOAD_CHUNK_SIZE); - stmt->bindInt64(chunk_data_id); - stmt->bindInt64(dataPtr->size()); - { - const auto ret = stmt->execute(); - if (ret != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - } - - const auto chunk_id = sqlite3_last_insert_rowid(hDB); - - stmt = diskCache->prepare( - "INSERT INTO linked_chunks(chunk_id, prev, next) VALUES (?,NULL,NULL)"); - if (!stmt) - return; - stmt->bindInt64(chunk_id); - if (stmt->execute() != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - - stmt = diskCache->prepare("SELECT head FROM linked_chunks_head_tail"); - if (!stmt) - return; - if (stmt->execute() != SQLITE_ROW) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - if (stmt->getInt64() == 0) { - stmt = diskCache->prepare( - "UPDATE linked_chunks_head_tail SET head = ?, tail = ?"); - if (!stmt) - return; - stmt->bindInt64(chunk_id); - stmt->bindInt64(chunk_id); - if (stmt->execute() != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - } - - diskCache->move_to_head(chunk_id); -} +FileLegacyAdapter::~FileLegacyAdapter() { pj_ctx_fclose(m_ctx, m_fp); } // --------------------------------------------------------------------------- -std::shared_ptr<std::vector<unsigned char>> -NetworkChunkCache::get(PJ_CONTEXT *ctx, const std::string &url, - unsigned long long chunkIdx) { - std::shared_ptr<std::vector<unsigned char>> ret; - if (cache_.tryGet(Key(url, chunkIdx), ret)) { - return ret; - } - - auto diskCache = DiskChunkCache::open(ctx); - if (!diskCache) - return ret; - auto hDB = diskCache->handle(); - - auto stmt = diskCache->prepare( - "SELECT chunks.id, chunks.data_size, chunk_data.data FROM chunks " - "JOIN chunk_data ON chunks.id = chunk_data.id " - "WHERE chunks.url = ? AND chunks.offset = ?"); - if (!stmt) - return ret; - - stmt->bindText(url.c_str()); - stmt->bindInt64(chunkIdx * DOWNLOAD_CHUNK_SIZE); - - const auto mainRet = stmt->execute(); - if (mainRet == SQLITE_ROW) { - const auto chunk_id = stmt->getInt64(); - const auto data_size = stmt->getInt64(); - int blob_size = 0; - const void *blob = stmt->getBlob(blob_size); - if (blob_size < data_size) { - pj_log(ctx, PJ_LOG_ERROR, - "blob_size=%d < data_size for chunk_id=%d", blob_size, - static_cast<int>(chunk_id)); - return ret; - } - if (data_size > static_cast<sqlite3_int64>(DOWNLOAD_CHUNK_SIZE)) { - pj_log(ctx, PJ_LOG_ERROR, "data_size > DOWNLOAD_CHUNK_SIZE"); - return ret; - } - ret.reset(new std::vector<unsigned char>()); - ret->assign(reinterpret_cast<const unsigned char *>(blob), - reinterpret_cast<const unsigned char *>(blob) + - static_cast<size_t>(data_size)); - cache_.insert(Key(url, chunkIdx), ret); - - if (!diskCache->move_to_head(chunk_id)) - return ret; - } else if (mainRet != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - } - - return ret; +size_t FileLegacyAdapter::read(void *buffer, size_t sizeBytes) { + return pj_ctx_fread(m_ctx, buffer, 1, sizeBytes, m_fp); } // --------------------------------------------------------------------------- -std::shared_ptr<std::vector<unsigned char>> -NetworkChunkCache::get(PJ_CONTEXT *ctx, const std::string &url, - unsigned long long chunkIdx, FileProperties &props) { - if (!gNetworkFileProperties.tryGet(ctx, url, props)) { - return nullptr; +bool FileLegacyAdapter::seek(unsigned long long offset, int whence) { + if (offset != static_cast<unsigned long long>(static_cast<long>(offset))) { + pj_log(m_ctx, PJ_LOG_ERROR, + "Attempt at seeking to a 64 bit offset. Not supported yet"); + return false; } - - return get(ctx, url, chunkIdx); -} - -// --------------------------------------------------------------------------- - -void NetworkChunkCache::clearMemoryCache() { cache_.clear(); } - -// --------------------------------------------------------------------------- - -void NetworkChunkCache::clearDiskChunkCache(PJ_CONTEXT *ctx) { - auto diskCache = DiskChunkCache::open(ctx); - if (!diskCache) - return; - diskCache->closeAndUnlink(); + return pj_ctx_fseek(m_ctx, m_fp, static_cast<long>(offset), whence) == 0; } // --------------------------------------------------------------------------- -void NetworkFilePropertiesCache::insert(PJ_CONTEXT *ctx, const std::string &url, - FileProperties &props) { - time(&props.lastChecked); - cache_.insert(url, props); - - auto diskCache = DiskChunkCache::open(ctx); - if (!diskCache) - return; - auto hDB = diskCache->handle(); - auto stmt = diskCache->prepare("SELECT fileSize, lastModified, etag " - "FROM properties WHERE url = ?"); - if (!stmt) - return; - stmt->bindText(url.c_str()); - if (stmt->execute() == SQLITE_ROW) { - FileProperties cachedProps; - cachedProps.size = stmt->getInt64(); - const char *lastModified = stmt->getText(); - cachedProps.lastModified = lastModified ? lastModified : std::string(); - const char *etag = stmt->getText(); - cachedProps.etag = etag ? etag : std::string(); - if (props.size != cachedProps.size || - props.lastModified != cachedProps.lastModified || - props.etag != cachedProps.etag) { - - // If cached properties don't match recent fresh ones, invalidate - // cached chunks - stmt = diskCache->prepare("SELECT id FROM chunks WHERE url = ?"); - if (!stmt) - return; - stmt->bindText(url.c_str()); - std::vector<sqlite3_int64> ids; - while (stmt->execute() == SQLITE_ROW) { - ids.emplace_back(stmt->getInt64()); - stmt->resetResIndex(); - } - - for (const auto id : ids) { - diskCache->move_to_tail(id); - } - - stmt = diskCache->prepare( - "UPDATE chunks SET url = " INVALIDATED_SQL_LITERAL ", " - "offset = -1, data_size = 0 WHERE url = ?"); - if (!stmt) - return; - stmt->bindText(url.c_str()); - if (stmt->execute() != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - } - - stmt = diskCache->prepare("UPDATE properties SET lastChecked = ?, " - "fileSize = ?, lastModified = ?, etag = ? " - "WHERE url = ?"); - if (!stmt) - return; - stmt->bindInt64(props.lastChecked); - stmt->bindInt64(props.size); - if (props.lastModified.empty()) - stmt->bindNull(); - else - stmt->bindText(props.lastModified.c_str()); - if (props.etag.empty()) - stmt->bindNull(); - else - stmt->bindText(props.etag.c_str()); - stmt->bindText(url.c_str()); - if (stmt->execute() != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - } else { - stmt = diskCache->prepare("INSERT INTO properties (url, lastChecked, " - "fileSize, lastModified, etag) VALUES " - "(?,?,?,?,?)"); - if (!stmt) - return; - stmt->bindText(url.c_str()); - stmt->bindInt64(props.lastChecked); - stmt->bindInt64(props.size); - if (props.lastModified.empty()) - stmt->bindNull(); - else - stmt->bindText(props.lastModified.c_str()); - if (props.etag.empty()) - stmt->bindNull(); - else - stmt->bindText(props.etag.c_str()); - if (stmt->execute() != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return; - } - } +unsigned long long FileLegacyAdapter::tell() { + return pj_ctx_ftell(m_ctx, m_fp); } // --------------------------------------------------------------------------- -bool NetworkFilePropertiesCache::tryGet(PJ_CONTEXT *ctx, const std::string &url, - FileProperties &props) { - if (cache_.tryGet(url, props)) { - return true; - } - - auto diskCache = DiskChunkCache::open(ctx); - if (!diskCache) - return false; - auto stmt = - diskCache->prepare("SELECT lastChecked, fileSize, lastModified, etag " - "FROM properties WHERE url = ?"); - if (!stmt) - return false; - stmt->bindText(url.c_str()); - if (stmt->execute() != SQLITE_ROW) { - return false; - } - props.lastChecked = stmt->getInt64(); - props.size = stmt->getInt64(); - const char *lastModified = stmt->getText(); - props.lastModified = lastModified ? lastModified : std::string(); - const char *etag = stmt->getText(); - props.etag = etag ? etag : std::string(); - - const auto ttl = pj_context_get_grid_cache_ttl(ctx); - if (ttl > 0) { - time_t curTime; - time(&curTime); - if (curTime > props.lastChecked + ttl) { - props = FileProperties(); - return false; - } - } - cache_.insert(url, props); - return true; +std::unique_ptr<File> +FileLegacyAdapter::open(PJ_CONTEXT *ctx, const char *filename, FileAccess) { + auto fid = pj_ctx_fopen(ctx, filename, "rb"); + return std::unique_ptr<File>(fid ? new FileLegacyAdapter(filename, ctx, fid) + : nullptr); } -// --------------------------------------------------------------------------- - -void NetworkFilePropertiesCache::clearMemoryCache() { cache_.clear(); } +#endif // REMOVE_LEGACY_SUPPORT // --------------------------------------------------------------------------- -class NetworkFile : public File { +class FileApiAdapter : public File { PJ_CONTEXT *m_ctx; - std::string m_url; - PROJ_NETWORK_HANDLE *m_handle; - unsigned long long m_pos = 0; - size_t m_nBlocksToDownload = 1; - unsigned long long m_lastDownloadedOffset; - FileProperties m_props; - proj_network_close_cbk_type m_closeCbk; - bool m_hasChanged = false; - - NetworkFile(const NetworkFile &) = delete; - NetworkFile &operator=(const NetworkFile &) = delete; + PROJ_FILE_HANDLE *m_fp; + + FileApiAdapter(const FileApiAdapter &) = delete; + FileApiAdapter &operator=(const FileApiAdapter &) = delete; protected: - NetworkFile(PJ_CONTEXT *ctx, const std::string &url, - PROJ_NETWORK_HANDLE *handle, - unsigned long long lastDownloadOffset, - const FileProperties &props) - : File(url), m_ctx(ctx), m_url(url), m_handle(handle), - m_lastDownloadedOffset(lastDownloadOffset), m_props(props), - m_closeCbk(ctx->networking.close) {} + FileApiAdapter(const std::string &name, PJ_CONTEXT *ctx, + PROJ_FILE_HANDLE *fp) + : File(name), m_ctx(ctx), m_fp(fp) {} public: - ~NetworkFile() override; + ~FileApiAdapter() override; size_t read(void *buffer, size_t sizeBytes) override; - bool seek(unsigned long long offset, int whence) override; + size_t write(const void *, size_t) override; + bool seek(unsigned long long offset, int whence = SEEK_SET) override; unsigned long long tell() override; - void reassign_context(PJ_CONTEXT *ctx) override; - bool hasChanged() const override { return m_hasChanged; } + void reassign_context(PJ_CONTEXT *ctx) override { m_ctx = ctx; } - static std::unique_ptr<File> open(PJ_CONTEXT *ctx, const char *filename); + // We may lie, but the real use case is only for network files + bool hasChanged() const override { return false; } - static bool get_props_from_headers(PJ_CONTEXT *ctx, - PROJ_NETWORK_HANDLE *handle, - FileProperties &props); + static std::unique_ptr<File> open(PJ_CONTEXT *ctx, const char *filename, + FileAccess access); }; // --------------------------------------------------------------------------- -bool NetworkFile::get_props_from_headers(PJ_CONTEXT *ctx, - PROJ_NETWORK_HANDLE *handle, - FileProperties &props) { - const char *contentRange = ctx->networking.get_header_value( - ctx, handle, "Content-Range", ctx->networking.user_data); - if (contentRange) { - const char *slash = strchr(contentRange, '/'); - if (slash) { - props.size = std::stoull(slash + 1); - - const char *lastModified = ctx->networking.get_header_value( - ctx, handle, "Last-Modified", ctx->networking.user_data); - if (lastModified) - props.lastModified = lastModified; - - const char *etag = ctx->networking.get_header_value( - ctx, handle, "ETag", ctx->networking.user_data); - if (etag) - props.etag = etag; - - return true; - } - } - return false; +FileApiAdapter::~FileApiAdapter() { + m_ctx->fileApi.close_cbk(m_ctx, m_fp, m_ctx->fileApi.user_data); } // --------------------------------------------------------------------------- -std::unique_ptr<File> NetworkFile::open(PJ_CONTEXT *ctx, const char *filename) { - FileProperties props; - if (gNetworkChunkCache.get(ctx, filename, 0, props)) { - return std::unique_ptr<File>(new NetworkFile( - ctx, filename, nullptr, - std::numeric_limits<unsigned long long>::max(), props)); - } else { - std::vector<unsigned char> buffer(DOWNLOAD_CHUNK_SIZE); - size_t size_read = 0; - std::string errorBuffer; - errorBuffer.resize(1024); - - auto handle = ctx->networking.open( - ctx, filename, 0, buffer.size(), &buffer[0], &size_read, - errorBuffer.size(), &errorBuffer[0], ctx->networking.user_data); - buffer.resize(size_read); - if (!handle) { - errorBuffer.resize(strlen(errorBuffer.data())); - pj_log(ctx, PJ_LOG_ERROR, "Cannot open %s: %s", filename, - errorBuffer.c_str()); - } - - bool ok = false; - if (handle) { - if (get_props_from_headers(ctx, handle, props)) { - ok = true; - gNetworkFileProperties.insert(ctx, filename, props); - gNetworkChunkCache.insert(ctx, filename, 0, std::move(buffer)); - } - } - - return std::unique_ptr<File>( - ok ? new NetworkFile(ctx, filename, handle, size_read, props) - : nullptr); - } +size_t FileApiAdapter::read(void *buffer, size_t sizeBytes) { + return m_ctx->fileApi.read_cbk(m_ctx, m_fp, buffer, sizeBytes, + m_ctx->fileApi.user_data); } // --------------------------------------------------------------------------- -size_t NetworkFile::read(void *buffer, size_t sizeBytes) { - - if (sizeBytes == 0) - return 0; - - auto iterOffset = m_pos; - while (sizeBytes) { - const auto chunkIdxToDownload = iterOffset / DOWNLOAD_CHUNK_SIZE; - const auto offsetToDownload = chunkIdxToDownload * DOWNLOAD_CHUNK_SIZE; - std::vector<unsigned char> region; - auto pChunk = gNetworkChunkCache.get(m_ctx, m_url, chunkIdxToDownload); - if (pChunk != nullptr) { - region = *pChunk; - } else { - if (offsetToDownload == m_lastDownloadedOffset) { - // In case of consecutive reads (of small size), we use a - // heuristic that we will read the file sequentially, so - // we double the requested size to decrease the number of - // client/server roundtrips. - if (m_nBlocksToDownload < 100) - m_nBlocksToDownload *= 2; - } else { - // Random reads. Cancel the above heuristics. - m_nBlocksToDownload = 1; - } - - // Ensure that we will request at least the number of blocks - // to satisfy the remaining buffer size to read. - const auto endOffsetToDownload = - ((iterOffset + sizeBytes + DOWNLOAD_CHUNK_SIZE - 1) / - DOWNLOAD_CHUNK_SIZE) * - DOWNLOAD_CHUNK_SIZE; - const auto nMinBlocksToDownload = static_cast<size_t>( - (endOffsetToDownload - offsetToDownload) / DOWNLOAD_CHUNK_SIZE); - if (m_nBlocksToDownload < nMinBlocksToDownload) - m_nBlocksToDownload = nMinBlocksToDownload; - - // Avoid reading already cached data. - // Note: this might get evicted if concurrent reads are done, but - // this should not cause bugs. Just missed optimization. - for (size_t i = 1; i < m_nBlocksToDownload; i++) { - if (gNetworkChunkCache.get(m_ctx, m_url, - chunkIdxToDownload + i) != nullptr) { - m_nBlocksToDownload = i; - break; - } - } - - if (m_nBlocksToDownload > MAX_CHUNKS) - m_nBlocksToDownload = MAX_CHUNKS; - - region.resize(m_nBlocksToDownload * DOWNLOAD_CHUNK_SIZE); - size_t nRead = 0; - std::string errorBuffer; - errorBuffer.resize(1024); - if (!m_handle) { - m_handle = m_ctx->networking.open( - m_ctx, m_url.c_str(), offsetToDownload, - m_nBlocksToDownload * DOWNLOAD_CHUNK_SIZE, ®ion[0], - &nRead, errorBuffer.size(), &errorBuffer[0], - m_ctx->networking.user_data); - if (!m_handle) { - return 0; - } - } else { - nRead = m_ctx->networking.read_range( - m_ctx, m_handle, offsetToDownload, - m_nBlocksToDownload * DOWNLOAD_CHUNK_SIZE, ®ion[0], - errorBuffer.size(), &errorBuffer[0], - m_ctx->networking.user_data); - } - if (nRead == 0) { - errorBuffer.resize(strlen(errorBuffer.data())); - if (!errorBuffer.empty()) { - pj_log(m_ctx, PJ_LOG_ERROR, "Cannot read in %s: %s", - m_url.c_str(), errorBuffer.c_str()); - } - return 0; - } - - if (!m_hasChanged) { - FileProperties props; - if (get_props_from_headers(m_ctx, m_handle, props)) { - if (props.size != m_props.size || - props.lastModified != m_props.lastModified || - props.etag != m_props.etag) { - gNetworkFileProperties.insert(m_ctx, m_url, props); - gNetworkChunkCache.clearMemoryCache(); - m_hasChanged = true; - } - } - } - - region.resize(nRead); - m_lastDownloadedOffset = offsetToDownload + nRead; - - const auto nChunks = - (region.size() + DOWNLOAD_CHUNK_SIZE - 1) / DOWNLOAD_CHUNK_SIZE; - for (size_t i = 0; i < nChunks; i++) { - std::vector<unsigned char> chunk( - region.data() + i * DOWNLOAD_CHUNK_SIZE, - region.data() + - std::min((i + 1) * DOWNLOAD_CHUNK_SIZE, region.size())); - gNetworkChunkCache.insert(m_ctx, m_url, chunkIdxToDownload + i, - std::move(chunk)); - } - } - const size_t nToCopy = static_cast<size_t>( - std::min(static_cast<unsigned long long>(sizeBytes), - region.size() - (iterOffset - offsetToDownload))); - memcpy(buffer, region.data() + iterOffset - offsetToDownload, nToCopy); - buffer = static_cast<char *>(buffer) + nToCopy; - iterOffset += nToCopy; - sizeBytes -= nToCopy; - if (region.size() < static_cast<size_t>(DOWNLOAD_CHUNK_SIZE) && - sizeBytes != 0) { - break; - } - } - - size_t nRead = static_cast<size_t>(iterOffset - m_pos); - m_pos = iterOffset; - return nRead; +size_t FileApiAdapter::write(const void *buffer, size_t sizeBytes) { + return m_ctx->fileApi.write_cbk(m_ctx, m_fp, buffer, sizeBytes, + m_ctx->fileApi.user_data); } // --------------------------------------------------------------------------- -bool NetworkFile::seek(unsigned long long offset, int whence) { - if (whence == SEEK_SET) { - m_pos = offset; - } else if (whence == SEEK_CUR) { - m_pos += offset; - } else { - if (offset != 0) - return false; - m_pos = m_props.size; - } - return true; +bool FileApiAdapter::seek(unsigned long long offset, int whence) { + return m_ctx->fileApi.seek_cbk(m_ctx, m_fp, static_cast<long long>(offset), + whence, m_ctx->fileApi.user_data) != 0; } // --------------------------------------------------------------------------- -unsigned long long NetworkFile::tell() { return m_pos; } - -// --------------------------------------------------------------------------- - -NetworkFile::~NetworkFile() { - if (m_handle) { - m_ctx->networking.close(m_ctx, m_handle, m_ctx->networking.user_data); - } +unsigned long long FileApiAdapter::tell() { + return m_ctx->fileApi.tell_cbk(m_ctx, m_fp, m_ctx->fileApi.user_data); } // --------------------------------------------------------------------------- -void NetworkFile::reassign_context(PJ_CONTEXT *ctx) { - m_ctx = ctx; - if (m_closeCbk != m_ctx->networking.close) { - pj_log(m_ctx, PJ_LOG_ERROR, - "Networking close callback has changed following context " - "reassignment ! This is highly suspicious"); +std::unique_ptr<File> FileApiAdapter::open(PJ_CONTEXT *ctx, + const char *filename, + FileAccess eAccess) { + PROJ_OPEN_ACCESS eCAccess = PROJ_OPEN_ACCESS_READ_ONLY; + switch (eAccess) { + case FileAccess::READ_ONLY: + // Initialized above + break; + case FileAccess::READ_UPDATE: + eCAccess = PROJ_OPEN_ACCESS_READ_UPDATE; + break; + case FileAccess::CREATE: + eCAccess = PROJ_OPEN_ACCESS_CREATE; + break; } + auto fp = + ctx->fileApi.open_cbk(ctx, filename, eCAccess, ctx->fileApi.user_data); + return std::unique_ptr<File>(fp ? new FileApiAdapter(filename, ctx, fp) + : nullptr); } // --------------------------------------------------------------------------- -std::unique_ptr<File> FileManager::open(PJ_CONTEXT *ctx, const char *filename) { -#ifndef REMOVE_LEGACY_SUPPORT - // If the user has specified a legacy fileapi, use it - if (ctx->fileapi_legacy != pj_get_default_fileapi()) { - return FileLegacyAdapter::open(ctx, filename); - } -#endif +std::unique_ptr<File> FileManager::open(PJ_CONTEXT *ctx, const char *filename, + FileAccess access) { if (starts_with(filename, "http://") || starts_with(filename, "https://")) { if (!pj_context_is_network_enabled(ctx)) { pj_log( @@ -1632,671 +888,232 @@ std::unique_ptr<File> FileManager::open(PJ_CONTEXT *ctx, const char *filename) { "proj_context_set_enable_network(ctx, TRUE)"); return nullptr; } - return NetworkFile::open(ctx, filename); - } - return FileStdio::open(ctx, filename); -} - -// --------------------------------------------------------------------------- - -#ifdef CURL_ENABLED - -struct CurlFileHandle { - std::string m_url; - CURL *m_handle; - std::string m_headers{}; - std::string m_lastval{}; - std::string m_useragent{}; - char m_szCurlErrBuf[CURL_ERROR_SIZE + 1] = {}; - - CurlFileHandle(const CurlFileHandle &) = delete; - CurlFileHandle &operator=(const CurlFileHandle &) = delete; - - explicit CurlFileHandle(const char *url, CURL *handle); - ~CurlFileHandle(); - - static PROJ_NETWORK_HANDLE * - open(PJ_CONTEXT *, const char *url, unsigned long long offset, - size_t size_to_read, void *buffer, size_t *out_size_read, - size_t error_string_max_size, char *out_error_string, void *); -}; - -// --------------------------------------------------------------------------- - -static std::string GetExecutableName() { -#if defined(__linux) - std::string path; - path.resize(1024); - const auto ret = readlink("/proc/self/exe", &path[0], path.size()); - if (ret > 0) { - path.resize(ret); - const auto pos = path.rfind('/'); - if (pos != std::string::npos) { - path = path.substr(pos + 1); - } - return path; - } -#elif defined(_WIN32) - std::string path; - path.resize(1024); - if (GetModuleFileNameA(nullptr, &path[0], - static_cast<DWORD>(path.size()))) { - path.resize(strlen(path.c_str())); - const auto pos = path.rfind('\\'); - if (pos != std::string::npos) { - path = path.substr(pos + 1); - } - return path; + return pj_network_file_open(ctx, filename); } -#elif defined(__MACH__) && defined(__APPLE__) - std::string path; - path.resize(1024); - uint32_t size = static_cast<uint32_t>(path.size()); - if (_NSGetExecutablePath(&path[0], &size) == 0) { - path.resize(strlen(path.c_str())); - const auto pos = path.rfind('/'); - if (pos != std::string::npos) { - path = path.substr(pos + 1); - } - return path; +#ifndef REMOVE_LEGACY_SUPPORT + // If the user has specified a legacy fileapi, use it + if (ctx->fileapi_legacy != pj_get_default_fileapi()) { + return FileLegacyAdapter::open(ctx, filename, access); } -#elif defined(__FreeBSD__) - int mib[4]; - mib[0] = CTL_KERN; - mib[1] = KERN_PROC; - mib[2] = KERN_PROC_PATHNAME; - mib[3] = -1; - std::string path; - path.resize(1024); - size_t size = path.size(); - if (sysctl(mib, 4, &path[0], &size, nullptr, 0) == 0) { - path.resize(strlen(path.c_str())); - const auto pos = path.rfind('/'); - if (pos != std::string::npos) { - path = path.substr(pos + 1); - } - return path; +#endif + if (ctx->fileApi.open_cbk != nullptr) { + return FileApiAdapter::open(ctx, filename, access); } +#ifdef _WIN32 + return FileWin32::open(ctx, filename, access); +#else + return FileStdio::open(ctx, filename, access); #endif - - return std::string(); } // --------------------------------------------------------------------------- -CurlFileHandle::CurlFileHandle(const char *url, CURL *handle) - : m_url(url), m_handle(handle) { - curl_easy_setopt(handle, CURLOPT_URL, m_url.c_str()); - - if (getenv("PROJ_CURL_VERBOSE")) - curl_easy_setopt(handle, CURLOPT_VERBOSE, 1); - -// CURLOPT_SUPPRESS_CONNECT_HEADERS is defined in curl 7.54.0 or newer. -#if LIBCURL_VERSION_NUM >= 0x073600 - curl_easy_setopt(handle, CURLOPT_SUPPRESS_CONNECT_HEADERS, 1L); -#endif - - // Enable following redirections. Requires libcurl 7.10.1 at least. - curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1); - curl_easy_setopt(handle, CURLOPT_MAXREDIRS, 10); - - if (getenv("PROJ_UNSAFE_SSL")) { - curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 0L); +bool FileManager::exists(PJ_CONTEXT *ctx, const char *filename) { + if (ctx->fileApi.exists_cbk) { + return ctx->fileApi.exists_cbk(ctx, filename, ctx->fileApi.user_data) != + 0; } - curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, m_szCurlErrBuf); - - if (getenv("PROJ_NO_USERAGENT") == nullptr) { - m_useragent = "PROJ " STR(PROJ_VERSION_MAJOR) "." STR( - PROJ_VERSION_MINOR) "." STR(PROJ_VERSION_PATCH); - const auto exeName = GetExecutableName(); - if (!exeName.empty()) { - m_useragent = exeName + " using " + m_useragent; - } - curl_easy_setopt(handle, CURLOPT_USERAGENT, m_useragent.data()); +#ifdef _WIN32 + struct __stat64 buf; + try { + return _wstat64(UTF8ToWString(filename).c_str(), &buf) == 0; + } catch (const std::exception &e) { + pj_log(ctx, PJ_LOG_DEBUG, "%s", e.what()); + return false; } +#else + (void)ctx; + struct stat sStat; + return stat(filename, &sStat) == 0; +#endif } // --------------------------------------------------------------------------- -CurlFileHandle::~CurlFileHandle() { curl_easy_cleanup(m_handle); } - -// --------------------------------------------------------------------------- - -static size_t pj_curl_write_func(void *buffer, size_t count, size_t nmemb, - void *req) { - const size_t nSize = count * nmemb; - auto pStr = static_cast<std::string *>(req); - if (pStr->size() + nSize > pStr->capacity()) { - // to avoid servers not honouring Range to cause excessive memory - // allocation - return 0; +bool FileManager::mkdir(PJ_CONTEXT *ctx, const char *filename) { + if (ctx->fileApi.mkdir_cbk) { + return ctx->fileApi.mkdir_cbk(ctx, filename, ctx->fileApi.user_data) != + 0; } - pStr->append(static_cast<const char *>(buffer), nSize); - return nmemb; -} -// --------------------------------------------------------------------------- - -static double GetNewRetryDelay(int response_code, double dfOldDelay, - const char *pszErrBuf, - const char *pszCurlError) { - if (response_code == 429 || response_code == 500 || - (response_code >= 502 && response_code <= 504) || - // S3 sends some client timeout errors as 400 Client Error - (response_code == 400 && pszErrBuf && - strstr(pszErrBuf, "RequestTimeout")) || - (pszCurlError && strstr(pszCurlError, "Connection timed out"))) { - // Use an exponential backoff factor of 2 plus some random jitter - // We don't care about cryptographic quality randomness, hence: - // coverity[dont_call] - return dfOldDelay * (2 + rand() * 0.5 / RAND_MAX); - } else { - return 0; +#ifdef _WIN32 + try { + return _wmkdir(UTF8ToWString(filename).c_str()) == 0; + } catch (const std::exception &e) { + pj_log(ctx, PJ_LOG_DEBUG, "%s", e.what()); + return false; } +#else + (void)ctx; + return ::mkdir(filename, 0755) == 0; +#endif } // --------------------------------------------------------------------------- -constexpr double MIN_RETRY_DELAY_MS = 500; -constexpr double MAX_RETRY_DELAY_MS = 60000; - -PROJ_NETWORK_HANDLE *CurlFileHandle::open(PJ_CONTEXT *ctx, const char *url, - unsigned long long offset, - size_t size_to_read, void *buffer, - size_t *out_size_read, - size_t error_string_max_size, - char *out_error_string, void *) { - CURL *hCurlHandle = curl_easy_init(); - if (!hCurlHandle) - return nullptr; - - auto file = - std::unique_ptr<CurlFileHandle>(new CurlFileHandle(url, hCurlHandle)); - - double oldDelay = MIN_RETRY_DELAY_MS; - std::string headers; - std::string body; - - char szBuffer[128]; - sqlite3_snprintf(sizeof(szBuffer), szBuffer, "%llu-%llu", offset, - offset + size_to_read - 1); - - while (true) { - curl_easy_setopt(hCurlHandle, CURLOPT_RANGE, szBuffer); - - headers.clear(); - headers.reserve(16 * 1024); - curl_easy_setopt(hCurlHandle, CURLOPT_HEADERDATA, &headers); - curl_easy_setopt(hCurlHandle, CURLOPT_HEADERFUNCTION, - pj_curl_write_func); - - body.clear(); - body.reserve(size_to_read); - curl_easy_setopt(hCurlHandle, CURLOPT_WRITEDATA, &body); - curl_easy_setopt(hCurlHandle, CURLOPT_WRITEFUNCTION, - pj_curl_write_func); - - file->m_szCurlErrBuf[0] = '\0'; - - curl_easy_perform(hCurlHandle); - - long response_code = 0; - curl_easy_getinfo(hCurlHandle, CURLINFO_HTTP_CODE, &response_code); - - curl_easy_setopt(hCurlHandle, CURLOPT_HEADERDATA, nullptr); - curl_easy_setopt(hCurlHandle, CURLOPT_HEADERFUNCTION, nullptr); - - curl_easy_setopt(hCurlHandle, CURLOPT_WRITEDATA, nullptr); - curl_easy_setopt(hCurlHandle, CURLOPT_WRITEFUNCTION, nullptr); - - if (response_code == 0 || response_code >= 300) { - const double delay = - GetNewRetryDelay(static_cast<int>(response_code), oldDelay, - body.c_str(), file->m_szCurlErrBuf); - if (delay != 0 && delay < MAX_RETRY_DELAY_MS) { - pj_log(ctx, PJ_LOG_TRACE, - "Got a HTTP %ld error. Retrying in %d ms", response_code, - static_cast<int>(delay)); - proj_sleep_ms(static_cast<int>(delay)); - oldDelay = delay; - } else { - if (out_error_string) { - if (file->m_szCurlErrBuf[0]) { - snprintf(out_error_string, error_string_max_size, "%s", - file->m_szCurlErrBuf); - } else { - snprintf(out_error_string, error_string_max_size, - "HTTP error %ld: %s", response_code, - body.c_str()); - } - } - return nullptr; - } - } else { - break; - } - } - - if (out_error_string && error_string_max_size) { - out_error_string[0] = '\0'; +bool FileManager::unlink(PJ_CONTEXT *ctx, const char *filename) { + if (ctx->fileApi.unlink_cbk) { + return ctx->fileApi.unlink_cbk(ctx, filename, ctx->fileApi.user_data) != + 0; } - if (!body.empty()) { - memcpy(buffer, body.data(), std::min(size_to_read, body.size())); +#ifdef _WIN32 + try { + return _wunlink(UTF8ToWString(filename).c_str()) == 0; + } catch (const std::exception &e) { + pj_log(ctx, PJ_LOG_DEBUG, "%s", e.what()); + return false; } - *out_size_read = std::min(size_to_read, body.size()); - - file->m_headers = std::move(headers); - return reinterpret_cast<PROJ_NETWORK_HANDLE *>(file.release()); -} - -// --------------------------------------------------------------------------- - -static void pj_curl_close(PJ_CONTEXT *, PROJ_NETWORK_HANDLE *handle, - void * /*user_data*/) { - delete reinterpret_cast<CurlFileHandle *>(handle); +#else + (void)ctx; + return ::unlink(filename) == 0; +#endif } // --------------------------------------------------------------------------- -static size_t pj_curl_read_range(PJ_CONTEXT *ctx, - PROJ_NETWORK_HANDLE *raw_handle, - unsigned long long offset, size_t size_to_read, - void *buffer, size_t error_string_max_size, - char *out_error_string, void *) { - auto handle = reinterpret_cast<CurlFileHandle *>(raw_handle); - auto hCurlHandle = handle->m_handle; - - double oldDelay = MIN_RETRY_DELAY_MS; - std::string headers; - std::string body; - - char szBuffer[128]; - sqlite3_snprintf(sizeof(szBuffer), szBuffer, "%llu-%llu", offset, - offset + size_to_read - 1); - - while (true) { - curl_easy_setopt(hCurlHandle, CURLOPT_RANGE, szBuffer); - - headers.clear(); - headers.reserve(16 * 1024); - curl_easy_setopt(hCurlHandle, CURLOPT_HEADERDATA, &headers); - curl_easy_setopt(hCurlHandle, CURLOPT_HEADERFUNCTION, - pj_curl_write_func); - - body.clear(); - body.reserve(size_to_read); - curl_easy_setopt(hCurlHandle, CURLOPT_WRITEDATA, &body); - curl_easy_setopt(hCurlHandle, CURLOPT_WRITEFUNCTION, - pj_curl_write_func); - - handle->m_szCurlErrBuf[0] = '\0'; - - curl_easy_perform(hCurlHandle); - - long response_code = 0; - curl_easy_getinfo(hCurlHandle, CURLINFO_HTTP_CODE, &response_code); - - curl_easy_setopt(hCurlHandle, CURLOPT_WRITEDATA, nullptr); - curl_easy_setopt(hCurlHandle, CURLOPT_WRITEFUNCTION, nullptr); - - if (response_code == 0 || response_code >= 300) { - const double delay = - GetNewRetryDelay(static_cast<int>(response_code), oldDelay, - body.c_str(), handle->m_szCurlErrBuf); - if (delay != 0 && delay < MAX_RETRY_DELAY_MS) { - pj_log(ctx, PJ_LOG_TRACE, - "Got a HTTP %ld error. Retrying in %d ms", response_code, - static_cast<int>(delay)); - proj_sleep_ms(static_cast<int>(delay)); - oldDelay = delay; - } else { - if (out_error_string) { - if (handle->m_szCurlErrBuf[0]) { - snprintf(out_error_string, error_string_max_size, "%s", - handle->m_szCurlErrBuf); - } else { - snprintf(out_error_string, error_string_max_size, - "HTTP error %ld: %s", response_code, - body.c_str()); - } - } - return 0; - } - } else { - break; - } - } - if (out_error_string && error_string_max_size) { - out_error_string[0] = '\0'; - } - - if (!body.empty()) { - memcpy(buffer, body.data(), std::min(size_to_read, body.size())); +bool FileManager::rename(PJ_CONTEXT *ctx, const char *oldPath, + const char *newPath) { + if (ctx->fileApi.rename_cbk) { + return ctx->fileApi.rename_cbk(ctx, oldPath, newPath, + ctx->fileApi.user_data) != 0; } - handle->m_headers = std::move(headers); - return std::min(size_to_read, body.size()); -} - -// --------------------------------------------------------------------------- - -static const char *pj_curl_get_header_value(PJ_CONTEXT *, - PROJ_NETWORK_HANDLE *raw_handle, - const char *header_name, void *) { - auto handle = reinterpret_cast<CurlFileHandle *>(raw_handle); - auto pos = ci_find(handle->m_headers, header_name); - if (pos == std::string::npos) - return nullptr; - pos += strlen(header_name); - const char *c_str = handle->m_headers.c_str(); - if (c_str[pos] == ':') - pos++; - while (c_str[pos] == ' ') - pos++; - auto posEnd = pos; - while (c_str[posEnd] != '\r' && c_str[posEnd] != '\n' && - c_str[posEnd] != '\0') - posEnd++; - handle->m_lastval = handle->m_headers.substr(pos, posEnd - pos); - return handle->m_lastval.c_str(); -} - -#else - -// --------------------------------------------------------------------------- - -static PROJ_NETWORK_HANDLE * -no_op_network_open(PJ_CONTEXT *, const char * /* url */, - unsigned long long, /* offset */ - size_t, /* size to read */ - void *, /* buffer to update with bytes read*/ - size_t *, /* output: size actually read */ - size_t error_string_max_size, char *out_error_string, - void * /*user_data*/) { - if (out_error_string) { - snprintf(out_error_string, error_string_max_size, "%s", - "Network functionality not available"); +#ifdef _WIN32 + try { + return _wrename(UTF8ToWString(oldPath).c_str(), + UTF8ToWString(newPath).c_str()) == 0; + } catch (const std::exception &e) { + pj_log(ctx, PJ_LOG_DEBUG, "%s", e.what()); + return false; } - return nullptr; -} - -// --------------------------------------------------------------------------- - -static void no_op_network_close(PJ_CONTEXT *, PROJ_NETWORK_HANDLE *, - void * /*user_data*/) {} - -#endif - -// --------------------------------------------------------------------------- - -void FileManager::fillDefaultNetworkInterface(PJ_CONTEXT *ctx) { -#ifdef CURL_ENABLED - ctx->networking.open = CurlFileHandle::open; - ctx->networking.close = pj_curl_close; - ctx->networking.read_range = pj_curl_read_range; - ctx->networking.get_header_value = pj_curl_get_header_value; #else - ctx->networking.open = no_op_network_open; - ctx->networking.close = no_op_network_close; + (void)ctx; + return ::rename(oldPath, newPath) == 0; #endif } // --------------------------------------------------------------------------- -void FileManager::clearMemoryCache() { - gNetworkChunkCache.clearMemoryCache(); - gNetworkFileProperties.clearMemoryCache(); +std::string FileManager::getProjLibEnvVar(PJ_CONTEXT *ctx) { + if (!ctx->env_var_proj_lib.empty()) { + return ctx->env_var_proj_lib; + } + (void)ctx; + std::string str; + const char *envvar = getenv("PROJ_LIB"); + if (!envvar) + return str; + str = envvar; +#ifdef _WIN32 + // Assume this is UTF-8. If not try to convert from ANSI page + bool looksLikeUTF8 = false; + try { + UTF8ToWString(envvar); + looksLikeUTF8 = true; + } catch (const std::exception &) { + } + if (!looksLikeUTF8 || !exists(ctx, envvar)) { + str = Win32Recode(envvar, CP_ACP, CP_UTF8); + if (str.empty() || !exists(ctx, str.c_str())) + str = envvar; + } +#endif + ctx->env_var_proj_lib = str; + return str; } -// --------------------------------------------------------------------------- - NS_PROJ_END //! @endcond // --------------------------------------------------------------------------- -/** Define a custom set of callbacks for network access. +/** Set a file API + * + * All callbacks should be provided (non NULL pointers). If read-only usage + * is intended, then the callbacks might have a dummy implementation. * - * All callbacks should be provided (non NULL pointers). + * \note Those callbacks will not be used for SQLite3 database access. If + * custom I/O is desired for that, then proj_context_set_sqlite3_vfs_name() + * should be used. * * @param ctx PROJ context, or NULL - * @param open_cbk Callback to open a remote file given its URL - * @param close_cbk Callback to close a remote file. - * @param get_header_value_cbk Callback to get HTTP headers - * @param read_range_cbk Callback to read a range of bytes inside a remote file. + * @param fileapi Pointer to file API structure (content will be copied). * @param user_data Arbitrary pointer provided by the user, and passed to the * above callbacks. May be NULL. * @return TRUE in case of success. + * @since 7.0 */ -int proj_context_set_network_callbacks( - PJ_CONTEXT *ctx, proj_network_open_cbk_type open_cbk, - proj_network_close_cbk_type close_cbk, - proj_network_get_header_value_cbk_type get_header_value_cbk, - proj_network_read_range_type read_range_cbk, void *user_data) { +int proj_context_set_fileapi(PJ_CONTEXT *ctx, const PROJ_FILE_API *fileapi, + void *user_data) { if (ctx == nullptr) { ctx = pj_get_default_ctx(); } - if (!open_cbk || !close_cbk || !get_header_value_cbk || !read_range_cbk) { + if (!fileapi) { return false; } - ctx->networking.open = open_cbk; - ctx->networking.close = close_cbk; - ctx->networking.get_header_value = get_header_value_cbk; - ctx->networking.read_range = read_range_cbk; - ctx->networking.user_data = user_data; - return true; -} - -// --------------------------------------------------------------------------- - -/** Enable or disable network access. -* -* This overrides the default endpoint in the PROJ configuration file or with -* the PROJ_NETWORK environment variable. -* -* @param ctx PROJ context, or NULL -* @param enable TRUE if network access is allowed. -* @return TRUE if network access is possible. That is either libcurl is -* available, or an alternate interface has been set. -*/ -int proj_context_set_enable_network(PJ_CONTEXT *ctx, int enable) { - if (ctx == nullptr) { - ctx = pj_get_default_ctx(); - } - // Load ini file, now so as to override its network settings - pj_load_ini(ctx); - ctx->networking.enabled_env_variable_checked = true; - ctx->networking.enabled = enable != FALSE; -#ifdef CURL_ENABLED - return ctx->networking.enabled; -#else - return ctx->networking.enabled && - ctx->networking.open != NS_PROJ::no_op_network_open; -#endif -} - -// --------------------------------------------------------------------------- - -/** Define the URL endpoint to query for remote grids. -* -* This overrides the default endpoint in the PROJ configuration file or with -* the PROJ_NETWORK_ENDPOINT environment variable. -* -* @param ctx PROJ context, or NULL -* @param url Endpoint URL. Must NOT be NULL. -*/ -void proj_context_set_url_endpoint(PJ_CONTEXT *ctx, const char *url) { - if (ctx == nullptr) { - ctx = pj_get_default_ctx(); - } - // Load ini file, now so as to override its network settings - pj_load_ini(ctx); - ctx->endpoint = url; -} - -// --------------------------------------------------------------------------- - -/** Enable or disable the local cache of grid chunks -* -* This overrides the setting in the PROJ configuration file. -* -* @param ctx PROJ context, or NULL -* @param enabled TRUE if the cache is enabled. -*/ -void proj_grid_cache_set_enable(PJ_CONTEXT *ctx, int enabled) { - if (ctx == nullptr) { - ctx = pj_get_default_ctx(); - } - // Load ini file, now so as to override its settings - pj_load_ini(ctx); - ctx->gridChunkCache.enabled = enabled != FALSE; -} - -// --------------------------------------------------------------------------- - -/** Override, for the considered context, the path and file of the local -* cache of grid chunks. -* -* @param ctx PROJ context, or NULL -* @param fullname Full name to the cache (encoded in UTF-8). If set to NULL, -* caching will be disabled. -*/ -void proj_grid_cache_set_filename(PJ_CONTEXT *ctx, const char *fullname) { - if (ctx == nullptr) { - ctx = pj_get_default_ctx(); - } - // Load ini file, now so as to override its settings - pj_load_ini(ctx); - ctx->gridChunkCache.filename = fullname ? fullname : std::string(); -} - -// --------------------------------------------------------------------------- - -/** Override, for the considered context, the maximum size of the local -* cache of grid chunks. -* -* @param ctx PROJ context, or NULL -* @param max_size_MB Maximum size, in mega-bytes (1024*1024 bytes), or -* negative value to set unlimited size. -*/ -void proj_grid_cache_set_max_size(PJ_CONTEXT *ctx, int max_size_MB) { - if (ctx == nullptr) { - ctx = pj_get_default_ctx(); - } - // Load ini file, now so as to override its settings - pj_load_ini(ctx); - ctx->gridChunkCache.max_size = - max_size_MB < 0 ? -1 - : static_cast<long long>(max_size_MB) * 1024 * 1024; - if (max_size_MB == 0) { - // For debug purposes only - const char *env_var = getenv("PROJ_GRID_CACHE_MAX_SIZE_BYTES"); - if (env_var && env_var[0] != '\0') { - ctx->gridChunkCache.max_size = atoi(env_var); - } + if (fileapi->version != 1) { + return false; } -} - -// --------------------------------------------------------------------------- - -/** Override, for the considered context, the time-to-live delay for -* re-checking if the cached properties of files are still up-to-date. -* -* @param ctx PROJ context, or NULL -* @param ttl_seconds Delay in seconds. Use negative value for no expiration. -*/ -void proj_grid_cache_set_ttl(PJ_CONTEXT *ctx, int ttl_seconds) { - if (ctx == nullptr) { - ctx = pj_get_default_ctx(); + if (!fileapi->open_cbk || !fileapi->close_cbk || !fileapi->read_cbk || + !fileapi->write_cbk || !fileapi->seek_cbk || !fileapi->tell_cbk || + !fileapi->exists_cbk || !fileapi->mkdir_cbk || !fileapi->unlink_cbk || + !fileapi->rename_cbk) { + return false; } - // Load ini file, now so as to override its settings - pj_load_ini(ctx); - ctx->gridChunkCache.ttl = ttl_seconds; + ctx->fileApi.open_cbk = fileapi->open_cbk; + ctx->fileApi.close_cbk = fileapi->close_cbk; + ctx->fileApi.read_cbk = fileapi->read_cbk; + ctx->fileApi.write_cbk = fileapi->write_cbk; + ctx->fileApi.seek_cbk = fileapi->seek_cbk; + ctx->fileApi.tell_cbk = fileapi->tell_cbk; + ctx->fileApi.exists_cbk = fileapi->exists_cbk; + ctx->fileApi.mkdir_cbk = fileapi->mkdir_cbk; + ctx->fileApi.unlink_cbk = fileapi->unlink_cbk; + ctx->fileApi.rename_cbk = fileapi->rename_cbk; + ctx->fileApi.user_data = user_data; + return true; } // --------------------------------------------------------------------------- -/** Clear the local cache of grid chunks. -* -* @param ctx PROJ context, or NULL -*/ -void proj_grid_cache_clear(PJ_CONTEXT *ctx) { +/** Set the name of a custom SQLite3 VFS. + * + * This should be a valid SQLite3 VFS name, such as the one passed to the + * sqlite3_vfs_register(). See https://www.sqlite.org/vfs.html + * + * It will be used to read proj.db or create&access the cache.db file in the + * PROJ user writable directory. + * + * @param ctx PROJ context, or NULL + * @param name SQLite3 VFS name. If NULL is passed, default implementation by + * SQLite will be used. + * @since 7.0 + */ +void proj_context_set_sqlite3_vfs_name(PJ_CONTEXT *ctx, const char *name) { if (ctx == nullptr) { ctx = pj_get_default_ctx(); } - NS_PROJ::gNetworkChunkCache.clearDiskChunkCache(ctx); + ctx->custom_sqlite3_vfs_name = name ? name : std::string(); } // --------------------------------------------------------------------------- //! @cond Doxygen_Suppress -bool pj_context_is_network_enabled(PJ_CONTEXT *ctx) { - if (ctx == nullptr) { - ctx = pj_get_default_ctx(); - } - if (ctx->networking.enabled_env_variable_checked) { - return ctx->networking.enabled; - } - const char *enabled = getenv("PROJ_NETWORK"); - if (enabled && enabled[0] != '\0') { - ctx->networking.enabled = ci_equal(enabled, "ON") || - ci_equal(enabled, "YES") || - ci_equal(enabled, "TRUE"); - } - pj_load_ini(ctx); - ctx->networking.enabled_env_variable_checked = true; - return ctx->networking.enabled; -} - // --------------------------------------------------------------------------- -#ifdef _WIN32 - -static std::wstring UTF8ToWString(const std::string &str) { - using convert_typeX = std::codecvt_utf8<wchar_t>; - std::wstring_convert<convert_typeX, wchar_t> converterX; - - return converterX.from_bytes(str); -} - -// --------------------------------------------------------------------------- - -static std::string WStringToUTF8(const std::wstring &wstr) { - using convert_typeX = std::codecvt_utf8<wchar_t>; - std::wstring_convert<convert_typeX, wchar_t> converterX; - - return converterX.to_bytes(wstr); -} -#endif - -// --------------------------------------------------------------------------- - -static void CreateDirectory(const std::string &path) { -#ifdef _WIN32 - struct __stat64 buf; - const auto wpath = UTF8ToWString(path); - if (_wstat64(wpath.c_str(), &buf) == 0) - return; - auto pos = path.find_last_of("/\\"); - if (pos == 0 || pos == std::string::npos) - return; - CreateDirectory(path.substr(0, pos)); - _wmkdir(wpath.c_str()); -#else - struct stat buf; - if (stat(path.c_str(), &buf) == 0) +static void CreateDirectoryRecursively(PJ_CONTEXT *ctx, + const std::string &path) { + if (NS_PROJ::FileManager::exists(ctx, path.c_str())) return; auto pos = path.find_last_of("/\\"); if (pos == 0 || pos == std::string::npos) return; - CreateDirectory(path.substr(0, pos)); - mkdir(path.c_str(), 0755); -#endif + CreateDirectoryRecursively(ctx, path.substr(0, pos)); + NS_PROJ::FileManager::mkdir(ctx, path.c_str()); } // --------------------------------------------------------------------------- @@ -2320,7 +1137,7 @@ std::string pj_context_get_user_writable_directory(PJ_CONTEXT *ctx, if (SHGetFolderPathW(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, &wPath[0]) == S_OK) { wPath.resize(wcslen(wPath.data())); - path = WStringToUTF8(wPath); + path = NS_PROJ::WStringToUTF8(wPath); } else { const char *local_app_data = getenv("LOCALAPPDATA"); if (!local_app_data) { @@ -2352,25 +1169,13 @@ std::string pj_context_get_user_writable_directory(PJ_CONTEXT *ctx, ctx->user_writable_directory = path; } if (create) { - CreateDirectory(ctx->user_writable_directory); + CreateDirectoryRecursively(ctx, ctx->user_writable_directory); } return ctx->user_writable_directory; } // --------------------------------------------------------------------------- -std::string pj_context_get_grid_cache_filename(PJ_CONTEXT *ctx) { - pj_load_ini(ctx); - if (!ctx->gridChunkCache.filename.empty()) { - return ctx->gridChunkCache.filename; - } - const std::string path(pj_context_get_user_writable_directory(ctx, true)); - ctx->gridChunkCache.filename = path + "/cache.db"; - return ctx->gridChunkCache.filename; -} - -// --------------------------------------------------------------------------- - #ifdef WIN32 static const char dir_chars[] = "/\\"; #else @@ -2388,9 +1193,268 @@ static bool is_rel_or_absolute_filename(const char *name) { (name[0] != '\0' && name[1] == ':' && strchr(dir_chars, name[2])); } -static std::string build_url(PJ_CONTEXT *ctx, const char *name) { - if (!is_tilde_slash(name) && !is_rel_or_absolute_filename(name) && - !starts_with(name, "http://") && !starts_with(name, "https://")) { +// --------------------------------------------------------------------------- + +#ifdef _WIN32 +static const char *get_path_from_win32_projlib(PJ_CONTEXT *ctx, + const char *name, + std::string &out) { + /* Check if proj.db lieves in a share/proj dir parallel to bin/proj.dll */ + /* Based in + * https://stackoverflow.com/questions/9112893/how-to-get-path-to-executable-in-c-running-on-windows + */ + + DWORD path_size = 1024; + + std::wstring wout; + for (;;) { + wout.clear(); + wout.resize(path_size); + DWORD result = GetModuleFileNameW(nullptr, &wout[0], path_size - 1); + DWORD last_error = GetLastError(); + + if (result == 0) { + return nullptr; + } else if (result == path_size - 1) { + if (ERROR_INSUFFICIENT_BUFFER != last_error) { + return nullptr; + } + path_size = path_size * 2; + } else { + break; + } + } + // Now remove the program's name. It was (example) + // "C:\programs\gmt6\bin\gdal_translate.exe" + wout.resize(wcslen(wout.c_str())); + out = NS_PROJ::WStringToUTF8(wout); + size_t k = out.size(); + while (k > 0 && out[--k] != '\\') { + } + out.resize(k); + + out += "/../share/proj/"; + out += name; + + return NS_PROJ::FileManager::exists(ctx, out.c_str()) ? out.c_str() + : nullptr; +} +#endif + +/************************************************************************/ +/* pj_open_lib_internal() */ +/************************************************************************/ + +#ifdef WIN32 +static const char dirSeparator = ';'; +#else +static const char dirSeparator = ':'; +#endif + +static const char *proj_lib_name = +#ifdef PROJ_LIB + PROJ_LIB; +#else + nullptr; +#endif + +static bool ignoreUserWritableDirectory() { + // Env var mostly for testing purposes and being independent from + // an existing installation + const char *envVarIgnoreUserWritableDirectory = + getenv("PROJ_IGNORE_USER_WRITABLE_DIRECTORY"); + return envVarIgnoreUserWritableDirectory != nullptr && + envVarIgnoreUserWritableDirectory[0] != '\0'; +} + +static void * +pj_open_lib_internal(projCtx ctx, const char *name, const char *mode, + void *(*open_file)(projCtx, const char *, const char *), + char *out_full_filename, size_t out_full_filename_size) { + try { + std::string fname; + const char *sysname = nullptr; + void *fid = nullptr; + std::string projLib; + + if (ctx == nullptr) { + ctx = pj_get_default_ctx(); + } + + if (out_full_filename != nullptr && out_full_filename_size > 0) + out_full_filename[0] = '\0'; + + /* check if ~/name */ + if (is_tilde_slash(name)) + if ((sysname = getenv("HOME")) != nullptr) { + fname = sysname; + fname += DIR_CHAR; + fname += name; + sysname = fname.c_str(); + } else + return nullptr; + + /* or fixed path: /name, ./name or ../name */ + else if (is_rel_or_absolute_filename(name)) { + sysname = name; +#ifdef _WIN32 + try { + NS_PROJ::UTF8ToWString(name); + } catch (const std::exception &) { + fname = NS_PROJ::Win32Recode(name, CP_ACP, CP_UTF8); + sysname = fname.c_str(); + } +#endif + } + + else if (starts_with(name, "http://") || starts_with(name, "https://")) + sysname = name; + + /* or try to use application provided file finder */ + else if (ctx->file_finder != nullptr && + (sysname = ctx->file_finder( + ctx, name, ctx->file_finder_user_data)) != nullptr) + ; + + else if (ctx->file_finder_legacy != nullptr && + (sysname = ctx->file_finder_legacy(name)) != nullptr) + ; + + /* The user has search paths set */ + else if (!ctx->search_paths.empty()) { + for (const auto &path : ctx->search_paths) { + try { + fname = path; + fname += DIR_CHAR; + fname += name; + sysname = fname.c_str(); + fid = open_file(ctx, sysname, mode); + } catch (const std::exception &) { + } + if (fid) + break; + } + } + + else if (!ignoreUserWritableDirectory() && + (fid = open_file( + ctx, (pj_context_get_user_writable_directory(ctx, false) + + DIR_CHAR + name) + .c_str(), + mode)) != nullptr) { + fname = pj_context_get_user_writable_directory(ctx, false); + fname += DIR_CHAR; + fname += name; + sysname = fname.c_str(); + } + + /* if is environment PROJ_LIB defined */ + else if (!(projLib = NS_PROJ::FileManager::getProjLibEnvVar(ctx)) + .empty()) { + auto paths = NS_PROJ::internal::split(projLib, dirSeparator); + for (const auto &path : paths) { + fname = path; + fname += DIR_CHAR; + fname += name; + sysname = fname.c_str(); + fid = open_file(ctx, sysname, mode); + if (fid) + break; + } +#ifdef _WIN32 + /* check if it lives in a ../share/proj dir of the proj dll */ + } else if ((sysname = get_path_from_win32_projlib(ctx, name, fname)) != + nullptr) { +#endif + /* or hardcoded path */ + } else if ((sysname = proj_lib_name) != nullptr) { + fname = sysname; + fname += DIR_CHAR; + fname += name; + sysname = fname.c_str(); + /* just try it bare bones */ + } else { + sysname = name; + } + + assert(sysname); // to make Coverity Scan happy + if (fid != nullptr || + (fid = open_file(ctx, sysname, mode)) != nullptr) { + if (out_full_filename != nullptr && out_full_filename_size > 0) { + // cppcheck-suppress nullPointer + strncpy(out_full_filename, sysname, out_full_filename_size); + out_full_filename[out_full_filename_size - 1] = '\0'; + } + errno = 0; + } + + if (ctx->last_errno == 0 && errno != 0) + pj_ctx_set_errno(ctx, errno); + + pj_log(ctx, PJ_LOG_DEBUG_MAJOR, "pj_open_lib(%s): call fopen(%s) - %s", + name, sysname, fid == nullptr ? "failed" : "succeeded"); + + return (fid); + } catch (const std::exception &) { + + pj_log(ctx, PJ_LOG_DEBUG_MAJOR, "pj_open_lib(%s): out of memory", name); + + return nullptr; + } +} + +/************************************************************************/ +/* pj_open_file_with_manager() */ +/************************************************************************/ + +static void *pj_open_file_with_manager(projCtx ctx, const char *name, + const char * /* mode */) { + return NS_PROJ::FileManager::open(ctx, name, NS_PROJ::FileAccess::READ_ONLY) + .release(); +} + +/************************************************************************/ +/* FileManager::open_resource_file() */ +/************************************************************************/ + +std::unique_ptr<NS_PROJ::File> +NS_PROJ::FileManager::open_resource_file(projCtx ctx, const char *name) { + + if (ctx == nullptr) { + ctx = pj_get_default_ctx(); + } + + auto file = std::unique_ptr<NS_PROJ::File>( + reinterpret_cast<NS_PROJ::File *>(pj_open_lib_internal( + ctx, name, "rb", pj_open_file_with_manager, nullptr, 0))); + + // Retry with a .tif extension if the file name doesn't end with .tif + if (file == nullptr && !is_tilde_slash(name) && + !is_rel_or_absolute_filename(name) && !starts_with(name, "http://") && + !starts_with(name, "https://") && strcmp(name, "proj.db") != 0 && + strstr(name, ".tif") == nullptr) { + std::string filename(name); + auto pos = filename.rfind('.'); + if (pos + 4 == filename.size()) { + filename = filename.substr(0, pos) + ".tif"; + file.reset(reinterpret_cast<NS_PROJ::File *>( + pj_open_lib_internal(ctx, filename.c_str(), "rb", + pj_open_file_with_manager, nullptr, 0))); + } else { + // For example for resource files like 'alaska' + filename += ".tif"; + file.reset(reinterpret_cast<NS_PROJ::File *>( + pj_open_lib_internal(ctx, filename.c_str(), "rb", + pj_open_file_with_manager, nullptr, 0))); + } + if (file) { + pj_ctx_set_errno(ctx, 0); + } + } + + if (file == nullptr && !is_tilde_slash(name) && + !is_rel_or_absolute_filename(name) && !starts_with(name, "http://") && + !starts_with(name, "https://") && pj_context_is_network_enabled(ctx)) { std::string remote_file(pj_context_get_url_endpoint(ctx)); if (!remote_file.empty()) { if (remote_file.back() != '/') { @@ -2400,369 +1464,272 @@ static std::string build_url(PJ_CONTEXT *ctx, const char *name) { auto pos = remote_file.rfind('.'); if (pos + 4 == remote_file.size()) { remote_file = remote_file.substr(0, pos) + ".tif"; + file = open(ctx, remote_file.c_str(), + NS_PROJ::FileAccess::READ_ONLY); + if (file) { + pj_log(ctx, PJ_LOG_DEBUG_MAJOR, "Using %s", + remote_file.c_str()); + pj_ctx_set_errno(ctx, 0); + } } else { // For example for resource files like 'alaska' - remote_file += ".tif"; + auto remote_file_tif = remote_file + ".tif"; + file = open(ctx, remote_file_tif.c_str(), + NS_PROJ::FileAccess::READ_ONLY); + if (file) { + pj_log(ctx, PJ_LOG_DEBUG_MAJOR, "Using %s", + remote_file_tif.c_str()); + pj_ctx_set_errno(ctx, 0); + } else { + // Init files + file = open(ctx, remote_file.c_str(), + NS_PROJ::FileAccess::READ_ONLY); + if (file) { + pj_log(ctx, PJ_LOG_DEBUG_MAJOR, "Using %s", + remote_file.c_str()); + pj_ctx_set_errno(ctx, 0); + } + } } } - return remote_file; } - return name; + return file; } -//! @endcond +/************************************************************************/ +/* pj_open_lib() */ +/************************************************************************/ -// --------------------------------------------------------------------------- +#ifndef REMOVE_LEGACY_SUPPORT -/** Return if a file must be downloaded or is already available in the - * PROJ user-writable directory. - * - * The file will be determinted to have to be downloaded if it does not exist - * yet in the user-writable directory, or if it is determined that a more recent - * version exists. To determine if a more recent version exists, PROJ will - * use the "downloaded_file_properties" table of its grid cache database. - * Consequently files manually placed in the user-writable - * directory without using this function would be considered as - * non-existing/obsolete and would be unconditionnaly downloaded again. - * - * This function can only be used if networking is enabled, and either - * the default curl network API or a custom one have been installed. +// Used by following legacy function +static void *pj_ctx_fopen_adapter(projCtx ctx, const char *name, + const char *mode) { + return pj_ctx_fopen(ctx, name, mode); +} + +// Legacy function +PAFile pj_open_lib(projCtx ctx, const char *name, const char *mode) { + return (PAFile)pj_open_lib_internal(ctx, name, mode, pj_ctx_fopen_adapter, + nullptr, 0); +} + +#endif // REMOVE_LEGACY_SUPPORT + +/************************************************************************/ +/* pj_find_file() */ +/************************************************************************/ + +/** Returns the full filename corresponding to a proj resource file specified + * as a short filename. * - * @param ctx PROJ context, or NULL - * @param url_or_filename URL or filename (without directory component) - * @param ignore_ttl_setting If set to FALSE, PROJ will only check the - * recentness of an already downloaded file, if - * the delay between the last time it has been - * verified and the current time exceeds the TTL - * setting. This can save network accesses. - * If set to TRUE, PROJ will unconditionnally - * check from the server the recentness of the file. - * @return TRUE if the file must be downloaded with proj_download_file() - * @since 7.0 + * @param ctx context. + * @param short_filename short filename (e.g. egm96_15.gtx). Must not be NULL. + * @param out_full_filename output buffer, of size out_full_filename_size, that + * will receive the full filename on success. + * Will be zero-terminated. + * @param out_full_filename_size size of out_full_filename. + * @return 1 if the file was found, 0 otherwise. */ - -int proj_is_download_needed(PJ_CONTEXT *ctx, const char *url_or_filename, - int ignore_ttl_setting) { - if (ctx == nullptr) { - ctx = pj_get_default_ctx(); - } - if (!pj_context_is_network_enabled(ctx)) { - pj_log(ctx, PJ_LOG_ERROR, "Networking capabilities are not enabled"); - return false; +int pj_find_file(projCtx ctx, const char *short_filename, + char *out_full_filename, size_t out_full_filename_size) { + auto f = reinterpret_cast<NS_PROJ::File *>(pj_open_lib_internal( + ctx, short_filename, "rb", pj_open_file_with_manager, out_full_filename, + out_full_filename_size)); + if (f != nullptr) { + delete f; + return 1; } + return 0; +} - const auto url(build_url(ctx, url_or_filename)); - const char *filename = strrchr(url.c_str(), '/'); - if (filename == nullptr) - return false; - const auto localFilename( - pj_context_get_user_writable_directory(ctx, false) + filename); +/************************************************************************/ +/* pj_context_get_url_endpoint() */ +/************************************************************************/ - auto f = NS_PROJ::FileManager::open(ctx, localFilename.c_str()); - if (!f) { - return true; +std::string pj_context_get_url_endpoint(PJ_CONTEXT *ctx) { + if (!ctx->endpoint.empty()) { + return ctx->endpoint; } - f.reset(); + pj_load_ini(ctx); + return ctx->endpoint; +} - auto diskCache = NS_PROJ::DiskChunkCache::open(ctx); - if (!diskCache) - return false; - auto stmt = - diskCache->prepare("SELECT lastChecked, fileSize, lastModified, etag " - "FROM downloaded_file_properties WHERE url = ?"); - if (!stmt) - return true; - stmt->bindText(url.c_str()); - if (stmt->execute() != SQLITE_ROW) { - return true; +/************************************************************************/ +/* trim() */ +/************************************************************************/ + +static std::string trim(const std::string &s) { + const auto first = s.find_first_not_of(' '); + const auto last = s.find_last_not_of(' '); + if (first == std::string::npos || last == std::string::npos) { + return std::string(); } + return s.substr(first, last - first + 1); +} - NS_PROJ::FileProperties cachedProps; - cachedProps.lastChecked = stmt->getInt64(); - cachedProps.size = stmt->getInt64(); - const char *lastModified = stmt->getText(); - cachedProps.lastModified = lastModified ? lastModified : std::string(); - const char *etag = stmt->getText(); - cachedProps.etag = etag ? etag : std::string(); - - if (!ignore_ttl_setting) { - const auto ttl = NS_PROJ::pj_context_get_grid_cache_ttl(ctx); - if (ttl > 0) { - time_t curTime; - time(&curTime); - if (curTime > cachedProps.lastChecked + ttl) { - - unsigned char dummy; - size_t size_read = 0; - std::string errorBuffer; - errorBuffer.resize(1024); - auto handle = ctx->networking.open( - ctx, url.c_str(), 0, 1, &dummy, &size_read, - errorBuffer.size(), &errorBuffer[0], - ctx->networking.user_data); - if (!handle) { - errorBuffer.resize(strlen(errorBuffer.data())); - pj_log(ctx, PJ_LOG_ERROR, "Cannot open %s: %s", url.c_str(), - errorBuffer.c_str()); - return false; - } - NS_PROJ::FileProperties props; - if (!NS_PROJ::NetworkFile::get_props_from_headers(ctx, handle, - props)) { - ctx->networking.close(ctx, handle, - ctx->networking.user_data); - return false; - } - ctx->networking.close(ctx, handle, ctx->networking.user_data); +/************************************************************************/ +/* pj_load_ini() */ +/************************************************************************/ - if (props.size != cachedProps.size || - props.lastModified != cachedProps.lastModified || - props.etag != cachedProps.etag) { - return true; - } +void pj_load_ini(projCtx ctx) { + if (ctx->iniFileLoaded) + return; + + const char *endpoint_from_env = getenv("PROJ_NETWORK_ENDPOINT"); + if (endpoint_from_env && endpoint_from_env[0] != '\0') { + ctx->endpoint = endpoint_from_env; + } + + ctx->iniFileLoaded = true; + auto file = std::unique_ptr<NS_PROJ::File>( + reinterpret_cast<NS_PROJ::File *>(pj_open_lib_internal( + ctx, "proj.ini", "rb", pj_open_file_with_manager, nullptr, 0))); + if (!file) + return; + file->seek(0, SEEK_END); + const auto filesize = file->tell(); + if (filesize == 0 || filesize > 100 * 1024U) + return; + file->seek(0, SEEK_SET); + std::string content; + content.resize(static_cast<size_t>(filesize)); + const auto nread = file->read(&content[0], content.size()); + if (nread != content.size()) + return; + content += '\n'; + size_t pos = 0; + while (pos != std::string::npos) { + const auto eol = content.find_first_of("\r\n", pos); + if (eol == std::string::npos) { + break; + } - stmt = diskCache->prepare( - "UPDATE downloaded_file_properties SET lastChecked = ? " - "WHERE url = ?"); - if (!stmt) - return false; - stmt->bindInt64(curTime); - stmt->bindText(url.c_str()); - if (stmt->execute() != SQLITE_DONE) { - auto hDB = diskCache->handle(); - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return false; + const auto equal = content.find('=', pos); + if (equal < eol) { + const auto key = trim(content.substr(pos, equal - pos)); + const auto value = + trim(content.substr(equal + 1, eol - (equal + 1))); + if (ctx->endpoint.empty() && key == "cdn_endpoint") { + ctx->endpoint = value; + } else if (key == "network") { + const char *enabled = getenv("PROJ_NETWORK"); + if (enabled == nullptr || enabled[0] == '\0') { + ctx->networking.enabled = ci_equal(value, "ON") || + ci_equal(value, "YES") || + ci_equal(value, "TRUE"); } + } else if (key == "cache_enabled") { + ctx->gridChunkCache.enabled = ci_equal(value, "ON") || + ci_equal(value, "YES") || + ci_equal(value, "TRUE"); + } else if (key == "cache_size_MB") { + const int val = atoi(value.c_str()); + ctx->gridChunkCache.max_size = + val > 0 ? static_cast<long long>(val) * 1024 * 1024 : -1; + } else if (key == "cache_ttl_sec") { + ctx->gridChunkCache.ttl = atoi(value.c_str()); } } + + pos = content.find_first_not_of("\r\n", eol); } +} + +//! @endcond + +/************************************************************************/ +/* pj_set_finder() */ +/************************************************************************/ - return false; +void pj_set_finder(const char *(*new_finder)(const char *)) + +{ + auto ctx = pj_get_default_ctx(); + if (ctx) { + ctx->file_finder_legacy = new_finder; + } } -// --------------------------------------------------------------------------- +/************************************************************************/ +/* proj_context_set_file_finder() */ +/************************************************************************/ -/** Download a file in the PROJ user-writable directory. +/** \brief Assign a file finder callback to a context. * - * The file will only be downloaded if it does not exist yet in the - * user-writable directory, or if it is determined that a more recent - * version exists. To determine if a more recent version exists, PROJ will - * use the "downloaded_file_properties" table of its grid cache database. - * Consequently files manually placed in the user-writable - * directory without using this function would be considered as - * non-existing/obsolete and would be unconditionnaly downloaded again. + * This callback will be used whenever PROJ must open one of its resource files + * (proj.db database, grids, etc...) * - * This function can only be used if networking is enabled, and either - * the default curl network API or a custom one have been installed. + * The callback will be called with the context currently in use at the moment + * where it is used (not necessarily the one provided during this call), and + * with the provided user_data (which may be NULL). + * The user_data must remain valid during the whole lifetime of the context. * - * @param ctx PROJ context, or NULL - * @param url_or_filename URL or filename (without directory component) - * @param ignore_ttl_setting If set to FALSE, PROJ will only check the - * recentness of an already downloaded file, if - * the delay between the last time it has been - * verified and the current time exceeds the TTL - * setting. This can save network accesses. - * If set to TRUE, PROJ will unconditionnally - * check from the server the recentness of the file. - * @param progress_cbk Progress callback, or NULL. - * The passed percentage is in the [0, 1] range. - * The progress callback must return TRUE - * if download must be continued. - * @param user_data User data to provide to the progress callback, or NULL - * @return TRUE if the download was successful (or not needed) - * @since 7.0 + * A finder set on the default context will be inherited by contexts created + * later. + * + * @param ctx PROJ context, or NULL for the default context. + * @param finder Finder callback. May be NULL + * @param user_data User data provided to the finder callback. May be NULL. + * + * @since PROJ 6.0 */ - -int proj_download_file(PJ_CONTEXT *ctx, const char *url_or_filename, - int ignore_ttl_setting, - int (*progress_cbk)(PJ_CONTEXT *, double pct, - void *user_data), - void *user_data) { - if (ctx == nullptr) { +void proj_context_set_file_finder(PJ_CONTEXT *ctx, proj_file_finder finder, + void *user_data) { + if (!ctx) ctx = pj_get_default_ctx(); - } - if (!pj_context_is_network_enabled(ctx)) { - pj_log(ctx, PJ_LOG_ERROR, "Networking capabilities are not enabled"); - return false; - } - if (!proj_is_download_needed(ctx, url_or_filename, ignore_ttl_setting)) { - return true; - } - - const auto url(build_url(ctx, url_or_filename)); - const char *filename = strrchr(url.c_str(), '/'); - if (filename == nullptr) - return false; - const auto localFilename(pj_context_get_user_writable_directory(ctx, true) + - filename); - -#ifdef _WIN32 - const int nPID = GetCurrentProcessId(); -#else - const int nPID = getpid(); -#endif - char szUniqueSuffix[128]; - snprintf(szUniqueSuffix, sizeof(szUniqueSuffix), "%d_%p", nPID, &url); - const auto localFilenameTmp(localFilename + szUniqueSuffix); - FILE *f = fopen(localFilenameTmp.c_str(), "wb"); - if (!f) { - pj_log(ctx, PJ_LOG_ERROR, "Cannot create %s", localFilenameTmp.c_str()); - return false; - } - - constexpr size_t FULL_FILE_CHUNK_SIZE = 1024 * 1024; - std::vector<unsigned char> buffer(FULL_FILE_CHUNK_SIZE); - // For testing purposes only - const char *env_var_PROJ_FULL_FILE_CHUNK_SIZE = - getenv("PROJ_FULL_FILE_CHUNK_SIZE"); - if (env_var_PROJ_FULL_FILE_CHUNK_SIZE && - env_var_PROJ_FULL_FILE_CHUNK_SIZE[0] != '\0') { - buffer.resize(atoi(env_var_PROJ_FULL_FILE_CHUNK_SIZE)); - } - size_t size_read = 0; - std::string errorBuffer; - errorBuffer.resize(1024); - auto handle = ctx->networking.open( - ctx, url.c_str(), 0, buffer.size(), &buffer[0], &size_read, - errorBuffer.size(), &errorBuffer[0], ctx->networking.user_data); - if (!handle) { - errorBuffer.resize(strlen(errorBuffer.data())); - pj_log(ctx, PJ_LOG_ERROR, "Cannot open %s: %s", url.c_str(), - errorBuffer.c_str()); - fclose(f); - unlink(localFilenameTmp.c_str()); - return false; - } - - time_t curTime; - time(&curTime); - NS_PROJ::FileProperties props; - if (!NS_PROJ::NetworkFile::get_props_from_headers(ctx, handle, props)) { - ctx->networking.close(ctx, handle, ctx->networking.user_data); - fclose(f); - unlink(localFilenameTmp.c_str()); - return false; - } + if (!ctx) + return; + ctx->file_finder = finder; + ctx->file_finder_user_data = user_data; +} - if (size_read < - std::min(static_cast<unsigned long long>(buffer.size()), props.size)) { - pj_log(ctx, PJ_LOG_ERROR, "Did not get as many bytes as expected"); - ctx->networking.close(ctx, handle, ctx->networking.user_data); - fclose(f); - unlink(localFilenameTmp.c_str()); - return false; - } - if (fwrite(buffer.data(), size_read, 1, f) != 1) { - pj_log(ctx, PJ_LOG_ERROR, "Write error"); - ctx->networking.close(ctx, handle, ctx->networking.user_data); - fclose(f); - unlink(localFilenameTmp.c_str()); - return false; - } +/************************************************************************/ +/* proj_context_set_search_paths() */ +/************************************************************************/ - unsigned long long totalDownloaded = size_read; - while (totalDownloaded < props.size) { - if (totalDownloaded + buffer.size() > props.size) { - buffer.resize(static_cast<size_t>(props.size - totalDownloaded)); - } - errorBuffer.resize(1024); - size_read = ctx->networking.read_range( - ctx, handle, totalDownloaded, buffer.size(), &buffer[0], - errorBuffer.size(), &errorBuffer[0], ctx->networking.user_data); - - if (size_read < buffer.size()) { - pj_log(ctx, PJ_LOG_ERROR, "Did not get as many bytes as expected"); - ctx->networking.close(ctx, handle, ctx->networking.user_data); - fclose(f); - unlink(localFilenameTmp.c_str()); - return false; - } - if (fwrite(buffer.data(), size_read, 1, f) != 1) { - pj_log(ctx, PJ_LOG_ERROR, "Write error"); - ctx->networking.close(ctx, handle, ctx->networking.user_data); - fclose(f); - unlink(localFilenameTmp.c_str()); - return false; - } - - totalDownloaded += size_read; - if (progress_cbk && - !progress_cbk(ctx, double(totalDownloaded) / props.size, - user_data)) { - ctx->networking.close(ctx, handle, ctx->networking.user_data); - fclose(f); - unlink(localFilenameTmp.c_str()); - return false; +/** \brief Sets search paths. + * + * Those search paths will be used whenever PROJ must open one of its resource + * files + * (proj.db database, grids, etc...) + * + * If set on the default context, they will be inherited by contexts created + * later. + * + * Starting with PROJ 7.0, the path(s) should be encoded in UTF-8. + * + * @param ctx PROJ context, or NULL for the default context. + * @param count_paths Number of paths. 0 if paths == NULL. + * @param paths Paths. May be NULL. + * + * @since PROJ 6.0 + */ +void proj_context_set_search_paths(PJ_CONTEXT *ctx, int count_paths, + const char *const *paths) { + if (!ctx) + ctx = pj_get_default_ctx(); + if (!ctx) + return; + try { + std::vector<std::string> vector_of_paths; + for (int i = 0; i < count_paths; i++) { + vector_of_paths.emplace_back(paths[i]); } + ctx->set_search_paths(vector_of_paths); + } catch (const std::exception &) { } +} - ctx->networking.close(ctx, handle, ctx->networking.user_data); - fclose(f); - - unlink(localFilename.c_str()); - if (rename(localFilenameTmp.c_str(), localFilename.c_str()) != 0) { - pj_log(ctx, PJ_LOG_ERROR, "Cannot rename %s to %s", - localFilenameTmp.c_str(), localFilename.c_str()); - return false; - } +/************************************************************************/ +/* pj_set_searchpath() */ +/* */ +/* Path control for callers that can't practically provide */ +/* pj_set_finder() style callbacks. Call with (0,NULL) as args */ +/* to clear the searchpath set. */ +/************************************************************************/ - auto diskCache = NS_PROJ::DiskChunkCache::open(ctx); - if (!diskCache) - return false; - auto stmt = - diskCache->prepare("SELECT lastChecked, fileSize, lastModified, etag " - "FROM downloaded_file_properties WHERE url = ?"); - if (!stmt) - return false; - stmt->bindText(url.c_str()); - - props.lastChecked = curTime; - auto hDB = diskCache->handle(); - - if (stmt->execute() == SQLITE_ROW) { - stmt = diskCache->prepare( - "UPDATE downloaded_file_properties SET lastChecked = ?, " - "fileSize = ?, lastModified = ?, etag = ? " - "WHERE url = ?"); - if (!stmt) - return false; - stmt->bindInt64(props.lastChecked); - stmt->bindInt64(props.size); - if (props.lastModified.empty()) - stmt->bindNull(); - else - stmt->bindText(props.lastModified.c_str()); - if (props.etag.empty()) - stmt->bindNull(); - else - stmt->bindText(props.etag.c_str()); - stmt->bindText(url.c_str()); - if (stmt->execute() != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return false; - } - } else { - stmt = diskCache->prepare( - "INSERT INTO downloaded_file_properties (url, lastChecked, " - "fileSize, lastModified, etag) VALUES " - "(?,?,?,?,?)"); - if (!stmt) - return false; - stmt->bindText(url.c_str()); - stmt->bindInt64(props.lastChecked); - stmt->bindInt64(props.size); - if (props.lastModified.empty()) - stmt->bindNull(); - else - stmt->bindText(props.lastModified.c_str()); - if (props.etag.empty()) - stmt->bindNull(); - else - stmt->bindText(props.etag.c_str()); - if (stmt->execute() != SQLITE_DONE) { - pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); - return false; - } - } - return true; +void pj_set_searchpath(int count, const char **path) { + proj_context_set_search_paths(nullptr, count, + const_cast<const char *const *>(path)); } diff --git a/src/filemanager.hpp b/src/filemanager.hpp index 9793267c..b84753ce 100644 --- a/src/filemanager.hpp +++ b/src/filemanager.hpp @@ -39,13 +39,26 @@ NS_PROJ_START class File; +enum class FileAccess { + READ_ONLY, // "rb" + READ_UPDATE, // "r+b" + CREATE, // "w+b" +}; + class FileManager { private: FileManager() = delete; public: // "Low-level" interface. - static std::unique_ptr<File> open(PJ_CONTEXT *ctx, const char *filename); + static std::unique_ptr<File> open(PJ_CONTEXT *ctx, const char *filename, + FileAccess access); + static bool exists(PJ_CONTEXT *ctx, const char *filename); + static bool mkdir(PJ_CONTEXT *ctx, const char *filename); + static bool unlink(PJ_CONTEXT *ctx, const char *filename); + static bool rename(PJ_CONTEXT *ctx, const char *oldPath, + const char *newPath); + static std::string getProjLibEnvVar(PJ_CONTEXT *ctx); // "High-level" interface, honoring PROJ_LIB and the like. static std::unique_ptr<File> open_resource_file(PJ_CONTEXT *ctx, @@ -66,6 +79,7 @@ class File { public: virtual ~File(); virtual size_t read(void *buffer, size_t sizeBytes) = 0; + virtual size_t write(const void *buffer, size_t sizeBytes) = 0; virtual bool seek(unsigned long long offset, int whence = SEEK_SET) = 0; virtual unsigned long long tell() = 0; virtual void reassign_context(PJ_CONTEXT *ctx) = 0; @@ -74,8 +88,12 @@ class File { const std::string &name() const { return name_; } }; +// --------------------------------------------------------------------------- + +std::unique_ptr<File> pj_network_file_open(PJ_CONTEXT* ctx, const char* filename); + NS_PROJ_END //! @endcond Doxygen_Suppress -#endif // FILEMANAGER_HPP_INCLUDED
\ No newline at end of file +#endif // FILEMANAGER_HPP_INCLUDED diff --git a/src/grids.cpp b/src/grids.cpp index 3007fedc..d5f961f7 100644 --- a/src/grids.cpp +++ b/src/grids.cpp @@ -112,9 +112,7 @@ VerticalShiftGrid::VerticalShiftGrid(const std::string &nameIn, int widthIn, // --------------------------------------------------------------------------- -bool VerticalShiftGrid::isNodata(float /*val*/, double /* multiplier */) const { - return false; -} +VerticalShiftGrid::~VerticalShiftGrid() = default; // --------------------------------------------------------------------------- @@ -2360,12 +2358,14 @@ bool HorizontalShiftGridSet::reopen(PJ_CONTEXT *ctx) { // --------------------------------------------------------------------------- +#define REL_TOLERANCE_HGRIDSHIFT 1e-5 + const HorizontalShiftGrid *HorizontalShiftGrid::gridAt(double lon, double lat) const { for (const auto &child : m_children) { const auto &extentChild = child->extentAndRes(); - const double epsilon = - (extentChild.resLon + extentChild.resLat) / 10000.0; + const double epsilon = (extentChild.resLon + extentChild.resLat) * + REL_TOLERANCE_HGRIDSHIFT; if ((extentChild.fullWorldLongitude() || (lon + epsilon >= extentChild.westLon && lon - epsilon <= extentChild.eastLon)) && @@ -2385,7 +2385,8 @@ const HorizontalShiftGrid *HorizontalShiftGridSet::gridAt(double lon, return grid.get(); } const auto &extent = grid->extentAndRes(); - const double epsilon = (extent.resLon + extent.resLat) / 10000.0; + const double epsilon = + (extent.resLon + extent.resLat) * REL_TOLERANCE_HGRIDSHIFT; if ((extent.fullWorldLongitude() || (lon + epsilon >= extent.westLon && lon - epsilon <= extent.eastLon)) && @@ -2857,26 +2858,26 @@ static PJ_LP pj_hgrid_interpolate(PJ_LP t, const HorizontalShiftGrid *grid, frct.phi = t.phi - indx.phi; val.lam = val.phi = HUGE_VAL; if (indx.lam < 0) { - if (indx.lam == -1 && frct.lam > 0.99999999999) { + if (indx.lam == -1 && frct.lam > 1 - 10 * REL_TOLERANCE_HGRIDSHIFT) { ++indx.lam; frct.lam = 0.; } else return val; } else if ((in = indx.lam + 1) >= grid->width()) { - if (in == grid->width() && frct.lam < 1e-11) { + if (in == grid->width() && frct.lam < 10 * REL_TOLERANCE_HGRIDSHIFT) { --indx.lam; frct.lam = 1.; } else return val; } if (indx.phi < 0) { - if (indx.phi == -1 && frct.phi > 0.99999999999) { + if (indx.phi == -1 && frct.phi > 1 - 10 * REL_TOLERANCE_HGRIDSHIFT) { ++indx.phi; frct.phi = 0.; } else return val; } else if ((in = indx.phi + 1) >= grid->height()) { - if (in == grid->height() && frct.phi < 1e-11) { + if (in == grid->height() && frct.phi < 10 * REL_TOLERANCE_HGRIDSHIFT) { --indx.phi; frct.phi = 1.; } else @@ -2937,8 +2938,6 @@ static PJ_LP pj_hgrid_apply_internal(projCtx ctx, PJ_LP in, tb.lam -= extent->westLon; tb.phi -= extent->southLat; - tb.lam = adjlon(tb.lam - M_PI) + M_PI; - t = pj_hgrid_interpolate(tb, grid, true); if (grid->hasChanged()) { shouldRetry = gridset->reopen(ctx); @@ -2981,7 +2980,6 @@ static PJ_LP pj_hgrid_apply_internal(projCtx ctx, PJ_LP in, tb = in; tb.lam -= extent->westLon; tb.phi -= extent->southLat; - tb.lam = adjlon(tb.lam - M_PI) + M_PI; dif.lam = std::numeric_limits<double>::max(); dif.phi = std::numeric_limits<double>::max(); continue; diff --git a/src/grids.hpp b/src/grids.hpp index 6b6ee0d2..0fd1b7b0 100644 --- a/src/grids.hpp +++ b/src/grids.hpp @@ -51,7 +51,7 @@ struct ExtentAndRes { // --------------------------------------------------------------------------- -class Grid { +class PROJ_GCC_DLL Grid { protected: std::string m_name; int m_width; @@ -62,40 +62,42 @@ class Grid { const ExtentAndRes &extentIn); public: - virtual ~Grid(); + PROJ_FOR_TEST virtual ~Grid(); - int width() const { return m_width; } - int height() const { return m_height; } - const ExtentAndRes &extentAndRes() const { return m_extent; } - const std::string &name() const { return m_name; } + PROJ_FOR_TEST int width() const { return m_width; } + PROJ_FOR_TEST int height() const { return m_height; } + PROJ_FOR_TEST const ExtentAndRes &extentAndRes() const { return m_extent; } + PROJ_FOR_TEST const std::string &name() const { return m_name; } - virtual bool isNullGrid() const { return false; } - virtual bool hasChanged() const = 0; + PROJ_FOR_TEST virtual bool isNullGrid() const { return false; } + PROJ_FOR_TEST virtual bool hasChanged() const = 0; }; // --------------------------------------------------------------------------- -class VerticalShiftGrid : public Grid { +class PROJ_GCC_DLL VerticalShiftGrid : public Grid { protected: std::vector<std::unique_ptr<VerticalShiftGrid>> m_children{}; public: - VerticalShiftGrid(const std::string &nameIn, int widthIn, int heightIn, - const ExtentAndRes &extentIn); + PROJ_FOR_TEST VerticalShiftGrid(const std::string &nameIn, int widthIn, + int heightIn, const ExtentAndRes &extentIn); + PROJ_FOR_TEST ~VerticalShiftGrid() override; - const VerticalShiftGrid *gridAt(double lon, double lat) const; + PROJ_FOR_TEST const VerticalShiftGrid *gridAt(double lon, double lat) const; - virtual bool isNodata(float /*val*/, double /* multiplier */) const; + PROJ_FOR_TEST virtual bool isNodata(float /*val*/, + double /* multiplier */) const = 0; // x = 0 is western-most column, y = 0 is southern-most line - virtual bool valueAt(int x, int y, float &out) const = 0; + PROJ_FOR_TEST virtual bool valueAt(int x, int y, float &out) const = 0; - virtual void reassign_context(PJ_CONTEXT *ctx) = 0; + PROJ_FOR_TEST virtual void reassign_context(PJ_CONTEXT *ctx) = 0; }; // --------------------------------------------------------------------------- -class VerticalShiftGridSet { +class PROJ_GCC_DLL VerticalShiftGridSet { protected: std::string m_name{}; std::string m_format{}; @@ -104,45 +106,50 @@ class VerticalShiftGridSet { VerticalShiftGridSet(); public: - virtual ~VerticalShiftGridSet(); + PROJ_FOR_TEST virtual ~VerticalShiftGridSet(); - static std::unique_ptr<VerticalShiftGridSet> + PROJ_FOR_TEST static std::unique_ptr<VerticalShiftGridSet> open(PJ_CONTEXT *ctx, const std::string &filename); - const std::string &name() const { return m_name; } - const std::string &format() const { return m_format; } - const std::vector<std::unique_ptr<VerticalShiftGrid>> &grids() const { + PROJ_FOR_TEST const std::string &name() const { return m_name; } + PROJ_FOR_TEST const std::string &format() const { return m_format; } + PROJ_FOR_TEST const std::vector<std::unique_ptr<VerticalShiftGrid>> & + grids() const { return m_grids; } - const VerticalShiftGrid *gridAt(double lon, double lat) const; + PROJ_FOR_TEST const VerticalShiftGrid *gridAt(double lon, double lat) const; - virtual void reassign_context(PJ_CONTEXT *ctx); - virtual bool reopen(PJ_CONTEXT *ctx); + PROJ_FOR_TEST virtual void reassign_context(PJ_CONTEXT *ctx); + PROJ_FOR_TEST virtual bool reopen(PJ_CONTEXT *ctx); }; // --------------------------------------------------------------------------- -class HorizontalShiftGrid : public Grid { +class PROJ_GCC_DLL HorizontalShiftGrid : public Grid { protected: std::vector<std::unique_ptr<HorizontalShiftGrid>> m_children{}; public: - HorizontalShiftGrid(const std::string &nameIn, int widthIn, int heightIn, - const ExtentAndRes &extentIn); - ~HorizontalShiftGrid() override; + PROJ_FOR_TEST HorizontalShiftGrid(const std::string &nameIn, int widthIn, + int heightIn, + const ExtentAndRes &extentIn); + PROJ_FOR_TEST ~HorizontalShiftGrid() override; - const HorizontalShiftGrid *gridAt(double lon, double lat) const; + PROJ_FOR_TEST const HorizontalShiftGrid *gridAt(double lon, + double lat) const; // x = 0 is western-most column, y = 0 is southern-most line - virtual bool valueAt(int x, int y, bool compensateNTConvention, - float &lonShift, float &latShift) const = 0; + PROJ_FOR_TEST virtual bool valueAt(int x, int y, + bool compensateNTConvention, + float &lonShift, + float &latShift) const = 0; - virtual void reassign_context(PJ_CONTEXT *ctx) = 0; + PROJ_FOR_TEST virtual void reassign_context(PJ_CONTEXT *ctx) = 0; }; // --------------------------------------------------------------------------- -class HorizontalShiftGridSet { +class PROJ_GCC_DLL HorizontalShiftGridSet { protected: std::string m_name{}; std::string m_format{}; @@ -151,54 +158,57 @@ class HorizontalShiftGridSet { HorizontalShiftGridSet(); public: - virtual ~HorizontalShiftGridSet(); + PROJ_FOR_TEST virtual ~HorizontalShiftGridSet(); - static std::unique_ptr<HorizontalShiftGridSet> + PROJ_FOR_TEST static std::unique_ptr<HorizontalShiftGridSet> open(PJ_CONTEXT *ctx, const std::string &filename); - const std::string &name() const { return m_name; } - const std::string &format() const { return m_format; } - const std::vector<std::unique_ptr<HorizontalShiftGrid>> &grids() const { + PROJ_FOR_TEST const std::string &name() const { return m_name; } + PROJ_FOR_TEST const std::string &format() const { return m_format; } + PROJ_FOR_TEST const std::vector<std::unique_ptr<HorizontalShiftGrid>> & + grids() const { return m_grids; } - const HorizontalShiftGrid *gridAt(double lon, double lat) const; + PROJ_FOR_TEST const HorizontalShiftGrid *gridAt(double lon, + double lat) const; - virtual void reassign_context(PJ_CONTEXT *ctx); - virtual bool reopen(PJ_CONTEXT *ctx); + PROJ_FOR_TEST virtual void reassign_context(PJ_CONTEXT *ctx); + PROJ_FOR_TEST virtual bool reopen(PJ_CONTEXT *ctx); }; // --------------------------------------------------------------------------- -class GenericShiftGrid : public Grid { +class PROJ_GCC_DLL GenericShiftGrid : public Grid { protected: std::vector<std::unique_ptr<GenericShiftGrid>> m_children{}; public: - GenericShiftGrid(const std::string &nameIn, int widthIn, int heightIn, - const ExtentAndRes &extentIn); + PROJ_FOR_TEST GenericShiftGrid(const std::string &nameIn, int widthIn, + int heightIn, const ExtentAndRes &extentIn); - ~GenericShiftGrid() override; + PROJ_FOR_TEST ~GenericShiftGrid() override; - const GenericShiftGrid *gridAt(double lon, double lat) const; + PROJ_FOR_TEST const GenericShiftGrid *gridAt(double lon, double lat) const; - virtual std::string unit(int sample) const = 0; + PROJ_FOR_TEST virtual std::string unit(int sample) const = 0; - virtual std::string description(int sample) const = 0; + PROJ_FOR_TEST virtual std::string description(int sample) const = 0; - virtual std::string metadataItem(const std::string &key, - int sample = -1) const = 0; + PROJ_FOR_TEST virtual std::string metadataItem(const std::string &key, + int sample = -1) const = 0; - virtual int samplesPerPixel() const = 0; + PROJ_FOR_TEST virtual int samplesPerPixel() const = 0; // x = 0 is western-most column, y = 0 is southern-most line - virtual bool valueAt(int x, int y, int sample, float &out) const = 0; + PROJ_FOR_TEST virtual bool valueAt(int x, int y, int sample, + float &out) const = 0; - virtual void reassign_context(PJ_CONTEXT *ctx) = 0; + PROJ_FOR_TEST virtual void reassign_context(PJ_CONTEXT *ctx) = 0; }; // --------------------------------------------------------------------------- -class GenericShiftGridSet { +class PROJ_GCC_DLL GenericShiftGridSet { protected: std::string m_name{}; std::string m_format{}; @@ -207,20 +217,21 @@ class GenericShiftGridSet { GenericShiftGridSet(); public: - virtual ~GenericShiftGridSet(); + PROJ_FOR_TEST virtual ~GenericShiftGridSet(); - static std::unique_ptr<GenericShiftGridSet> + PROJ_FOR_TEST static std::unique_ptr<GenericShiftGridSet> open(PJ_CONTEXT *ctx, const std::string &filename); - const std::string &name() const { return m_name; } - const std::string &format() const { return m_format; } - const std::vector<std::unique_ptr<GenericShiftGrid>> &grids() const { + PROJ_FOR_TEST const std::string &name() const { return m_name; } + PROJ_FOR_TEST const std::string &format() const { return m_format; } + PROJ_FOR_TEST const std::vector<std::unique_ptr<GenericShiftGrid>> & + grids() const { return m_grids; } - const GenericShiftGrid *gridAt(double lon, double lat) const; + PROJ_FOR_TEST const GenericShiftGrid *gridAt(double lon, double lat) const; - virtual void reassign_context(PJ_CONTEXT *ctx); - virtual bool reopen(PJ_CONTEXT *ctx); + PROJ_FOR_TEST virtual void reassign_context(PJ_CONTEXT *ctx); + PROJ_FOR_TEST virtual bool reopen(PJ_CONTEXT *ctx); }; // --------------------------------------------------------------------------- diff --git a/src/iso19111/factory.cpp b/src/iso19111/factory.cpp index 7fb248c6..96e62c16 100644 --- a/src/iso19111/factory.cpp +++ b/src/iso19111/factory.cpp @@ -45,7 +45,7 @@ #include "proj/internal/lru_cache.hpp" #include "proj/internal/tracing.hpp" -#include "sqlite3.hpp" +#include "sqlite3_utils.hpp" #include <cmath> #include <cstdlib> @@ -491,7 +491,10 @@ void DatabaseContext::Private::cache(const std::string &code, void DatabaseContext::Private::open(const std::string &databasePath, PJ_CONTEXT *ctx) { - setPjCtxt(ctx ? ctx : pj_get_default_ctx()); + if (!ctx) { + ctx = pj_get_default_ctx(); + } + setPjCtxt(ctx); std::string path(databasePath); if (path.empty()) { path.resize(2048); @@ -503,23 +506,26 @@ void DatabaseContext::Private::open(const std::string &databasePath, } } + std::string vfsName; #ifdef ENABLE_CUSTOM_LOCKLESS_VFS - vfs_ = SQLite3VFS::create(false, true, true); - if (vfs_ == nullptr || - sqlite3_open_v2(path.c_str(), &sqlite_handle_, - SQLITE_OPEN_READONLY | SQLITE_OPEN_NOMUTEX, - vfs_->name()) != SQLITE_OK || - !sqlite_handle_) { - throw FactoryException("Open of " + path + " failed"); + if (ctx->custom_sqlite3_vfs_name.empty()) { + vfs_ = SQLite3VFS::create(false, true, true); + if (vfs_ == nullptr) { + throw FactoryException("Open of " + path + " failed"); + } + vfsName = vfs_->name(); + } else +#endif + { + vfsName = ctx->custom_sqlite3_vfs_name; } -#else if (sqlite3_open_v2(path.c_str(), &sqlite_handle_, SQLITE_OPEN_READONLY | SQLITE_OPEN_NOMUTEX, - nullptr) != SQLITE_OK || + vfsName.empty() ? nullptr : vfsName.c_str()) != + SQLITE_OK || !sqlite_handle_) { throw FactoryException("Open of " + path + " failed"); } -#endif databasePath_ = path; registerFunctions(); diff --git a/src/lib_proj.cmake b/src/lib_proj.cmake index fdb59434..d5a4d63e 100644 --- a/src/lib_proj.cmake +++ b/src/lib_proj.cmake @@ -247,7 +247,6 @@ set(SRC_LIBPROJ_CORE mlfn.cpp msfn.cpp mutex.cpp - open_lib.cpp param.cpp phi2.cpp pipeline.cpp @@ -282,8 +281,9 @@ set(SRC_LIBPROJ_CORE grids.cpp filemanager.hpp filemanager.cpp - sqlite3.hpp - sqlite3.cpp + networkfilemanager.cpp + sqlite3_utils.hpp + sqlite3_utils.cpp ${CMAKE_CURRENT_BINARY_DIR}/proj_config.h ) @@ -367,6 +367,10 @@ target_compile_options(${PROJ_CORE_TARGET} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${PROJ_CXX_WARN_FLAGS}> ) +if(MSVC OR MINGW) + target_compile_definitions(${PROJ_CORE_TARGET} PRIVATE -DNOMINMAX) +endif() + # Tell Intel compiler to do arithmetic accurately. This is needed to stop the # compiler from ignoring parentheses in expressions like (a + b) + c and from # simplifying 0.0 + x to x (which is wrong if x = -0.0). @@ -438,16 +442,16 @@ if(USE_THREAD AND Threads_FOUND AND CMAKE_USE_PTHREADS_INIT) target_link_libraries(${PROJ_CORE_TARGET} ${CMAKE_THREAD_LIBS_INIT}) endif() -include_directories(${SQLITE3_INCLUDE_DIR}) +target_include_directories(${PROJ_CORE_TARGET} PRIVATE ${SQLITE3_INCLUDE_DIR}) target_link_libraries(${PROJ_CORE_TARGET} ${SQLITE3_LIBRARY}) -if(NOT DISABLE_TIFF_IS_STRONGLY_DISCOURAGED) - include_directories(${TIFF_INCLUDE_DIR}) +if(NOT DISABLE_TIFF) + target_include_directories(${PROJ_CORE_TARGET} PRIVATE ${TIFF_INCLUDE_DIR}) target_link_libraries(${PROJ_CORE_TARGET} ${TIFF_LIBRARY}) endif() if(CURL_FOUND) - include_directories(${CURL_INCLUDE_DIR}) + target_include_directories(${PROJ_CORE_TARGET} PRIVATE ${CURL_INCLUDE_DIR}) target_link_libraries(${PROJ_CORE_TARGET} ${CURL_LIBRARY}) endif() diff --git a/src/networkfilemanager.cpp b/src/networkfilemanager.cpp new file mode 100644 index 00000000..5d15c5af --- /dev/null +++ b/src/networkfilemanager.cpp @@ -0,0 +1,2541 @@ +/****************************************************************************** + * Project: PROJ + * Purpose: Functionality related to network access and caching + * Author: Even Rouault, <even.rouault at spatialys.com> + * + ****************************************************************************** + * Copyright (c) 2019-2020, Even Rouault, <even.rouault at spatialys.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#ifndef FROM_PROJ_CPP +#define FROM_PROJ_CPP +#endif +#define LRU11_DO_NOT_DEFINE_OUT_OF_CLASS_METHODS + +#include <stdlib.h> + +#include <algorithm> +#include <limits> +#include <string> + +#include "filemanager.hpp" +#include "proj.h" +#include "proj/internal/internal.hpp" +#include "proj/internal/lru_cache.hpp" +#include "proj_internal.h" +#include "sqlite3_utils.hpp" + +#ifdef __MINGW32__ +// mingw32-win32 doesn't implement std::mutex +namespace { +class MyMutex { + public: + // cppcheck-suppress functionStatic + void lock() { pj_acquire_lock(); } + // cppcheck-suppress functionStatic + void unlock() { pj_release_lock(); } +}; +} +#else +#include <mutex> +#define MyMutex std::mutex +#endif + +#ifdef CURL_ENABLED +#include <curl/curl.h> +#include <sqlite3.h> // for sqlite3_snprintf +#endif + +#include <sys/stat.h> + +#ifdef _WIN32 +#include <shlobj.h> +#else +#include <sys/types.h> +#include <unistd.h> +#endif + +#if defined(_WIN32) +#include <windows.h> +#elif defined(__MACH__) && defined(__APPLE__) +#include <mach-o/dyld.h> +#elif defined(__FreeBSD__) +#include <sys/sysctl.h> +#include <sys/types.h> +#endif + +#include <time.h> + +//! @cond Doxygen_Suppress + +#define STR_HELPER(x) #x +#define STR(x) STR_HELPER(x) + +using namespace NS_PROJ::internal; + +NS_PROJ_START + +// --------------------------------------------------------------------------- + +static void sleep_ms(int ms) { +#ifdef _WIN32 + Sleep(ms); +#else + usleep(ms * 1000); +#endif +} + +// --------------------------------------------------------------------------- + +constexpr size_t DOWNLOAD_CHUNK_SIZE = 16 * 1024; +constexpr int MAX_CHUNKS = 64; + +struct FileProperties { + unsigned long long size = 0; + time_t lastChecked = 0; + std::string lastModified{}; + std::string etag{}; +}; + +class NetworkChunkCache { + public: + void insert(PJ_CONTEXT *ctx, const std::string &url, + unsigned long long chunkIdx, std::vector<unsigned char> &&data); + + std::shared_ptr<std::vector<unsigned char>> + get(PJ_CONTEXT *ctx, const std::string &url, unsigned long long chunkIdx); + + std::shared_ptr<std::vector<unsigned char>> get(PJ_CONTEXT *ctx, + const std::string &url, + unsigned long long chunkIdx, + FileProperties &props); + + void clearMemoryCache(); + + static void clearDiskChunkCache(PJ_CONTEXT *ctx); + + private: + struct Key { + std::string url; + unsigned long long chunkIdx; + + Key(const std::string &urlIn, unsigned long long chunkIdxIn) + : url(urlIn), chunkIdx(chunkIdxIn) {} + bool operator==(const Key &other) const { + return url == other.url && chunkIdx == other.chunkIdx; + } + }; + + struct KeyHasher { + std::size_t operator()(const Key &k) const { + return std::hash<std::string>{}(k.url) ^ + (std::hash<unsigned long long>{}(k.chunkIdx) << 1); + } + }; + + lru11::Cache< + Key, std::shared_ptr<std::vector<unsigned char>>, MyMutex, + std::unordered_map< + Key, + typename std::list<lru11::KeyValuePair< + Key, std::shared_ptr<std::vector<unsigned char>>>>::iterator, + KeyHasher>> + cache_{MAX_CHUNKS}; +}; + +// --------------------------------------------------------------------------- + +static NetworkChunkCache gNetworkChunkCache{}; + +// --------------------------------------------------------------------------- + +class NetworkFilePropertiesCache { + public: + void insert(PJ_CONTEXT *ctx, const std::string &url, FileProperties &props); + + bool tryGet(PJ_CONTEXT *ctx, const std::string &url, FileProperties &props); + + void clearMemoryCache(); + + private: + lru11::Cache<std::string, FileProperties, MyMutex> cache_{}; +}; + +// --------------------------------------------------------------------------- + +static NetworkFilePropertiesCache gNetworkFileProperties{}; + +// --------------------------------------------------------------------------- + +class DiskChunkCache { + PJ_CONTEXT *ctx_ = nullptr; + std::string path_{}; + sqlite3 *hDB_ = nullptr; + std::string thisNamePtr_{}; + std::unique_ptr<SQLite3VFS> vfs_{}; + + explicit DiskChunkCache(PJ_CONTEXT *ctx, const std::string &path); + + bool initialize(); + void commitAndClose(); + + bool createDBStructure(); + bool checkConsistency(); + bool get_links(sqlite3_int64 chunk_id, sqlite3_int64 &link_id, + sqlite3_int64 &prev, sqlite3_int64 &next, + sqlite3_int64 &head, sqlite3_int64 &tail); + bool update_links_of_prev_and_next_links(sqlite3_int64 prev, + sqlite3_int64 next); + bool update_linked_chunks(sqlite3_int64 link_id, sqlite3_int64 prev, + sqlite3_int64 next); + bool update_linked_chunks_head_tail(sqlite3_int64 head, sqlite3_int64 tail); + + DiskChunkCache(const DiskChunkCache &) = delete; + DiskChunkCache &operator=(const DiskChunkCache &) = delete; + + public: + static std::unique_ptr<DiskChunkCache> open(PJ_CONTEXT *ctx); + ~DiskChunkCache(); + + sqlite3 *handle() { return hDB_; } + std::unique_ptr<SQLiteStatement> prepare(const char *sql); + bool move_to_head(sqlite3_int64 chunk_id); + bool move_to_tail(sqlite3_int64 chunk_id); + void closeAndUnlink(); +}; + +// --------------------------------------------------------------------------- + +static bool pj_context_get_grid_cache_is_enabled(PJ_CONTEXT *ctx) { + pj_load_ini(ctx); + return ctx->gridChunkCache.enabled; +} + +// --------------------------------------------------------------------------- + +static long long pj_context_get_grid_cache_max_size(PJ_CONTEXT *ctx) { + pj_load_ini(ctx); + return ctx->gridChunkCache.max_size; +} + +// --------------------------------------------------------------------------- + +static int pj_context_get_grid_cache_ttl(PJ_CONTEXT *ctx) { + pj_load_ini(ctx); + return ctx->gridChunkCache.ttl; +} + +// --------------------------------------------------------------------------- + +std::unique_ptr<DiskChunkCache> DiskChunkCache::open(PJ_CONTEXT *ctx) { + if (!pj_context_get_grid_cache_is_enabled(ctx)) { + return nullptr; + } + const auto cachePath = pj_context_get_grid_cache_filename(ctx); + if (cachePath.empty()) { + return nullptr; + } + + auto diskCache = + std::unique_ptr<DiskChunkCache>(new DiskChunkCache(ctx, cachePath)); + if (!diskCache->initialize()) + diskCache.reset(); + return diskCache; +} + +// --------------------------------------------------------------------------- + +DiskChunkCache::DiskChunkCache(PJ_CONTEXT *ctx, const std::string &path) + : ctx_(ctx), path_(path) {} + +// --------------------------------------------------------------------------- + +bool DiskChunkCache::initialize() { + std::string vfsName; + if (ctx_->custom_sqlite3_vfs_name.empty()) { + vfs_ = SQLite3VFS::create(true, false, false); + if (vfs_ == nullptr) { + return false; + } + vfsName = vfs_->name(); + } else { + vfsName = ctx_->custom_sqlite3_vfs_name; + } + sqlite3_open_v2(path_.c_str(), &hDB_, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, + vfsName.c_str()); + if (!hDB_) { + pj_log(ctx_, PJ_LOG_ERROR, "Cannot open %s", path_.c_str()); + return false; + } + + // Cannot run more than 30 times / a bit more than one second. + for (int i = 0;; i++) { + int ret = + sqlite3_exec(hDB_, "BEGIN EXCLUSIVE", nullptr, nullptr, nullptr); + if (ret == SQLITE_OK) { + break; + } + if (ret != SQLITE_BUSY) { + pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); + sqlite3_close(hDB_); + hDB_ = nullptr; + return false; + } + const char *max_iters = getenv("PROJ_LOCK_MAX_ITERS"); + if (i >= (max_iters && max_iters[0] ? atoi(max_iters) + : 30)) { // A bit more than 1 second + pj_log(ctx_, PJ_LOG_ERROR, "Cannot take exclusive lock on %s", + path_.c_str()); + sqlite3_close(hDB_); + hDB_ = nullptr; + return false; + } + pj_log(ctx_, PJ_LOG_TRACE, "Lock taken on cache. Waiting a bit..."); + // Retry every 5 ms for 50 ms, then every 10 ms for 100 ms, then + // every 100 ms + sleep_ms(i < 10 ? 5 : i < 20 ? 10 : 100); + } + char **pasResult = nullptr; + int nRows = 0; + int nCols = 0; + sqlite3_get_table(hDB_, + "SELECT 1 FROM sqlite_master WHERE name = 'properties'", + &pasResult, &nRows, &nCols, nullptr); + sqlite3_free_table(pasResult); + if (nRows == 0) { + if (!createDBStructure()) { + sqlite3_close(hDB_); + hDB_ = nullptr; + return false; + } + } + + if (getenv("PROJ_CHECK_CACHE_CONSISTENCY")) { + checkConsistency(); + } + return true; +} + +// --------------------------------------------------------------------------- + +static const char *cache_db_structure_sql = + "CREATE TABLE properties(" + " url TEXT PRIMARY KEY NOT NULL," + " lastChecked TIMESTAMP NOT NULL," + " fileSize INTEGER NOT NULL," + " lastModified TEXT," + " etag TEXT" + ");" + "CREATE TABLE downloaded_file_properties(" + " url TEXT PRIMARY KEY NOT NULL," + " lastChecked TIMESTAMP NOT NULL," + " fileSize INTEGER NOT NULL," + " lastModified TEXT," + " etag TEXT" + ");" + "CREATE TABLE chunk_data(" + " id INTEGER PRIMARY KEY AUTOINCREMENT CHECK (id > 0)," + " data BLOB NOT NULL" + ");" + "CREATE TABLE chunks(" + " id INTEGER PRIMARY KEY AUTOINCREMENT CHECK (id > 0)," + " url TEXT NOT NULL," + " offset INTEGER NOT NULL," + " data_id INTEGER NOT NULL," + " data_size INTEGER NOT NULL," + " CONSTRAINT fk_chunks_url FOREIGN KEY (url) REFERENCES properties(url)," + " CONSTRAINT fk_chunks_data FOREIGN KEY (data_id) REFERENCES chunk_data(id)" + ");" + "CREATE INDEX idx_chunks ON chunks(url, offset);" + "CREATE TABLE linked_chunks(" + " id INTEGER PRIMARY KEY AUTOINCREMENT CHECK (id > 0)," + " chunk_id INTEGER NOT NULL," + " prev INTEGER," + " next INTEGER," + " CONSTRAINT fk_links_chunkid FOREIGN KEY (chunk_id) REFERENCES chunks(id)," + " CONSTRAINT fk_links_prev FOREIGN KEY (prev) REFERENCES linked_chunks(id)," + " CONSTRAINT fk_links_next FOREIGN KEY (next) REFERENCES linked_chunks(id)" + ");" + "CREATE INDEX idx_linked_chunks_chunk_id ON linked_chunks(chunk_id);" + "CREATE TABLE linked_chunks_head_tail(" + " head INTEGER," + " tail INTEGER," + " CONSTRAINT lht_head FOREIGN KEY (head) REFERENCES linked_chunks(id)," + " CONSTRAINT lht_tail FOREIGN KEY (tail) REFERENCES linked_chunks(id)" + ");" + "INSERT INTO linked_chunks_head_tail VALUES (NULL, NULL);"; + +bool DiskChunkCache::createDBStructure() { + + pj_log(ctx_, PJ_LOG_TRACE, "Creating cache DB structure"); + if (sqlite3_exec(hDB_, cache_db_structure_sql, nullptr, nullptr, nullptr) != + SQLITE_OK) { + pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); + return false; + } + return true; +} + +// --------------------------------------------------------------------------- + +// Used by checkConsistency() and insert() +#define INVALIDATED_SQL_LITERAL "'invalidated'" + +bool DiskChunkCache::checkConsistency() { + + auto stmt = prepare("SELECT * FROM chunk_data WHERE id NOT IN (SELECT " + "data_id FROM chunks)"); + if (!stmt) { + return false; + } + if (stmt->execute() != SQLITE_DONE) { + fprintf(stderr, "Rows in chunk_data not referenced by chunks.\n"); + return false; + } + + stmt = prepare("SELECT * FROM chunks WHERE id NOT IN (SELECT chunk_id FROM " + "linked_chunks)"); + if (!stmt) { + return false; + } + if (stmt->execute() != SQLITE_DONE) { + fprintf(stderr, "Rows in chunks not referenced by linked_chunks.\n"); + return false; + } + + stmt = prepare("SELECT * FROM chunks WHERE url <> " INVALIDATED_SQL_LITERAL + " AND url " + "NOT IN (SELECT url FROM properties)"); + if (!stmt) { + return false; + } + if (stmt->execute() != SQLITE_DONE) { + fprintf(stderr, "url values in chunks not referenced by properties.\n"); + return false; + } + + stmt = prepare("SELECT head, tail FROM linked_chunks_head_tail"); + if (!stmt) { + return false; + } + if (stmt->execute() != SQLITE_ROW) { + fprintf(stderr, "linked_chunks_head_tail empty.\n"); + return false; + } + const auto head = stmt->getInt64(); + const auto tail = stmt->getInt64(); + if (stmt->execute() != SQLITE_DONE) { + fprintf(stderr, "linked_chunks_head_tail has more than one row.\n"); + return false; + } + + stmt = prepare("SELECT COUNT(*) FROM linked_chunks"); + if (!stmt) { + return false; + } + if (stmt->execute() != SQLITE_ROW) { + fprintf(stderr, "linked_chunks_head_tail empty.\n"); + return false; + } + const auto count_linked_chunks = stmt->getInt64(); + + if (head) { + auto id = head; + std::set<sqlite3_int64> visitedIds; + stmt = prepare("SELECT next FROM linked_chunks WHERE id = ?"); + if (!stmt) { + return false; + } + while (true) { + visitedIds.insert(id); + stmt->reset(); + stmt->bindInt64(id); + if (stmt->execute() != SQLITE_ROW) { + fprintf(stderr, "cannot find linked_chunks.id = %d.\n", + static_cast<int>(id)); + return false; + } + auto next = stmt->getInt64(); + if (next == 0) { + if (id != tail) { + fprintf(stderr, + "last item when following next is not tail.\n"); + return false; + } + break; + } + if (visitedIds.find(next) != visitedIds.end()) { + fprintf(stderr, "found cycle on linked_chunks.next = %d.\n", + static_cast<int>(next)); + return false; + } + id = next; + } + if (visitedIds.size() != static_cast<size_t>(count_linked_chunks)) { + fprintf(stderr, + "ghost items in linked_chunks when following next.\n"); + return false; + } + } else if (count_linked_chunks) { + fprintf(stderr, "linked_chunks_head_tail.head = NULL but linked_chunks " + "not empty.\n"); + return false; + } + + if (tail) { + auto id = tail; + std::set<sqlite3_int64> visitedIds; + stmt = prepare("SELECT prev FROM linked_chunks WHERE id = ?"); + if (!stmt) { + return false; + } + while (true) { + visitedIds.insert(id); + stmt->reset(); + stmt->bindInt64(id); + if (stmt->execute() != SQLITE_ROW) { + fprintf(stderr, "cannot find linked_chunks.id = %d.\n", + static_cast<int>(id)); + return false; + } + auto prev = stmt->getInt64(); + if (prev == 0) { + if (id != head) { + fprintf(stderr, + "last item when following prev is not head.\n"); + return false; + } + break; + } + if (visitedIds.find(prev) != visitedIds.end()) { + fprintf(stderr, "found cycle on linked_chunks.prev = %d.\n", + static_cast<int>(prev)); + return false; + } + id = prev; + } + if (visitedIds.size() != static_cast<size_t>(count_linked_chunks)) { + fprintf(stderr, + "ghost items in linked_chunks when following prev.\n"); + return false; + } + } else if (count_linked_chunks) { + fprintf(stderr, "linked_chunks_head_tail.tail = NULL but linked_chunks " + "not empty.\n"); + return false; + } + + fprintf(stderr, "check ok\n"); + return true; +} + +// --------------------------------------------------------------------------- + +void DiskChunkCache::commitAndClose() { + if (hDB_) { + if( sqlite3_exec(hDB_, "COMMIT", nullptr, nullptr, nullptr) != SQLITE_OK ) { + pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); + } + sqlite3_close(hDB_); + hDB_ = nullptr; + } +} + +// --------------------------------------------------------------------------- + +DiskChunkCache::~DiskChunkCache() { + commitAndClose(); +} + +// --------------------------------------------------------------------------- + +void DiskChunkCache::closeAndUnlink() { + commitAndClose(); + if (vfs_) { + vfs_->raw()->xDelete(vfs_->raw(), path_.c_str(), 0); + } +} + +// --------------------------------------------------------------------------- + +std::unique_ptr<SQLiteStatement> DiskChunkCache::prepare(const char *sql) { + sqlite3_stmt *hStmt = nullptr; + sqlite3_prepare_v2(hDB_, sql, -1, &hStmt, nullptr); + if (!hStmt) { + pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); + return nullptr; + } + return std::unique_ptr<SQLiteStatement>(new SQLiteStatement(hStmt)); +} + +// --------------------------------------------------------------------------- + +bool DiskChunkCache::get_links(sqlite3_int64 chunk_id, sqlite3_int64 &link_id, + sqlite3_int64 &prev, sqlite3_int64 &next, + sqlite3_int64 &head, sqlite3_int64 &tail) { + auto stmt = + prepare("SELECT id, prev, next FROM linked_chunks WHERE chunk_id = ?"); + if (!stmt) + return false; + stmt->bindInt64(chunk_id); + { + const auto ret = stmt->execute(); + if (ret != SQLITE_ROW) { + pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); + return false; + } + } + link_id = stmt->getInt64(); + prev = stmt->getInt64(); + next = stmt->getInt64(); + + stmt = prepare("SELECT head, tail FROM linked_chunks_head_tail"); + { + const auto ret = stmt->execute(); + if (ret != SQLITE_ROW) { + pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); + return false; + } + } + head = stmt->getInt64(); + tail = stmt->getInt64(); + return true; +} + +// --------------------------------------------------------------------------- + +bool DiskChunkCache::update_links_of_prev_and_next_links(sqlite3_int64 prev, + sqlite3_int64 next) { + if (prev) { + auto stmt = prepare("UPDATE linked_chunks SET next = ? WHERE id = ?"); + if (!stmt) + return false; + if (next) + stmt->bindInt64(next); + else + stmt->bindNull(); + stmt->bindInt64(prev); + const auto ret = stmt->execute(); + if (ret != SQLITE_DONE) { + pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); + return false; + } + } + + if (next) { + auto stmt = prepare("UPDATE linked_chunks SET prev = ? WHERE id = ?"); + if (!stmt) + return false; + if (prev) + stmt->bindInt64(prev); + else + stmt->bindNull(); + stmt->bindInt64(next); + const auto ret = stmt->execute(); + if (ret != SQLITE_DONE) { + pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); + return false; + } + } + return true; +} + +// --------------------------------------------------------------------------- + +bool DiskChunkCache::update_linked_chunks(sqlite3_int64 link_id, + sqlite3_int64 prev, + sqlite3_int64 next) { + auto stmt = + prepare("UPDATE linked_chunks SET prev = ?, next = ? WHERE id = ?"); + if (!stmt) + return false; + if (prev) + stmt->bindInt64(prev); + else + stmt->bindNull(); + if (next) + stmt->bindInt64(next); + else + stmt->bindNull(); + stmt->bindInt64(link_id); + const auto ret = stmt->execute(); + if (ret != SQLITE_DONE) { + pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); + return false; + } + return true; +} + +// --------------------------------------------------------------------------- + +bool DiskChunkCache::update_linked_chunks_head_tail(sqlite3_int64 head, + sqlite3_int64 tail) { + auto stmt = + prepare("UPDATE linked_chunks_head_tail SET head = ?, tail = ?"); + if (!stmt) + return false; + if (head) + stmt->bindInt64(head); + else + stmt->bindNull(); // shouldn't happen normally + if (tail) + stmt->bindInt64(tail); + else + stmt->bindNull(); // shouldn't happen normally + const auto ret = stmt->execute(); + if (ret != SQLITE_DONE) { + pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); + return false; + } + return true; +} + +// --------------------------------------------------------------------------- + +bool DiskChunkCache::move_to_head(sqlite3_int64 chunk_id) { + + sqlite3_int64 link_id = 0; + sqlite3_int64 prev = 0; + sqlite3_int64 next = 0; + sqlite3_int64 head = 0; + sqlite3_int64 tail = 0; + if (!get_links(chunk_id, link_id, prev, next, head, tail)) { + return false; + } + + if (link_id == head) { + return true; + } + + if (!update_links_of_prev_and_next_links(prev, next)) { + return false; + } + + if (head) { + auto stmt = prepare("UPDATE linked_chunks SET prev = ? WHERE id = ?"); + if (!stmt) + return false; + stmt->bindInt64(link_id); + stmt->bindInt64(head); + const auto ret = stmt->execute(); + if (ret != SQLITE_DONE) { + pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); + return false; + } + } + + return update_linked_chunks(link_id, 0, head) && + update_linked_chunks_head_tail(link_id, + (link_id == tail) ? prev : tail); +} + +// --------------------------------------------------------------------------- + +bool DiskChunkCache::move_to_tail(sqlite3_int64 chunk_id) { + sqlite3_int64 link_id = 0; + sqlite3_int64 prev = 0; + sqlite3_int64 next = 0; + sqlite3_int64 head = 0; + sqlite3_int64 tail = 0; + if (!get_links(chunk_id, link_id, prev, next, head, tail)) { + return false; + } + + if (link_id == tail) { + return true; + } + + if (!update_links_of_prev_and_next_links(prev, next)) { + return false; + } + + if (tail) { + auto stmt = prepare("UPDATE linked_chunks SET next = ? WHERE id = ?"); + if (!stmt) + return false; + stmt->bindInt64(link_id); + stmt->bindInt64(tail); + const auto ret = stmt->execute(); + if (ret != SQLITE_DONE) { + pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_)); + return false; + } + } + + return update_linked_chunks(link_id, tail, 0) && + update_linked_chunks_head_tail((link_id == head) ? next : head, + link_id); +} + +// --------------------------------------------------------------------------- + +void NetworkChunkCache::insert(PJ_CONTEXT *ctx, const std::string &url, + unsigned long long chunkIdx, + std::vector<unsigned char> &&data) { + auto dataPtr(std::make_shared<std::vector<unsigned char>>(std::move(data))); + cache_.insert(Key(url, chunkIdx), dataPtr); + + auto diskCache = DiskChunkCache::open(ctx); + if (!diskCache) + return; + auto hDB = diskCache->handle(); + + // Always insert DOWNLOAD_CHUNK_SIZE bytes to avoid fragmentation + std::vector<unsigned char> blob(*dataPtr); + assert(blob.size() <= DOWNLOAD_CHUNK_SIZE); + blob.resize(DOWNLOAD_CHUNK_SIZE); + + // Check if there is an existing entry for that URL and offset + auto stmt = diskCache->prepare( + "SELECT id, data_id FROM chunks WHERE url = ? AND offset = ?"); + if (!stmt) + return; + stmt->bindText(url.c_str()); + stmt->bindInt64(chunkIdx * DOWNLOAD_CHUNK_SIZE); + + const auto mainRet = stmt->execute(); + if (mainRet == SQLITE_ROW) { + const auto chunk_id = stmt->getInt64(); + const auto data_id = stmt->getInt64(); + stmt = + diskCache->prepare("UPDATE chunk_data SET data = ? WHERE id = ?"); + if (!stmt) + return; + stmt->bindBlob(blob.data(), blob.size()); + stmt->bindInt64(data_id); + { + const auto ret = stmt->execute(); + if (ret != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + } + + diskCache->move_to_head(chunk_id); + + return; + } else if (mainRet != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + + // Lambda to recycle an existing entry that was either invalidated, or + // least recently used. + const auto reuseExistingEntry = [ctx, &blob, &diskCache, hDB, &url, + chunkIdx, &dataPtr]( + std::unique_ptr<SQLiteStatement> &stmtIn) { + const auto chunk_id = stmtIn->getInt64(); + const auto data_id = stmtIn->getInt64(); + if (data_id <= 0) { + pj_log(ctx, PJ_LOG_ERROR, "data_id <= 0"); + return; + } + + auto l_stmt = + diskCache->prepare("UPDATE chunk_data SET data = ? WHERE id = ?"); + if (!l_stmt) + return; + l_stmt->bindBlob(blob.data(), blob.size()); + l_stmt->bindInt64(data_id); + { + const auto ret2 = l_stmt->execute(); + if (ret2 != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + } + + l_stmt = diskCache->prepare("UPDATE chunks SET url = ?, " + "offset = ?, data_size = ?, data_id = ? " + "WHERE id = ?"); + if (!l_stmt) + return; + l_stmt->bindText(url.c_str()); + l_stmt->bindInt64(chunkIdx * DOWNLOAD_CHUNK_SIZE); + l_stmt->bindInt64(dataPtr->size()); + l_stmt->bindInt64(data_id); + l_stmt->bindInt64(chunk_id); + { + const auto ret2 = l_stmt->execute(); + if (ret2 != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + } + + diskCache->move_to_head(chunk_id); + }; + + // Find if there is an invalidated chunk we can reuse + stmt = diskCache->prepare( + "SELECT id, data_id FROM chunks " + "WHERE id = (SELECT tail FROM linked_chunks_head_tail) AND " + "url = " INVALIDATED_SQL_LITERAL); + if (!stmt) + return; + { + const auto ret = stmt->execute(); + if (ret == SQLITE_ROW) { + reuseExistingEntry(stmt); + return; + } else if (ret != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + } + + // Check if we have not reached the max size of the cache + stmt = diskCache->prepare("SELECT COUNT(*) FROM chunks"); + if (!stmt) + return; + { + const auto ret = stmt->execute(); + if (ret != SQLITE_ROW) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + } + + const auto max_size = pj_context_get_grid_cache_max_size(ctx); + if (max_size > 0 && + static_cast<long long>(stmt->getInt64() * DOWNLOAD_CHUNK_SIZE) >= + max_size) { + stmt = diskCache->prepare( + "SELECT id, data_id FROM chunks " + "WHERE id = (SELECT tail FROM linked_chunks_head_tail)"); + if (!stmt) + return; + + const auto ret = stmt->execute(); + if (ret != SQLITE_ROW) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + reuseExistingEntry(stmt); + return; + } + + // Otherwise just append a new entry + stmt = diskCache->prepare("INSERT INTO chunk_data(data) VALUES (?)"); + if (!stmt) + return; + stmt->bindBlob(blob.data(), blob.size()); + { + const auto ret = stmt->execute(); + if (ret != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + } + + const auto chunk_data_id = sqlite3_last_insert_rowid(hDB); + + stmt = diskCache->prepare("INSERT INTO chunks(url, offset, data_id, " + "data_size) VALUES (?,?,?,?)"); + if (!stmt) + return; + stmt->bindText(url.c_str()); + stmt->bindInt64(chunkIdx * DOWNLOAD_CHUNK_SIZE); + stmt->bindInt64(chunk_data_id); + stmt->bindInt64(dataPtr->size()); + { + const auto ret = stmt->execute(); + if (ret != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + } + + const auto chunk_id = sqlite3_last_insert_rowid(hDB); + + stmt = diskCache->prepare( + "INSERT INTO linked_chunks(chunk_id, prev, next) VALUES (?,NULL,NULL)"); + if (!stmt) + return; + stmt->bindInt64(chunk_id); + if (stmt->execute() != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + + stmt = diskCache->prepare("SELECT head FROM linked_chunks_head_tail"); + if (!stmt) + return; + if (stmt->execute() != SQLITE_ROW) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + if (stmt->getInt64() == 0) { + stmt = diskCache->prepare( + "UPDATE linked_chunks_head_tail SET head = ?, tail = ?"); + if (!stmt) + return; + stmt->bindInt64(chunk_id); + stmt->bindInt64(chunk_id); + if (stmt->execute() != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + } + + diskCache->move_to_head(chunk_id); +} + +// --------------------------------------------------------------------------- + +std::shared_ptr<std::vector<unsigned char>> +NetworkChunkCache::get(PJ_CONTEXT *ctx, const std::string &url, + unsigned long long chunkIdx) { + std::shared_ptr<std::vector<unsigned char>> ret; + if (cache_.tryGet(Key(url, chunkIdx), ret)) { + return ret; + } + + auto diskCache = DiskChunkCache::open(ctx); + if (!diskCache) + return ret; + auto hDB = diskCache->handle(); + + auto stmt = diskCache->prepare( + "SELECT chunks.id, chunks.data_size, chunk_data.data FROM chunks " + "JOIN chunk_data ON chunks.id = chunk_data.id " + "WHERE chunks.url = ? AND chunks.offset = ?"); + if (!stmt) + return ret; + + stmt->bindText(url.c_str()); + stmt->bindInt64(chunkIdx * DOWNLOAD_CHUNK_SIZE); + + const auto mainRet = stmt->execute(); + if (mainRet == SQLITE_ROW) { + const auto chunk_id = stmt->getInt64(); + const auto data_size = stmt->getInt64(); + int blob_size = 0; + const void *blob = stmt->getBlob(blob_size); + if (blob_size < data_size) { + pj_log(ctx, PJ_LOG_ERROR, + "blob_size=%d < data_size for chunk_id=%d", blob_size, + static_cast<int>(chunk_id)); + return ret; + } + if (data_size > static_cast<sqlite3_int64>(DOWNLOAD_CHUNK_SIZE)) { + pj_log(ctx, PJ_LOG_ERROR, "data_size > DOWNLOAD_CHUNK_SIZE"); + return ret; + } + ret.reset(new std::vector<unsigned char>()); + ret->assign(reinterpret_cast<const unsigned char *>(blob), + reinterpret_cast<const unsigned char *>(blob) + + static_cast<size_t>(data_size)); + cache_.insert(Key(url, chunkIdx), ret); + + if (!diskCache->move_to_head(chunk_id)) + return ret; + } else if (mainRet != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + } + + return ret; +} + +// --------------------------------------------------------------------------- + +std::shared_ptr<std::vector<unsigned char>> +NetworkChunkCache::get(PJ_CONTEXT *ctx, const std::string &url, + unsigned long long chunkIdx, FileProperties &props) { + if (!gNetworkFileProperties.tryGet(ctx, url, props)) { + return nullptr; + } + + return get(ctx, url, chunkIdx); +} + +// --------------------------------------------------------------------------- + +void NetworkChunkCache::clearMemoryCache() { cache_.clear(); } + +// --------------------------------------------------------------------------- + +void NetworkChunkCache::clearDiskChunkCache(PJ_CONTEXT *ctx) { + auto diskCache = DiskChunkCache::open(ctx); + if (!diskCache) + return; + diskCache->closeAndUnlink(); +} + +// --------------------------------------------------------------------------- + +void NetworkFilePropertiesCache::insert(PJ_CONTEXT *ctx, const std::string &url, + FileProperties &props) { + time(&props.lastChecked); + cache_.insert(url, props); + + auto diskCache = DiskChunkCache::open(ctx); + if (!diskCache) + return; + auto hDB = diskCache->handle(); + auto stmt = diskCache->prepare("SELECT fileSize, lastModified, etag " + "FROM properties WHERE url = ?"); + if (!stmt) + return; + stmt->bindText(url.c_str()); + if (stmt->execute() == SQLITE_ROW) { + FileProperties cachedProps; + cachedProps.size = stmt->getInt64(); + const char *lastModified = stmt->getText(); + cachedProps.lastModified = lastModified ? lastModified : std::string(); + const char *etag = stmt->getText(); + cachedProps.etag = etag ? etag : std::string(); + if (props.size != cachedProps.size || + props.lastModified != cachedProps.lastModified || + props.etag != cachedProps.etag) { + + // If cached properties don't match recent fresh ones, invalidate + // cached chunks + stmt = diskCache->prepare("SELECT id FROM chunks WHERE url = ?"); + if (!stmt) + return; + stmt->bindText(url.c_str()); + std::vector<sqlite3_int64> ids; + while (stmt->execute() == SQLITE_ROW) { + ids.emplace_back(stmt->getInt64()); + stmt->resetResIndex(); + } + + for (const auto id : ids) { + diskCache->move_to_tail(id); + } + + stmt = diskCache->prepare( + "UPDATE chunks SET url = " INVALIDATED_SQL_LITERAL ", " + "offset = -1, data_size = 0 WHERE url = ?"); + if (!stmt) + return; + stmt->bindText(url.c_str()); + if (stmt->execute() != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + } + + stmt = diskCache->prepare("UPDATE properties SET lastChecked = ?, " + "fileSize = ?, lastModified = ?, etag = ? " + "WHERE url = ?"); + if (!stmt) + return; + stmt->bindInt64(props.lastChecked); + stmt->bindInt64(props.size); + if (props.lastModified.empty()) + stmt->bindNull(); + else + stmt->bindText(props.lastModified.c_str()); + if (props.etag.empty()) + stmt->bindNull(); + else + stmt->bindText(props.etag.c_str()); + stmt->bindText(url.c_str()); + if (stmt->execute() != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + } else { + stmt = diskCache->prepare("INSERT INTO properties (url, lastChecked, " + "fileSize, lastModified, etag) VALUES " + "(?,?,?,?,?)"); + if (!stmt) + return; + stmt->bindText(url.c_str()); + stmt->bindInt64(props.lastChecked); + stmt->bindInt64(props.size); + if (props.lastModified.empty()) + stmt->bindNull(); + else + stmt->bindText(props.lastModified.c_str()); + if (props.etag.empty()) + stmt->bindNull(); + else + stmt->bindText(props.etag.c_str()); + if (stmt->execute() != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return; + } + } +} + +// --------------------------------------------------------------------------- + +bool NetworkFilePropertiesCache::tryGet(PJ_CONTEXT *ctx, const std::string &url, + FileProperties &props) { + if (cache_.tryGet(url, props)) { + return true; + } + + auto diskCache = DiskChunkCache::open(ctx); + if (!diskCache) + return false; + auto stmt = + diskCache->prepare("SELECT lastChecked, fileSize, lastModified, etag " + "FROM properties WHERE url = ?"); + if (!stmt) + return false; + stmt->bindText(url.c_str()); + if (stmt->execute() != SQLITE_ROW) { + return false; + } + props.lastChecked = stmt->getInt64(); + props.size = stmt->getInt64(); + const char *lastModified = stmt->getText(); + props.lastModified = lastModified ? lastModified : std::string(); + const char *etag = stmt->getText(); + props.etag = etag ? etag : std::string(); + + const auto ttl = pj_context_get_grid_cache_ttl(ctx); + if (ttl > 0) { + time_t curTime; + time(&curTime); + if (curTime > props.lastChecked + ttl) { + props = FileProperties(); + return false; + } + } + cache_.insert(url, props); + return true; +} + +// --------------------------------------------------------------------------- + +void NetworkFilePropertiesCache::clearMemoryCache() { cache_.clear(); } + +// --------------------------------------------------------------------------- + +class NetworkFile : public File { + PJ_CONTEXT *m_ctx; + std::string m_url; + PROJ_NETWORK_HANDLE *m_handle; + unsigned long long m_pos = 0; + size_t m_nBlocksToDownload = 1; + unsigned long long m_lastDownloadedOffset; + FileProperties m_props; + proj_network_close_cbk_type m_closeCbk; + bool m_hasChanged = false; + + NetworkFile(const NetworkFile &) = delete; + NetworkFile &operator=(const NetworkFile &) = delete; + + protected: + NetworkFile(PJ_CONTEXT *ctx, const std::string &url, + PROJ_NETWORK_HANDLE *handle, + unsigned long long lastDownloadOffset, + const FileProperties &props) + : File(url), m_ctx(ctx), m_url(url), m_handle(handle), + m_lastDownloadedOffset(lastDownloadOffset), m_props(props), + m_closeCbk(ctx->networking.close) {} + + public: + ~NetworkFile() override; + + size_t read(void *buffer, size_t sizeBytes) override; + size_t write(const void *, size_t) override { return 0; } + bool seek(unsigned long long offset, int whence) override; + unsigned long long tell() override; + void reassign_context(PJ_CONTEXT *ctx) override; + bool hasChanged() const override { return m_hasChanged; } + + static std::unique_ptr<File> open(PJ_CONTEXT *ctx, const char *filename); + + static bool get_props_from_headers(PJ_CONTEXT *ctx, + PROJ_NETWORK_HANDLE *handle, + FileProperties &props); +}; + +// --------------------------------------------------------------------------- + +bool NetworkFile::get_props_from_headers(PJ_CONTEXT *ctx, + PROJ_NETWORK_HANDLE *handle, + FileProperties &props) { + const char *contentRange = ctx->networking.get_header_value( + ctx, handle, "Content-Range", ctx->networking.user_data); + if (contentRange) { + const char *slash = strchr(contentRange, '/'); + if (slash) { + props.size = std::stoull(slash + 1); + + const char *lastModified = ctx->networking.get_header_value( + ctx, handle, "Last-Modified", ctx->networking.user_data); + if (lastModified) + props.lastModified = lastModified; + + const char *etag = ctx->networking.get_header_value( + ctx, handle, "ETag", ctx->networking.user_data); + if (etag) + props.etag = etag; + + return true; + } + } + return false; +} + +// --------------------------------------------------------------------------- + +std::unique_ptr<File> NetworkFile::open(PJ_CONTEXT *ctx, const char *filename) { + FileProperties props; + if (gNetworkChunkCache.get(ctx, filename, 0, props)) { + return std::unique_ptr<File>(new NetworkFile( + ctx, filename, nullptr, + std::numeric_limits<unsigned long long>::max(), props)); + } else { + std::vector<unsigned char> buffer(DOWNLOAD_CHUNK_SIZE); + size_t size_read = 0; + std::string errorBuffer; + errorBuffer.resize(1024); + + auto handle = ctx->networking.open( + ctx, filename, 0, buffer.size(), &buffer[0], &size_read, + errorBuffer.size(), &errorBuffer[0], ctx->networking.user_data); + buffer.resize(size_read); + if (!handle) { + errorBuffer.resize(strlen(errorBuffer.data())); + pj_log(ctx, PJ_LOG_ERROR, "Cannot open %s: %s", filename, + errorBuffer.c_str()); + } + + bool ok = false; + if (handle) { + if (get_props_from_headers(ctx, handle, props)) { + ok = true; + gNetworkFileProperties.insert(ctx, filename, props); + gNetworkChunkCache.insert(ctx, filename, 0, std::move(buffer)); + } + } + + return std::unique_ptr<File>( + ok ? new NetworkFile(ctx, filename, handle, size_read, props) + : nullptr); + } +} + +// --------------------------------------------------------------------------- + +std::unique_ptr<File> pj_network_file_open(PJ_CONTEXT* ctx, const char* filename) { + return NetworkFile::open(ctx, filename); +} + +// --------------------------------------------------------------------------- + +size_t NetworkFile::read(void *buffer, size_t sizeBytes) { + + if (sizeBytes == 0) + return 0; + + auto iterOffset = m_pos; + while (sizeBytes) { + const auto chunkIdxToDownload = iterOffset / DOWNLOAD_CHUNK_SIZE; + const auto offsetToDownload = chunkIdxToDownload * DOWNLOAD_CHUNK_SIZE; + std::vector<unsigned char> region; + auto pChunk = gNetworkChunkCache.get(m_ctx, m_url, chunkIdxToDownload); + if (pChunk != nullptr) { + region = *pChunk; + } else { + if (offsetToDownload == m_lastDownloadedOffset) { + // In case of consecutive reads (of small size), we use a + // heuristic that we will read the file sequentially, so + // we double the requested size to decrease the number of + // client/server roundtrips. + if (m_nBlocksToDownload < 100) + m_nBlocksToDownload *= 2; + } else { + // Random reads. Cancel the above heuristics. + m_nBlocksToDownload = 1; + } + + // Ensure that we will request at least the number of blocks + // to satisfy the remaining buffer size to read. + const auto endOffsetToDownload = + ((iterOffset + sizeBytes + DOWNLOAD_CHUNK_SIZE - 1) / + DOWNLOAD_CHUNK_SIZE) * + DOWNLOAD_CHUNK_SIZE; + const auto nMinBlocksToDownload = static_cast<size_t>( + (endOffsetToDownload - offsetToDownload) / DOWNLOAD_CHUNK_SIZE); + if (m_nBlocksToDownload < nMinBlocksToDownload) + m_nBlocksToDownload = nMinBlocksToDownload; + + // Avoid reading already cached data. + // Note: this might get evicted if concurrent reads are done, but + // this should not cause bugs. Just missed optimization. + for (size_t i = 1; i < m_nBlocksToDownload; i++) { + if (gNetworkChunkCache.get(m_ctx, m_url, + chunkIdxToDownload + i) != nullptr) { + m_nBlocksToDownload = i; + break; + } + } + + if (m_nBlocksToDownload > MAX_CHUNKS) + m_nBlocksToDownload = MAX_CHUNKS; + + region.resize(m_nBlocksToDownload * DOWNLOAD_CHUNK_SIZE); + size_t nRead = 0; + std::string errorBuffer; + errorBuffer.resize(1024); + if (!m_handle) { + m_handle = m_ctx->networking.open( + m_ctx, m_url.c_str(), offsetToDownload, + m_nBlocksToDownload * DOWNLOAD_CHUNK_SIZE, ®ion[0], + &nRead, errorBuffer.size(), &errorBuffer[0], + m_ctx->networking.user_data); + if (!m_handle) { + return 0; + } + } else { + nRead = m_ctx->networking.read_range( + m_ctx, m_handle, offsetToDownload, + m_nBlocksToDownload * DOWNLOAD_CHUNK_SIZE, ®ion[0], + errorBuffer.size(), &errorBuffer[0], + m_ctx->networking.user_data); + } + if (nRead == 0) { + errorBuffer.resize(strlen(errorBuffer.data())); + if (!errorBuffer.empty()) { + pj_log(m_ctx, PJ_LOG_ERROR, "Cannot read in %s: %s", + m_url.c_str(), errorBuffer.c_str()); + } + return 0; + } + + if (!m_hasChanged) { + FileProperties props; + if (get_props_from_headers(m_ctx, m_handle, props)) { + if (props.size != m_props.size || + props.lastModified != m_props.lastModified || + props.etag != m_props.etag) { + gNetworkFileProperties.insert(m_ctx, m_url, props); + gNetworkChunkCache.clearMemoryCache(); + m_hasChanged = true; + } + } + } + + region.resize(nRead); + m_lastDownloadedOffset = offsetToDownload + nRead; + + const auto nChunks = + (region.size() + DOWNLOAD_CHUNK_SIZE - 1) / DOWNLOAD_CHUNK_SIZE; + for (size_t i = 0; i < nChunks; i++) { + std::vector<unsigned char> chunk( + region.data() + i * DOWNLOAD_CHUNK_SIZE, + region.data() + + std::min((i + 1) * DOWNLOAD_CHUNK_SIZE, region.size())); + gNetworkChunkCache.insert(m_ctx, m_url, chunkIdxToDownload + i, + std::move(chunk)); + } + } + const size_t nToCopy = static_cast<size_t>( + std::min(static_cast<unsigned long long>(sizeBytes), + region.size() - (iterOffset - offsetToDownload))); + memcpy(buffer, region.data() + iterOffset - offsetToDownload, nToCopy); + buffer = static_cast<char *>(buffer) + nToCopy; + iterOffset += nToCopy; + sizeBytes -= nToCopy; + if (region.size() < static_cast<size_t>(DOWNLOAD_CHUNK_SIZE) && + sizeBytes != 0) { + break; + } + } + + size_t nRead = static_cast<size_t>(iterOffset - m_pos); + m_pos = iterOffset; + return nRead; +} + +// --------------------------------------------------------------------------- + +bool NetworkFile::seek(unsigned long long offset, int whence) { + if (whence == SEEK_SET) { + m_pos = offset; + } else if (whence == SEEK_CUR) { + m_pos += offset; + } else { + if (offset != 0) + return false; + m_pos = m_props.size; + } + return true; +} + +// --------------------------------------------------------------------------- + +unsigned long long NetworkFile::tell() { return m_pos; } + +// --------------------------------------------------------------------------- + +NetworkFile::~NetworkFile() { + if (m_handle) { + m_ctx->networking.close(m_ctx, m_handle, m_ctx->networking.user_data); + } +} + +// --------------------------------------------------------------------------- + +void NetworkFile::reassign_context(PJ_CONTEXT *ctx) { + m_ctx = ctx; + if (m_closeCbk != m_ctx->networking.close) { + pj_log(m_ctx, PJ_LOG_ERROR, + "Networking close callback has changed following context " + "reassignment ! This is highly suspicious"); + } +} + +// --------------------------------------------------------------------------- + +#ifdef CURL_ENABLED + +struct CurlFileHandle { + std::string m_url; + CURL *m_handle; + std::string m_headers{}; + std::string m_lastval{}; + std::string m_useragent{}; + char m_szCurlErrBuf[CURL_ERROR_SIZE + 1] = {}; + + CurlFileHandle(const CurlFileHandle &) = delete; + CurlFileHandle &operator=(const CurlFileHandle &) = delete; + + explicit CurlFileHandle(const char *url, CURL *handle); + ~CurlFileHandle(); + + static PROJ_NETWORK_HANDLE * + open(PJ_CONTEXT *, const char *url, unsigned long long offset, + size_t size_to_read, void *buffer, size_t *out_size_read, + size_t error_string_max_size, char *out_error_string, void *); +}; + +// --------------------------------------------------------------------------- + +static std::string GetExecutableName() { +#if defined(__linux) + std::string path; + path.resize(1024); + const auto ret = readlink("/proc/self/exe", &path[0], path.size()); + if (ret > 0) { + path.resize(ret); + const auto pos = path.rfind('/'); + if (pos != std::string::npos) { + path = path.substr(pos + 1); + } + return path; + } +#elif defined(_WIN32) + std::string path; + path.resize(1024); + if (GetModuleFileNameA(nullptr, &path[0], + static_cast<DWORD>(path.size()))) { + path.resize(strlen(path.c_str())); + const auto pos = path.rfind('\\'); + if (pos != std::string::npos) { + path = path.substr(pos + 1); + } + return path; + } +#elif defined(__MACH__) && defined(__APPLE__) + std::string path; + path.resize(1024); + uint32_t size = static_cast<uint32_t>(path.size()); + if (_NSGetExecutablePath(&path[0], &size) == 0) { + path.resize(strlen(path.c_str())); + const auto pos = path.rfind('/'); + if (pos != std::string::npos) { + path = path.substr(pos + 1); + } + return path; + } +#elif defined(__FreeBSD__) + int mib[4]; + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PATHNAME; + mib[3] = -1; + std::string path; + path.resize(1024); + size_t size = path.size(); + if (sysctl(mib, 4, &path[0], &size, nullptr, 0) == 0) { + path.resize(strlen(path.c_str())); + const auto pos = path.rfind('/'); + if (pos != std::string::npos) { + path = path.substr(pos + 1); + } + return path; + } +#endif + + return std::string(); +} + +// --------------------------------------------------------------------------- + +CurlFileHandle::CurlFileHandle(const char *url, CURL *handle) + : m_url(url), m_handle(handle) { + curl_easy_setopt(handle, CURLOPT_URL, m_url.c_str()); + + if (getenv("PROJ_CURL_VERBOSE")) + curl_easy_setopt(handle, CURLOPT_VERBOSE, 1); + +// CURLOPT_SUPPRESS_CONNECT_HEADERS is defined in curl 7.54.0 or newer. +#if LIBCURL_VERSION_NUM >= 0x073600 + curl_easy_setopt(handle, CURLOPT_SUPPRESS_CONNECT_HEADERS, 1L); +#endif + + // Enable following redirections. Requires libcurl 7.10.1 at least. + curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(handle, CURLOPT_MAXREDIRS, 10); + + if (getenv("PROJ_UNSAFE_SSL")) { + curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 0L); + } + + curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, m_szCurlErrBuf); + + if (getenv("PROJ_NO_USERAGENT") == nullptr) { + m_useragent = "PROJ " STR(PROJ_VERSION_MAJOR) "." STR( + PROJ_VERSION_MINOR) "." STR(PROJ_VERSION_PATCH); + const auto exeName = GetExecutableName(); + if (!exeName.empty()) { + m_useragent = exeName + " using " + m_useragent; + } + curl_easy_setopt(handle, CURLOPT_USERAGENT, m_useragent.data()); + } +} + +// --------------------------------------------------------------------------- + +CurlFileHandle::~CurlFileHandle() { curl_easy_cleanup(m_handle); } + +// --------------------------------------------------------------------------- + +static size_t pj_curl_write_func(void *buffer, size_t count, size_t nmemb, + void *req) { + const size_t nSize = count * nmemb; + auto pStr = static_cast<std::string *>(req); + if (pStr->size() + nSize > pStr->capacity()) { + // to avoid servers not honouring Range to cause excessive memory + // allocation + return 0; + } + pStr->append(static_cast<const char *>(buffer), nSize); + return nmemb; +} + +// --------------------------------------------------------------------------- + +static double GetNewRetryDelay(int response_code, double dfOldDelay, + const char *pszErrBuf, + const char *pszCurlError) { + if (response_code == 429 || response_code == 500 || + (response_code >= 502 && response_code <= 504) || + // S3 sends some client timeout errors as 400 Client Error + (response_code == 400 && pszErrBuf && + strstr(pszErrBuf, "RequestTimeout")) || + (pszCurlError && strstr(pszCurlError, "Connection timed out"))) { + // Use an exponential backoff factor of 2 plus some random jitter + // We don't care about cryptographic quality randomness, hence: + // coverity[dont_call] + return dfOldDelay * (2 + rand() * 0.5 / RAND_MAX); + } else { + return 0; + } +} + +// --------------------------------------------------------------------------- + +constexpr double MIN_RETRY_DELAY_MS = 500; +constexpr double MAX_RETRY_DELAY_MS = 60000; + +PROJ_NETWORK_HANDLE *CurlFileHandle::open(PJ_CONTEXT *ctx, const char *url, + unsigned long long offset, + size_t size_to_read, void *buffer, + size_t *out_size_read, + size_t error_string_max_size, + char *out_error_string, void *) { + CURL *hCurlHandle = curl_easy_init(); + if (!hCurlHandle) + return nullptr; + + auto file = + std::unique_ptr<CurlFileHandle>(new CurlFileHandle(url, hCurlHandle)); + + double oldDelay = MIN_RETRY_DELAY_MS; + std::string headers; + std::string body; + + char szBuffer[128]; + sqlite3_snprintf(sizeof(szBuffer), szBuffer, "%llu-%llu", offset, + offset + size_to_read - 1); + + while (true) { + curl_easy_setopt(hCurlHandle, CURLOPT_RANGE, szBuffer); + + headers.clear(); + headers.reserve(16 * 1024); + curl_easy_setopt(hCurlHandle, CURLOPT_HEADERDATA, &headers); + curl_easy_setopt(hCurlHandle, CURLOPT_HEADERFUNCTION, + pj_curl_write_func); + + body.clear(); + body.reserve(size_to_read); + curl_easy_setopt(hCurlHandle, CURLOPT_WRITEDATA, &body); + curl_easy_setopt(hCurlHandle, CURLOPT_WRITEFUNCTION, + pj_curl_write_func); + + file->m_szCurlErrBuf[0] = '\0'; + + curl_easy_perform(hCurlHandle); + + long response_code = 0; + curl_easy_getinfo(hCurlHandle, CURLINFO_HTTP_CODE, &response_code); + + curl_easy_setopt(hCurlHandle, CURLOPT_HEADERDATA, nullptr); + curl_easy_setopt(hCurlHandle, CURLOPT_HEADERFUNCTION, nullptr); + + curl_easy_setopt(hCurlHandle, CURLOPT_WRITEDATA, nullptr); + curl_easy_setopt(hCurlHandle, CURLOPT_WRITEFUNCTION, nullptr); + + if (response_code == 0 || response_code >= 300) { + const double delay = + GetNewRetryDelay(static_cast<int>(response_code), oldDelay, + body.c_str(), file->m_szCurlErrBuf); + if (delay != 0 && delay < MAX_RETRY_DELAY_MS) { + pj_log(ctx, PJ_LOG_TRACE, + "Got a HTTP %ld error. Retrying in %d ms", response_code, + static_cast<int>(delay)); + sleep_ms(static_cast<int>(delay)); + oldDelay = delay; + } else { + if (out_error_string) { + if (file->m_szCurlErrBuf[0]) { + snprintf(out_error_string, error_string_max_size, "%s", + file->m_szCurlErrBuf); + } else { + snprintf(out_error_string, error_string_max_size, + "HTTP error %ld: %s", response_code, + body.c_str()); + } + } + return nullptr; + } + } else { + break; + } + } + + if (out_error_string && error_string_max_size) { + out_error_string[0] = '\0'; + } + + if (!body.empty()) { + memcpy(buffer, body.data(), std::min(size_to_read, body.size())); + } + *out_size_read = std::min(size_to_read, body.size()); + + file->m_headers = std::move(headers); + return reinterpret_cast<PROJ_NETWORK_HANDLE *>(file.release()); +} + +// --------------------------------------------------------------------------- + +static void pj_curl_close(PJ_CONTEXT *, PROJ_NETWORK_HANDLE *handle, + void * /*user_data*/) { + delete reinterpret_cast<CurlFileHandle *>(handle); +} + +// --------------------------------------------------------------------------- + +static size_t pj_curl_read_range(PJ_CONTEXT *ctx, + PROJ_NETWORK_HANDLE *raw_handle, + unsigned long long offset, size_t size_to_read, + void *buffer, size_t error_string_max_size, + char *out_error_string, void *) { + auto handle = reinterpret_cast<CurlFileHandle *>(raw_handle); + auto hCurlHandle = handle->m_handle; + + double oldDelay = MIN_RETRY_DELAY_MS; + std::string headers; + std::string body; + + char szBuffer[128]; + sqlite3_snprintf(sizeof(szBuffer), szBuffer, "%llu-%llu", offset, + offset + size_to_read - 1); + + while (true) { + curl_easy_setopt(hCurlHandle, CURLOPT_RANGE, szBuffer); + + headers.clear(); + headers.reserve(16 * 1024); + curl_easy_setopt(hCurlHandle, CURLOPT_HEADERDATA, &headers); + curl_easy_setopt(hCurlHandle, CURLOPT_HEADERFUNCTION, + pj_curl_write_func); + + body.clear(); + body.reserve(size_to_read); + curl_easy_setopt(hCurlHandle, CURLOPT_WRITEDATA, &body); + curl_easy_setopt(hCurlHandle, CURLOPT_WRITEFUNCTION, + pj_curl_write_func); + + handle->m_szCurlErrBuf[0] = '\0'; + + curl_easy_perform(hCurlHandle); + + long response_code = 0; + curl_easy_getinfo(hCurlHandle, CURLINFO_HTTP_CODE, &response_code); + + curl_easy_setopt(hCurlHandle, CURLOPT_WRITEDATA, nullptr); + curl_easy_setopt(hCurlHandle, CURLOPT_WRITEFUNCTION, nullptr); + + if (response_code == 0 || response_code >= 300) { + const double delay = + GetNewRetryDelay(static_cast<int>(response_code), oldDelay, + body.c_str(), handle->m_szCurlErrBuf); + if (delay != 0 && delay < MAX_RETRY_DELAY_MS) { + pj_log(ctx, PJ_LOG_TRACE, + "Got a HTTP %ld error. Retrying in %d ms", response_code, + static_cast<int>(delay)); + sleep_ms(static_cast<int>(delay)); + oldDelay = delay; + } else { + if (out_error_string) { + if (handle->m_szCurlErrBuf[0]) { + snprintf(out_error_string, error_string_max_size, "%s", + handle->m_szCurlErrBuf); + } else { + snprintf(out_error_string, error_string_max_size, + "HTTP error %ld: %s", response_code, + body.c_str()); + } + } + return 0; + } + } else { + break; + } + } + if (out_error_string && error_string_max_size) { + out_error_string[0] = '\0'; + } + + if (!body.empty()) { + memcpy(buffer, body.data(), std::min(size_to_read, body.size())); + } + handle->m_headers = std::move(headers); + + return std::min(size_to_read, body.size()); +} + +// --------------------------------------------------------------------------- + +static const char *pj_curl_get_header_value(PJ_CONTEXT *, + PROJ_NETWORK_HANDLE *raw_handle, + const char *header_name, void *) { + auto handle = reinterpret_cast<CurlFileHandle *>(raw_handle); + auto pos = ci_find(handle->m_headers, header_name); + if (pos == std::string::npos) + return nullptr; + pos += strlen(header_name); + const char *c_str = handle->m_headers.c_str(); + if (c_str[pos] == ':') + pos++; + while (c_str[pos] == ' ') + pos++; + auto posEnd = pos; + while (c_str[posEnd] != '\r' && c_str[posEnd] != '\n' && + c_str[posEnd] != '\0') + posEnd++; + handle->m_lastval = handle->m_headers.substr(pos, posEnd - pos); + return handle->m_lastval.c_str(); +} + +#else + +// --------------------------------------------------------------------------- + +static PROJ_NETWORK_HANDLE * +no_op_network_open(PJ_CONTEXT *, const char * /* url */, + unsigned long long, /* offset */ + size_t, /* size to read */ + void *, /* buffer to update with bytes read*/ + size_t *, /* output: size actually read */ + size_t error_string_max_size, char *out_error_string, + void * /*user_data*/) { + if (out_error_string) { + snprintf(out_error_string, error_string_max_size, "%s", + "Network functionality not available"); + } + return nullptr; +} + +// --------------------------------------------------------------------------- + +static void no_op_network_close(PJ_CONTEXT *, PROJ_NETWORK_HANDLE *, + void * /*user_data*/) {} + +#endif + +// --------------------------------------------------------------------------- + +void FileManager::fillDefaultNetworkInterface(PJ_CONTEXT *ctx) { +#ifdef CURL_ENABLED + ctx->networking.open = CurlFileHandle::open; + ctx->networking.close = pj_curl_close; + ctx->networking.read_range = pj_curl_read_range; + ctx->networking.get_header_value = pj_curl_get_header_value; +#else + ctx->networking.open = no_op_network_open; + ctx->networking.close = no_op_network_close; +#endif +} + +// --------------------------------------------------------------------------- + +void FileManager::clearMemoryCache() { + gNetworkChunkCache.clearMemoryCache(); + gNetworkFileProperties.clearMemoryCache(); +} + +NS_PROJ_END + +//! @endcond + +// --------------------------------------------------------------------------- + +#ifdef WIN32 +static const char dir_chars[] = "/\\"; +#else +static const char dir_chars[] = "/"; +#endif + +static bool is_tilde_slash(const char *name) { + return *name == '~' && strchr(dir_chars, name[1]); +} + +static bool is_rel_or_absolute_filename(const char *name) { + return strchr(dir_chars, *name) || + (*name == '.' && strchr(dir_chars, name[1])) || + (!strncmp(name, "..", 2) && strchr(dir_chars, name[2])) || + (name[0] != '\0' && name[1] == ':' && strchr(dir_chars, name[2])); +} + +static std::string build_url(PJ_CONTEXT *ctx, const char *name) { + if (!is_tilde_slash(name) && !is_rel_or_absolute_filename(name) && + !starts_with(name, "http://") && !starts_with(name, "https://")) { + std::string remote_file(pj_context_get_url_endpoint(ctx)); + if (!remote_file.empty()) { + if (remote_file.back() != '/') { + remote_file += '/'; + } + remote_file += name; + auto pos = remote_file.rfind('.'); + if (pos + 4 == remote_file.size()) { + remote_file = remote_file.substr(0, pos) + ".tif"; + } else { + // For example for resource files like 'alaska' + remote_file += ".tif"; + } + } + return remote_file; + } + return name; +} + +// --------------------------------------------------------------------------- + +/** Define a custom set of callbacks for network access. + * + * All callbacks should be provided (non NULL pointers). + * + * @param ctx PROJ context, or NULL + * @param open_cbk Callback to open a remote file given its URL + * @param close_cbk Callback to close a remote file. + * @param get_header_value_cbk Callback to get HTTP headers + * @param read_range_cbk Callback to read a range of bytes inside a remote file. + * @param user_data Arbitrary pointer provided by the user, and passed to the + * above callbacks. May be NULL. + * @return TRUE in case of success. + * @since 7.0 + */ +int proj_context_set_network_callbacks( + PJ_CONTEXT *ctx, proj_network_open_cbk_type open_cbk, + proj_network_close_cbk_type close_cbk, + proj_network_get_header_value_cbk_type get_header_value_cbk, + proj_network_read_range_type read_range_cbk, void *user_data) { + if (ctx == nullptr) { + ctx = pj_get_default_ctx(); + } + if (!open_cbk || !close_cbk || !get_header_value_cbk || !read_range_cbk) { + return false; + } + ctx->networking.open = open_cbk; + ctx->networking.close = close_cbk; + ctx->networking.get_header_value = get_header_value_cbk; + ctx->networking.read_range = read_range_cbk; + ctx->networking.user_data = user_data; + return true; +} + +// --------------------------------------------------------------------------- + +/** Enable or disable network access. +* +* This overrides the default endpoint in the PROJ configuration file or with +* the PROJ_NETWORK environment variable. +* +* @param ctx PROJ context, or NULL +* @param enable TRUE if network access is allowed. +* @return TRUE if network access is possible. That is either libcurl is +* available, or an alternate interface has been set. +* @since 7.0 +*/ +int proj_context_set_enable_network(PJ_CONTEXT *ctx, int enable) { + if (ctx == nullptr) { + ctx = pj_get_default_ctx(); + } + // Load ini file, now so as to override its network settings + pj_load_ini(ctx); + ctx->networking.enabled_env_variable_checked = true; + ctx->networking.enabled = enable != FALSE; +#ifdef CURL_ENABLED + return ctx->networking.enabled; +#else + return ctx->networking.enabled && + ctx->networking.open != NS_PROJ::no_op_network_open; +#endif +} + +//! @endcond + +// --------------------------------------------------------------------------- + +/** Define the URL endpoint to query for remote grids. +* +* This overrides the default endpoint in the PROJ configuration file or with +* the PROJ_NETWORK_ENDPOINT environment variable. +* +* @param ctx PROJ context, or NULL +* @param url Endpoint URL. Must NOT be NULL. +* @since 7.0 +*/ +void proj_context_set_url_endpoint(PJ_CONTEXT *ctx, const char *url) { + if (ctx == nullptr) { + ctx = pj_get_default_ctx(); + } + // Load ini file, now so as to override its network settings + pj_load_ini(ctx); + ctx->endpoint = url; +} + +// --------------------------------------------------------------------------- + +/** Enable or disable the local cache of grid chunks +* +* This overrides the setting in the PROJ configuration file. +* +* @param ctx PROJ context, or NULL +* @param enabled TRUE if the cache is enabled. +* @since 7.0 +*/ +void proj_grid_cache_set_enable(PJ_CONTEXT *ctx, int enabled) { + if (ctx == nullptr) { + ctx = pj_get_default_ctx(); + } + // Load ini file, now so as to override its settings + pj_load_ini(ctx); + ctx->gridChunkCache.enabled = enabled != FALSE; +} + +// --------------------------------------------------------------------------- + +/** Override, for the considered context, the path and file of the local +* cache of grid chunks. +* +* @param ctx PROJ context, or NULL +* @param fullname Full name to the cache (encoded in UTF-8). If set to NULL, +* caching will be disabled. +* @since 7.0 +*/ +void proj_grid_cache_set_filename(PJ_CONTEXT *ctx, const char *fullname) { + if (ctx == nullptr) { + ctx = pj_get_default_ctx(); + } + // Load ini file, now so as to override its settings + pj_load_ini(ctx); + ctx->gridChunkCache.filename = fullname ? fullname : std::string(); +} + +// --------------------------------------------------------------------------- + +/** Override, for the considered context, the maximum size of the local +* cache of grid chunks. +* +* @param ctx PROJ context, or NULL +* @param max_size_MB Maximum size, in mega-bytes (1024*1024 bytes), or +* negative value to set unlimited size. +* @since 7.0 +*/ +void proj_grid_cache_set_max_size(PJ_CONTEXT *ctx, int max_size_MB) { + if (ctx == nullptr) { + ctx = pj_get_default_ctx(); + } + // Load ini file, now so as to override its settings + pj_load_ini(ctx); + ctx->gridChunkCache.max_size = + max_size_MB < 0 ? -1 + : static_cast<long long>(max_size_MB) * 1024 * 1024; + if (max_size_MB == 0) { + // For debug purposes only + const char *env_var = getenv("PROJ_GRID_CACHE_MAX_SIZE_BYTES"); + if (env_var && env_var[0] != '\0') { + ctx->gridChunkCache.max_size = atoi(env_var); + } + } +} + +// --------------------------------------------------------------------------- + +/** Override, for the considered context, the time-to-live delay for +* re-checking if the cached properties of files are still up-to-date. +* +* @param ctx PROJ context, or NULL +* @param ttl_seconds Delay in seconds. Use negative value for no expiration. +* @since 7.0 +*/ +void proj_grid_cache_set_ttl(PJ_CONTEXT *ctx, int ttl_seconds) { + if (ctx == nullptr) { + ctx = pj_get_default_ctx(); + } + // Load ini file, now so as to override its settings + pj_load_ini(ctx); + ctx->gridChunkCache.ttl = ttl_seconds; +} + +// --------------------------------------------------------------------------- + +/** Clear the local cache of grid chunks. +* +* @param ctx PROJ context, or NULL +* @since 7.0 +*/ +void proj_grid_cache_clear(PJ_CONTEXT *ctx) { + if (ctx == nullptr) { + ctx = pj_get_default_ctx(); + } + NS_PROJ::gNetworkChunkCache.clearDiskChunkCache(ctx); +} + +// --------------------------------------------------------------------------- + +/** Return if a file must be downloaded or is already available in the + * PROJ user-writable directory. + * + * The file will be determinted to have to be downloaded if it does not exist + * yet in the user-writable directory, or if it is determined that a more recent + * version exists. To determine if a more recent version exists, PROJ will + * use the "downloaded_file_properties" table of its grid cache database. + * Consequently files manually placed in the user-writable + * directory without using this function would be considered as + * non-existing/obsolete and would be unconditionnaly downloaded again. + * + * This function can only be used if networking is enabled, and either + * the default curl network API or a custom one have been installed. + * + * @param ctx PROJ context, or NULL + * @param url_or_filename URL or filename (without directory component) + * @param ignore_ttl_setting If set to FALSE, PROJ will only check the + * recentness of an already downloaded file, if + * the delay between the last time it has been + * verified and the current time exceeds the TTL + * setting. This can save network accesses. + * If set to TRUE, PROJ will unconditionnally + * check from the server the recentness of the file. + * @return TRUE if the file must be downloaded with proj_download_file() + * @since 7.0 + */ + +int proj_is_download_needed(PJ_CONTEXT *ctx, const char *url_or_filename, + int ignore_ttl_setting) { + if (ctx == nullptr) { + ctx = pj_get_default_ctx(); + } + if (!pj_context_is_network_enabled(ctx)) { + pj_log(ctx, PJ_LOG_ERROR, "Networking capabilities are not enabled"); + return false; + } + + const auto url(build_url(ctx, url_or_filename)); + const char *filename = strrchr(url.c_str(), '/'); + if (filename == nullptr) + return false; + const auto localFilename( + pj_context_get_user_writable_directory(ctx, false) + filename); + + auto f = NS_PROJ::FileManager::open(ctx, localFilename.c_str(), + NS_PROJ::FileAccess::READ_ONLY); + if (!f) { + return true; + } + f.reset(); + + auto diskCache = NS_PROJ::DiskChunkCache::open(ctx); + if (!diskCache) + return false; + auto stmt = + diskCache->prepare("SELECT lastChecked, fileSize, lastModified, etag " + "FROM downloaded_file_properties WHERE url = ?"); + if (!stmt) + return true; + stmt->bindText(url.c_str()); + if (stmt->execute() != SQLITE_ROW) { + return true; + } + + NS_PROJ::FileProperties cachedProps; + cachedProps.lastChecked = stmt->getInt64(); + cachedProps.size = stmt->getInt64(); + const char *lastModified = stmt->getText(); + cachedProps.lastModified = lastModified ? lastModified : std::string(); + const char *etag = stmt->getText(); + cachedProps.etag = etag ? etag : std::string(); + + if (!ignore_ttl_setting) { + const auto ttl = NS_PROJ::pj_context_get_grid_cache_ttl(ctx); + if (ttl > 0) { + time_t curTime; + time(&curTime); + if (curTime > cachedProps.lastChecked + ttl) { + + unsigned char dummy; + size_t size_read = 0; + std::string errorBuffer; + errorBuffer.resize(1024); + auto handle = ctx->networking.open( + ctx, url.c_str(), 0, 1, &dummy, &size_read, + errorBuffer.size(), &errorBuffer[0], + ctx->networking.user_data); + if (!handle) { + errorBuffer.resize(strlen(errorBuffer.data())); + pj_log(ctx, PJ_LOG_ERROR, "Cannot open %s: %s", url.c_str(), + errorBuffer.c_str()); + return false; + } + NS_PROJ::FileProperties props; + if (!NS_PROJ::NetworkFile::get_props_from_headers(ctx, handle, + props)) { + ctx->networking.close(ctx, handle, + ctx->networking.user_data); + return false; + } + ctx->networking.close(ctx, handle, ctx->networking.user_data); + + if (props.size != cachedProps.size || + props.lastModified != cachedProps.lastModified || + props.etag != cachedProps.etag) { + return true; + } + + stmt = diskCache->prepare( + "UPDATE downloaded_file_properties SET lastChecked = ? " + "WHERE url = ?"); + if (!stmt) + return false; + stmt->bindInt64(curTime); + stmt->bindText(url.c_str()); + if (stmt->execute() != SQLITE_DONE) { + auto hDB = diskCache->handle(); + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return false; + } + } + } + } + + return false; +} + +// --------------------------------------------------------------------------- + +/** Download a file in the PROJ user-writable directory. + * + * The file will only be downloaded if it does not exist yet in the + * user-writable directory, or if it is determined that a more recent + * version exists. To determine if a more recent version exists, PROJ will + * use the "downloaded_file_properties" table of its grid cache database. + * Consequently files manually placed in the user-writable + * directory without using this function would be considered as + * non-existing/obsolete and would be unconditionnaly downloaded again. + * + * This function can only be used if networking is enabled, and either + * the default curl network API or a custom one have been installed. + * + * @param ctx PROJ context, or NULL + * @param url_or_filename URL or filename (without directory component) + * @param ignore_ttl_setting If set to FALSE, PROJ will only check the + * recentness of an already downloaded file, if + * the delay between the last time it has been + * verified and the current time exceeds the TTL + * setting. This can save network accesses. + * If set to TRUE, PROJ will unconditionnally + * check from the server the recentness of the file. + * @param progress_cbk Progress callback, or NULL. + * The passed percentage is in the [0, 1] range. + * The progress callback must return TRUE + * if download must be continued. + * @param user_data User data to provide to the progress callback, or NULL + * @return TRUE if the download was successful (or not needed) + * @since 7.0 + */ + +int proj_download_file(PJ_CONTEXT *ctx, const char *url_or_filename, + int ignore_ttl_setting, + int (*progress_cbk)(PJ_CONTEXT *, double pct, + void *user_data), + void *user_data) { + if (ctx == nullptr) { + ctx = pj_get_default_ctx(); + } + if (!pj_context_is_network_enabled(ctx)) { + pj_log(ctx, PJ_LOG_ERROR, "Networking capabilities are not enabled"); + return false; + } + if (!proj_is_download_needed(ctx, url_or_filename, ignore_ttl_setting)) { + return true; + } + + const auto url(build_url(ctx, url_or_filename)); + const char *filename = strrchr(url.c_str(), '/'); + if (filename == nullptr) + return false; + const auto localFilename(pj_context_get_user_writable_directory(ctx, true) + + filename); + +#ifdef _WIN32 + const int nPID = GetCurrentProcessId(); +#else + const int nPID = getpid(); +#endif + char szUniqueSuffix[128]; + snprintf(szUniqueSuffix, sizeof(szUniqueSuffix), "%d_%p", nPID, &url); + const auto localFilenameTmp(localFilename + szUniqueSuffix); + auto f = NS_PROJ::FileManager::open(ctx, localFilenameTmp.c_str(), + NS_PROJ::FileAccess::CREATE); + if (!f) { + pj_log(ctx, PJ_LOG_ERROR, "Cannot create %s", localFilenameTmp.c_str()); + return false; + } + + constexpr size_t FULL_FILE_CHUNK_SIZE = 1024 * 1024; + std::vector<unsigned char> buffer(FULL_FILE_CHUNK_SIZE); + // For testing purposes only + const char *env_var_PROJ_FULL_FILE_CHUNK_SIZE = + getenv("PROJ_FULL_FILE_CHUNK_SIZE"); + if (env_var_PROJ_FULL_FILE_CHUNK_SIZE && + env_var_PROJ_FULL_FILE_CHUNK_SIZE[0] != '\0') { + buffer.resize(atoi(env_var_PROJ_FULL_FILE_CHUNK_SIZE)); + } + size_t size_read = 0; + std::string errorBuffer; + errorBuffer.resize(1024); + auto handle = ctx->networking.open( + ctx, url.c_str(), 0, buffer.size(), &buffer[0], &size_read, + errorBuffer.size(), &errorBuffer[0], ctx->networking.user_data); + if (!handle) { + errorBuffer.resize(strlen(errorBuffer.data())); + pj_log(ctx, PJ_LOG_ERROR, "Cannot open %s: %s", url.c_str(), + errorBuffer.c_str()); + f.reset(); + NS_PROJ::FileManager::unlink(ctx, localFilenameTmp.c_str()); + return false; + } + + time_t curTime; + time(&curTime); + NS_PROJ::FileProperties props; + if (!NS_PROJ::NetworkFile::get_props_from_headers(ctx, handle, props)) { + ctx->networking.close(ctx, handle, ctx->networking.user_data); + f.reset(); + NS_PROJ::FileManager::unlink(ctx, localFilenameTmp.c_str()); + return false; + } + + if (size_read < + std::min(static_cast<unsigned long long>(buffer.size()), props.size)) { + pj_log(ctx, PJ_LOG_ERROR, "Did not get as many bytes as expected"); + ctx->networking.close(ctx, handle, ctx->networking.user_data); + f.reset(); + NS_PROJ::FileManager::unlink(ctx, localFilenameTmp.c_str()); + return false; + } + if (f->write(buffer.data(), size_read) != size_read) { + pj_log(ctx, PJ_LOG_ERROR, "Write error"); + ctx->networking.close(ctx, handle, ctx->networking.user_data); + f.reset(); + NS_PROJ::FileManager::unlink(ctx, localFilenameTmp.c_str()); + return false; + } + + unsigned long long totalDownloaded = size_read; + while (totalDownloaded < props.size) { + if (totalDownloaded + buffer.size() > props.size) { + buffer.resize(static_cast<size_t>(props.size - totalDownloaded)); + } + errorBuffer.resize(1024); + size_read = ctx->networking.read_range( + ctx, handle, totalDownloaded, buffer.size(), &buffer[0], + errorBuffer.size(), &errorBuffer[0], ctx->networking.user_data); + + if (size_read < buffer.size()) { + pj_log(ctx, PJ_LOG_ERROR, "Did not get as many bytes as expected"); + ctx->networking.close(ctx, handle, ctx->networking.user_data); + f.reset(); + NS_PROJ::FileManager::unlink(ctx, localFilenameTmp.c_str()); + return false; + } + if (f->write(buffer.data(), size_read) != size_read) { + pj_log(ctx, PJ_LOG_ERROR, "Write error"); + ctx->networking.close(ctx, handle, ctx->networking.user_data); + f.reset(); + NS_PROJ::FileManager::unlink(ctx, localFilenameTmp.c_str()); + return false; + } + + totalDownloaded += size_read; + if (progress_cbk && + !progress_cbk(ctx, double(totalDownloaded) / props.size, + user_data)) { + ctx->networking.close(ctx, handle, ctx->networking.user_data); + f.reset(); + NS_PROJ::FileManager::unlink(ctx, localFilenameTmp.c_str()); + return false; + } + } + + ctx->networking.close(ctx, handle, ctx->networking.user_data); + f.reset(); + NS_PROJ::FileManager::unlink(ctx, localFilename.c_str()); + if (!NS_PROJ::FileManager::rename(ctx, localFilenameTmp.c_str(), + localFilename.c_str())) { + pj_log(ctx, PJ_LOG_ERROR, "Cannot rename %s to %s", + localFilenameTmp.c_str(), localFilename.c_str()); + return false; + } + + auto diskCache = NS_PROJ::DiskChunkCache::open(ctx); + if (!diskCache) + return false; + auto stmt = + diskCache->prepare("SELECT lastChecked, fileSize, lastModified, etag " + "FROM downloaded_file_properties WHERE url = ?"); + if (!stmt) + return false; + stmt->bindText(url.c_str()); + + props.lastChecked = curTime; + auto hDB = diskCache->handle(); + + if (stmt->execute() == SQLITE_ROW) { + stmt = diskCache->prepare( + "UPDATE downloaded_file_properties SET lastChecked = ?, " + "fileSize = ?, lastModified = ?, etag = ? " + "WHERE url = ?"); + if (!stmt) + return false; + stmt->bindInt64(props.lastChecked); + stmt->bindInt64(props.size); + if (props.lastModified.empty()) + stmt->bindNull(); + else + stmt->bindText(props.lastModified.c_str()); + if (props.etag.empty()) + stmt->bindNull(); + else + stmt->bindText(props.etag.c_str()); + stmt->bindText(url.c_str()); + if (stmt->execute() != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return false; + } + } else { + stmt = diskCache->prepare( + "INSERT INTO downloaded_file_properties (url, lastChecked, " + "fileSize, lastModified, etag) VALUES " + "(?,?,?,?,?)"); + if (!stmt) + return false; + stmt->bindText(url.c_str()); + stmt->bindInt64(props.lastChecked); + stmt->bindInt64(props.size); + if (props.lastModified.empty()) + stmt->bindNull(); + else + stmt->bindText(props.lastModified.c_str()); + if (props.etag.empty()) + stmt->bindNull(); + else + stmt->bindText(props.etag.c_str()); + if (stmt->execute() != SQLITE_DONE) { + pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB)); + return false; + } + } + return true; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +bool pj_context_is_network_enabled(PJ_CONTEXT *ctx) { + if (ctx == nullptr) { + ctx = pj_get_default_ctx(); + } + if (ctx->networking.enabled_env_variable_checked) { + return ctx->networking.enabled; + } + const char *enabled = getenv("PROJ_NETWORK"); + if (enabled && enabled[0] != '\0') { + ctx->networking.enabled = ci_equal(enabled, "ON") || + ci_equal(enabled, "YES") || + ci_equal(enabled, "TRUE"); + } + pj_load_ini(ctx); + ctx->networking.enabled_env_variable_checked = true; + return ctx->networking.enabled; +} + +// --------------------------------------------------------------------------- + +std::string pj_context_get_grid_cache_filename(PJ_CONTEXT *ctx) { + pj_load_ini(ctx); + if (!ctx->gridChunkCache.filename.empty()) { + return ctx->gridChunkCache.filename; + } + const std::string path(pj_context_get_user_writable_directory(ctx, true)); + ctx->gridChunkCache.filename = path + "/cache.db"; + return ctx->gridChunkCache.filename; +} + +//! @endcond diff --git a/src/open_lib.cpp b/src/open_lib.cpp deleted file mode 100644 index ae387281..00000000 --- a/src/open_lib.cpp +++ /dev/null @@ -1,576 +0,0 @@ -/****************************************************************************** - * Project: PROJ.4 - * Purpose: Implementation of pj_open_lib(), and pj_set_finder(). These - * provide a standard interface for opening projections support - * data files. - * Author: Gerald Evenden, Frank Warmerdam <warmerdam@pobox.com> - * - ****************************************************************************** - * Copyright (c) 1995, Gerald Evenden - * Copyright (c) 2002, Frank Warmerdam <warmerdam@pobox.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - *****************************************************************************/ - -#define PJ_LIB__ - -#ifndef FROM_PROJ_CPP -#define FROM_PROJ_CPP -#endif - -#include <assert.h> -#include <errno.h> -#include <stddef.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - -#include "proj/internal/internal.hpp" - -#include "proj_internal.h" -#include "filemanager.hpp" - -static const char * proj_lib_name = -#ifdef PROJ_LIB -PROJ_LIB; -#else -nullptr; -#endif - -using namespace NS_PROJ::internal; - -/************************************************************************/ -/* pj_set_finder() */ -/************************************************************************/ - -void pj_set_finder( const char *(*new_finder)(const char *) ) - -{ - auto ctx = pj_get_default_ctx(); - if( ctx ) { - ctx->file_finder_legacy = new_finder; - } -} - -/************************************************************************/ -/* proj_context_set_file_finder() */ -/************************************************************************/ - -/** \brief Assign a file finder callback to a context. - * - * This callback will be used whenever PROJ must open one of its resource files - * (proj.db database, grids, etc...) - * - * The callback will be called with the context currently in use at the moment - * where it is used (not necessarily the one provided during this call), and - * with the provided user_data (which may be NULL). - * The user_data must remain valid during the whole lifetime of the context. - * - * A finder set on the default context will be inherited by contexts created - * later. - * - * @param ctx PROJ context, or NULL for the default context. - * @param finder Finder callback. May be NULL - * @param user_data User data provided to the finder callback. May be NULL. - * - * @since PROJ 6.0 - */ -void proj_context_set_file_finder(PJ_CONTEXT *ctx, proj_file_finder finder, - void* user_data) -{ - if( !ctx ) - ctx = pj_get_default_ctx(); - if( !ctx ) - return; - ctx->file_finder = finder; - ctx->file_finder_user_data = user_data; -} - -/************************************************************************/ -/* proj_context_set_search_paths() */ -/************************************************************************/ - - -/** \brief Sets search paths. - * - * Those search paths will be used whenever PROJ must open one of its resource files - * (proj.db database, grids, etc...) - * - * If set on the default context, they will be inherited by contexts created - * later. - * - * @param ctx PROJ context, or NULL for the default context. - * @param count_paths Number of paths. 0 if paths == NULL. - * @param paths Paths. May be NULL. - * - * @since PROJ 6.0 - */ -void proj_context_set_search_paths(PJ_CONTEXT *ctx, - int count_paths, - const char* const* paths) -{ - if( !ctx ) - ctx = pj_get_default_ctx(); - if( !ctx ) - return; - try { - std::vector<std::string> vector_of_paths; - for (int i = 0; i < count_paths; i++) - { - vector_of_paths.emplace_back(paths[i]); - } - ctx->set_search_paths(vector_of_paths); - } catch( const std::exception& ) - { - } -} - -/************************************************************************/ -/* pj_set_searchpath() */ -/* */ -/* Path control for callers that can't practically provide */ -/* pj_set_finder() style callbacks. Call with (0,NULL) as args */ -/* to clear the searchpath set. */ -/************************************************************************/ - -void pj_set_searchpath ( int count, const char **path ) -{ - proj_context_set_search_paths( nullptr, count, const_cast<const char* const*>(path) ); -} - -#ifdef _WIN32 -#include <windows.h> -#include <sys/stat.h> -static const char *get_path_from_win32_projlib(const char *name, std::string& out) { - /* Check if proj.db lieves in a share/proj dir parallel to bin/proj.dll */ - /* Based in https://stackoverflow.com/questions/9112893/how-to-get-path-to-executable-in-c-running-on-windows */ - - DWORD path_size = 1024; - - for (;;) { - out.resize(path_size); - memset(&out[0], 0, path_size); - DWORD result = GetModuleFileNameA(nullptr, &out[0], path_size - 1); - DWORD last_error = GetLastError(); - - if (result == 0) { - return nullptr; - } - else if (result == path_size - 1) { - if (ERROR_INSUFFICIENT_BUFFER != last_error) { - return nullptr; - } - path_size = path_size * 2; - } - else { - break; - } - } - // Now remove the program's name. It was (example) "C:\programs\gmt6\bin\gdal_translate.exe" - size_t k = strlen(out.c_str()); - while (k > 0 && out[--k] != '\\') {} - out.resize(k); - - out += "/../share/proj/"; - out += name; - - struct stat fileInfo; - if (stat(out.c_str(), &fileInfo) == 0) // Check if file exists (probably there are simpler ways) - return out.c_str(); - else { - return nullptr; - } -} -#endif - -/************************************************************************/ -/* pj_open_lib_internal() */ -/************************************************************************/ - -#ifdef WIN32 -static const char dir_chars[] = "/\\"; -static const char dirSeparator = ';'; -#else -static const char dir_chars[] = "/"; -static const char dirSeparator = ':'; -#endif - -static bool is_tilde_slash(const char* name) -{ - return *name == '~' && strchr(dir_chars,name[1]); -} - -static bool is_rel_or_absolute_filename(const char *name) -{ - return strchr(dir_chars,*name) - || (*name == '.' && strchr(dir_chars,name[1])) - || (!strncmp(name, "..", 2) && strchr(dir_chars,name[2])) - || (name[0] != '\0' && name[1] == ':' && strchr(dir_chars,name[2])); -} - -static bool ignoreUserWritableDirectory() -{ - // Env var mostly for testing purposes and being independent from - // an existing installation - const char* envVarIgnoreUserWritableDirectory = - getenv("PROJ_IGNORE_USER_WRITABLE_DIRECTORY"); - return envVarIgnoreUserWritableDirectory != nullptr && - envVarIgnoreUserWritableDirectory[0] != '\0'; -} - -static void* -pj_open_lib_internal(projCtx ctx, const char *name, const char *mode, - void* (*open_file)(projCtx, const char*, const char*), - char* out_full_filename, size_t out_full_filename_size) { - try { - std::string fname; - const char *sysname = nullptr; - void* fid = nullptr; - - if( ctx == nullptr ) { - ctx = pj_get_default_ctx(); - } - - if( out_full_filename != nullptr && out_full_filename_size > 0 ) - out_full_filename[0] = '\0'; - - /* check if ~/name */ - if (is_tilde_slash(name)) - if ((sysname = getenv("HOME")) != nullptr) { - fname = sysname; - fname += DIR_CHAR; - fname += name; - sysname = fname.c_str(); - } else - return nullptr; - - /* or fixed path: /name, ./name or ../name or http[s]:// */ - else if (is_rel_or_absolute_filename(name) - || starts_with(name, "http://") - || starts_with(name, "https://")) - sysname = name; - - /* or try to use application provided file finder */ - else if( ctx->file_finder != nullptr && (sysname = ctx->file_finder( ctx, name, ctx->file_finder_user_data )) != nullptr ) - ; - - else if( ctx->file_finder_legacy != nullptr && (sysname = ctx->file_finder_legacy( name )) != nullptr ) - ; - - /* The user has search paths set */ - else if( !ctx->search_paths.empty() ) { - for( const auto& path: ctx->search_paths ) { - try { - fname = path; - fname += DIR_CHAR; - fname += name; - sysname = fname.c_str(); - fid = open_file(ctx, sysname, mode); - } catch( const std::exception& ) - { - } - if( fid ) - break; - } - } - - else if( !ignoreUserWritableDirectory() && - (fid = open_file(ctx, - (pj_context_get_user_writable_directory(ctx, false) + - DIR_CHAR + name).c_str(), mode)) != nullptr ) { - fname = pj_context_get_user_writable_directory(ctx, false); - fname += DIR_CHAR; - fname += name; - sysname = fname.c_str(); - } - - /* if is environment PROJ_LIB defined */ - else if ((sysname = getenv("PROJ_LIB")) != nullptr) { - auto paths = NS_PROJ::internal::split(std::string(sysname), dirSeparator); - for( const auto& path: paths ) { - fname = path; - fname += DIR_CHAR; - fname += name; - sysname = fname.c_str(); - fid = open_file(ctx, sysname, mode); - if( fid ) - break; - } -#ifdef _WIN32 - /* check if it lives in a ../share/proj dir of the proj dll */ - } else if ((sysname = get_path_from_win32_projlib(name, fname)) != nullptr) { -#endif - /* or hardcoded path */ - } else if ((sysname = proj_lib_name) != nullptr) { - fname = sysname; - fname += DIR_CHAR; - fname += name; - sysname = fname.c_str(); - /* just try it bare bones */ - } else { - sysname = name; - } - - assert(sysname); // to make Coverity Scan happy - if ( fid != nullptr || (fid = open_file(ctx, sysname, mode)) != nullptr) - { - if( out_full_filename != nullptr && out_full_filename_size > 0 ) - { - // cppcheck-suppress nullPointer - strncpy(out_full_filename, sysname, out_full_filename_size); - out_full_filename[out_full_filename_size-1] = '\0'; - } - errno = 0; - } - - if( ctx->last_errno == 0 && errno != 0 ) - pj_ctx_set_errno( ctx, errno ); - - pj_log( ctx, PJ_LOG_DEBUG_MAJOR, - "pj_open_lib(%s): call fopen(%s) - %s", - name, sysname, - fid == nullptr ? "failed" : "succeeded" ); - - return(fid); - } - catch( const std::exception& ) { - - pj_log( ctx, PJ_LOG_DEBUG_MAJOR, - "pj_open_lib(%s): out of memory", - name ); - - return nullptr; - } -} - -/************************************************************************/ -/* pj_open_file_with_manager() */ -/************************************************************************/ - -static void* pj_open_file_with_manager(projCtx ctx, const char *name, - const char * /* mode */) -{ - return NS_PROJ::FileManager::open(ctx, name).release(); -} - -/************************************************************************/ -/* FileManager::open_resource_file() */ -/************************************************************************/ - -std::unique_ptr<NS_PROJ::File> NS_PROJ::FileManager::open_resource_file( - projCtx ctx, const char *name) -{ - auto file = std::unique_ptr<NS_PROJ::File>( - reinterpret_cast<NS_PROJ::File*>( - pj_open_lib_internal(ctx, name, "rb", - pj_open_file_with_manager, - nullptr, 0))); - if( file == nullptr && - !is_tilde_slash(name) && - !is_rel_or_absolute_filename(name) && - !starts_with(name, "http://") && - !starts_with(name, "https://") && - pj_context_is_network_enabled(ctx) ) { - std::string remote_file(pj_context_get_url_endpoint(ctx)); - if( !remote_file.empty() ) { - if( remote_file.back() != '/' ) { - remote_file += '/'; - } - remote_file += name; - auto pos = remote_file.rfind('.'); - if( pos + 4 == remote_file.size() ) { - remote_file = remote_file.substr(0, pos) + ".tif"; - file = open(ctx, remote_file.c_str()); - if( file ) { - pj_log( ctx, PJ_LOG_DEBUG_MAJOR, - "Using %s", remote_file.c_str() ); - pj_ctx_set_errno( ctx, 0 ); - } - } else { - // For example for resource files like 'alaska' - auto remote_file_tif = remote_file + ".tif"; - file = open(ctx, remote_file_tif.c_str()); - if( file ) { - pj_log( ctx, PJ_LOG_DEBUG_MAJOR, - "Using %s", remote_file_tif.c_str() ); - pj_ctx_set_errno( ctx, 0 ); - } else { - // Init files - file = open(ctx, remote_file.c_str()); - if( file ) { - pj_log( ctx, PJ_LOG_DEBUG_MAJOR, - "Using %s", remote_file.c_str() ); - pj_ctx_set_errno( ctx, 0 ); - } - } - } - } - } - return file; -} - -/************************************************************************/ -/* pj_open_lib() */ -/************************************************************************/ - -#ifndef REMOVE_LEGACY_SUPPORT - -// Used by following legacy function -static void* pj_ctx_fopen_adapter(projCtx ctx, const char *name, const char *mode) -{ - return pj_ctx_fopen(ctx, name, mode); -} - -// Legacy function -PAFile -pj_open_lib(projCtx ctx, const char *name, const char *mode) { - return (PAFile)pj_open_lib_internal(ctx, name, mode, pj_ctx_fopen_adapter, nullptr, 0); -} - -#endif // REMOVE_LEGACY_SUPPORT - -/************************************************************************/ -/* pj_find_file() */ -/************************************************************************/ - - -/** Returns the full filename corresponding to a proj resource file specified - * as a short filename. - * - * @param ctx context. - * @param short_filename short filename (e.g. egm96_15.gtx). Must not be NULL. - * @param out_full_filename output buffer, of size out_full_filename_size, that - * will receive the full filename on success. - * Will be zero-terminated. - * @param out_full_filename_size size of out_full_filename. - * @return 1 if the file was found, 0 otherwise. - */ -int pj_find_file(projCtx ctx, const char *short_filename, - char* out_full_filename, size_t out_full_filename_size) -{ - auto f = reinterpret_cast<NS_PROJ::File*>( - pj_open_lib_internal(ctx, short_filename, "rb", - pj_open_file_with_manager, - out_full_filename, - out_full_filename_size)); - if( f != nullptr ) - { - delete f; - return 1; - } - return 0; -} - -/************************************************************************/ -/* pj_context_get_url_endpoint() */ -/************************************************************************/ - -std::string pj_context_get_url_endpoint(PJ_CONTEXT* ctx) -{ - if( !ctx->endpoint.empty() ) { - return ctx->endpoint; - } - pj_load_ini(ctx); - return ctx->endpoint; -} - -/************************************************************************/ -/* trim() */ -/************************************************************************/ - -static std::string trim(const std::string& s) { - const auto first = s.find_first_not_of(' '); - const auto last = s.find_last_not_of(' '); - if( first == std::string::npos || last == std::string::npos ) { - return std::string(); - } - return s.substr(first, last - first + 1); -} - -/************************************************************************/ -/* pj_load_ini() */ -/************************************************************************/ - -void pj_load_ini(projCtx ctx) -{ - if( ctx->iniFileLoaded ) - return; - - const char* endpoint_from_env = getenv("PROJ_NETWORK_ENDPOINT"); - if( endpoint_from_env && endpoint_from_env[0] != '\0' ) { - ctx->endpoint = endpoint_from_env; - } - - ctx->iniFileLoaded = true; - auto file = std::unique_ptr<NS_PROJ::File>( - reinterpret_cast<NS_PROJ::File*>( - pj_open_lib_internal(ctx, "proj.ini", "rb", - pj_open_file_with_manager, - nullptr, 0))); - if( !file ) - return; - file->seek(0, SEEK_END); - const auto filesize = file->tell(); - if( filesize == 0 || filesize > 100 * 1024U ) - return; - file->seek(0, SEEK_SET); - std::string content; - content.resize(static_cast<size_t>(filesize)); - const auto nread = file->read(&content[0], content.size()); - if( nread != content.size() ) - return; - content += '\n'; - size_t pos = 0; - while( pos != std::string::npos ) { - const auto eol = content.find_first_of("\r\n", pos); - if( eol == std::string::npos ) { - break; - } - - const auto equal = content.find('=', pos); - if( equal < eol ) - { - const auto key = trim(content.substr(pos, equal-pos)); - const auto value = trim(content.substr(equal + 1, - eol - (equal+1))); - if( ctx->endpoint.empty() && key == "cdn_endpoint" ) { - ctx->endpoint = value; - } else if ( key == "network" ) { - const char *enabled = getenv("PROJ_NETWORK"); - if (enabled == nullptr || enabled[0] == '\0') { - ctx->networking.enabled = ci_equal(value, "ON") || - ci_equal(value, "YES") || - ci_equal(value, "TRUE"); - } - } else if ( key == "cache_enabled" ) { - ctx->gridChunkCache.enabled = ci_equal(value, "ON") || - ci_equal(value, "YES") || - ci_equal(value, "TRUE"); - } else if ( key == "cache_size_MB" ) { - const int val = atoi(value.c_str()); - ctx->gridChunkCache.max_size = val > 0 ? - static_cast<long long>(val) * 1024 * 1024 : -1; - } else if ( key == "cache_ttl_sec" ) { - ctx->gridChunkCache.ttl = atoi(value.c_str()); - } - } - - pos = content.find_first_not_of("\r\n", eol); - } -} @@ -355,7 +355,66 @@ int PROJ_DLL proj_context_get_use_proj4_init_rules(PJ_CONTEXT *ctx, int from_leg /*! @endcond */ -/** Opaque structure for PROJ. Implementations might cast it to their +/** Opaque structure for PROJ for a file handle. Implementations might cast it to their + * structure/class of choice. */ +typedef struct PROJ_FILE_HANDLE PROJ_FILE_HANDLE; + +/** Open access / mode */ +typedef enum PROJ_OPEN_ACCESS +{ + /** Read-only access. Equivalent to "rb" */ + PROJ_OPEN_ACCESS_READ_ONLY, + + /** Read-update access. File should be created if not existing. Equivalent to "r+b" */ + PROJ_OPEN_ACCESS_READ_UPDATE, + + /** Create access. File should be truncated to 0-byte if already existing. Equivalent to "w+b" */ + PROJ_OPEN_ACCESS_CREATE +} PROJ_OPEN_ACCESS; + +/** File API callbacks */ +typedef struct PROJ_FILE_API +{ + /** Version of this structure. Should be set to 1 currently. */ + int version; + + /** Open file. Return NULL if error */ + PROJ_FILE_HANDLE* (*open_cbk)(PJ_CONTEXT *ctx, const char *filename, PROJ_OPEN_ACCESS access, void* user_data); + + /** Read sizeBytes into buffer from current position and return number of bytes read */ + size_t (*read_cbk)(PJ_CONTEXT *ctx, PROJ_FILE_HANDLE*, void* buffer, size_t sizeBytes, void* user_data); + + /** Write sizeBytes into buffer from current position and return number of bytes written */ + size_t (*write_cbk)(PJ_CONTEXT *ctx, PROJ_FILE_HANDLE*, const void* buffer, size_t sizeBytes, void* user_data); + + /** Seek to offset using whence=SEEK_SET/SEEK_CUR/SEEK_END. Return TRUE in case of success */ + int (*seek_cbk)(PJ_CONTEXT *ctx, PROJ_FILE_HANDLE*, long long offset, int whence, void* user_data); + + /** Return current file position */ + unsigned long long (*tell_cbk)(PJ_CONTEXT *ctx, PROJ_FILE_HANDLE*, void* user_data); + + /** Close file */ + void (*close_cbk)(PJ_CONTEXT *ctx, PROJ_FILE_HANDLE*, void* user_data); + + /** Return TRUE if a file exists */ + int (*exists_cbk)(PJ_CONTEXT *ctx, const char *filename, void* user_data); + + /** Return TRUE if directory exists or could be created */ + int (*mkdir_cbk)(PJ_CONTEXT *ctx, const char *filename, void* user_data); + + /** Return TRUE if file could be removed */ + int (*unlink_cbk)(PJ_CONTEXT *ctx, const char *filename, void* user_data); + + /** Return TRUE if file could be renamed */ + int (*rename_cbk)(PJ_CONTEXT *ctx, const char *oldPath, const char *newPath, void* user_data); +} PROJ_FILE_API; + +int PROJ_DLL proj_context_set_fileapi( + PJ_CONTEXT* ctx, const PROJ_FILE_API* fileapi, void* user_data); + +void PROJ_DLL proj_context_set_sqlite3_vfs_name(PJ_CONTEXT* ctx, const char* name); + +/** Opaque structure for PROJ for a network handle. Implementations might cast it to their * structure/class of choice. */ typedef struct PROJ_NETWORK_HANDLE PROJ_NETWORK_HANDLE; diff --git a/src/proj_internal.h b/src/proj_internal.h index ce7b9d74..15b98859 100644 --- a/src/proj_internal.h +++ b/src/proj_internal.h @@ -685,6 +685,23 @@ struct projGridChunkCache int ttl = 86400; // 1 day }; +struct projFileApiCallbackAndData +{ + PROJ_FILE_HANDLE* (*open_cbk)(PJ_CONTEXT *ctx, const char *filename, PROJ_OPEN_ACCESS access, void* user_data) = nullptr; + size_t (*read_cbk)(PJ_CONTEXT *ctx, PROJ_FILE_HANDLE*, void* buffer, size_t size, void* user_data) = nullptr; + size_t (*write_cbk)(PJ_CONTEXT *ctx, PROJ_FILE_HANDLE*, const void* buffer, size_t size, void* user_data) = nullptr; + int (*seek_cbk)(PJ_CONTEXT *ctx, PROJ_FILE_HANDLE*, long long offset, int whence, void* user_data) = nullptr; + unsigned long long (*tell_cbk)(PJ_CONTEXT *ctx, PROJ_FILE_HANDLE*, void* user_data) = nullptr; + void (*close_cbk)(PJ_CONTEXT *ctx, PROJ_FILE_HANDLE*, void* user_data) = nullptr; + + int (*exists_cbk)(PJ_CONTEXT *ctx, const char *filename, void* user_data) = nullptr; + int (*mkdir_cbk)(PJ_CONTEXT *ctx, const char *filename, void* user_data) = nullptr; + int (*unlink_cbk)(PJ_CONTEXT *ctx, const char *filename, void* user_data) = nullptr; + int (*rename_cbk)(PJ_CONTEXT *ctx, const char *oldPath, const char *newPath, void* user_data) = nullptr; + + void* user_data = nullptr; +}; + /* proj thread context */ struct projCtx_t { int last_errno = 0; @@ -696,6 +713,7 @@ struct projCtx_t { int use_proj4_init_rules = -1; /* -1 = unknown, 0 = no, 1 = yes */ int epsg_file_exists = -1; /* -1 = unknown, 0 = no, 1 = yes */ + std::string env_var_proj_lib{}; // content of PROJ_LIB environment variable. Use Filemanager::getProjLibEnvVar() to access std::vector<std::string> search_paths{}; const char **c_compat_paths = nullptr; // same, but for projinfo usage @@ -706,6 +724,9 @@ struct projCtx_t { projNetworkCallbacksAndData networking{}; bool defer_grid_opening = false; // set by pj_obj_create() + projFileApiCallbackAndData fileApi{}; + std::string custom_sqlite3_vfs_name{}; + bool iniFileLoaded = false; std::string endpoint{}; diff --git a/src/sqlite3.cpp b/src/sqlite3_utils.cpp index 90e73c2a..673eb89c 100644 --- a/src/sqlite3.cpp +++ b/src/sqlite3_utils.cpp @@ -30,7 +30,7 @@ #pragma GCC diagnostic ignored "-Weffc++" #endif -#include "sqlite3.hpp" +#include "sqlite3_utils.hpp" #ifdef __GNUC__ #pragma GCC diagnostic pop diff --git a/src/sqlite3.hpp b/src/sqlite3_utils.hpp index ef141d1f..ef141d1f 100644 --- a/src/sqlite3.hpp +++ b/src/sqlite3_utils.hpp diff --git a/test/cli/ntv2_out.dist b/test/cli/ntv2_out.dist index 650a69d8..ce866f37 100644 --- a/test/cli/ntv2_out.dist +++ b/test/cli/ntv2_out.dist @@ -12,3 +12,11 @@ Try with NTv2 and NTv1 together ... falls back to NTv1 ############################################################## Switching between NTv2 subgrids -112.5839956 49.4914451 0 -112.58307487 49.49145197 0.00000000 +############################################################## +Interpolating very close (and sometimes a bit outside) to the edges a NTv2 subgrid (#209) +-115.58333333 51.25000000 0 -115.58228512 51.24997866 0.00000000 +-115.58333333 51.25000010 0 -115.58228512 51.24997876 0.00000000 +-115.58333334 51.25000000 0 -115.58228513 51.24997866 0.00000000 +-115.49166667 51.07500000 0 -115.49062909 51.07497666 0.00000000 +-115.49166668 51.07500000 0 -115.49062910 51.07497666 0.00000000 +-115.49166667 51.07499990 0 -115.49062909 51.07497656 0.00000000 diff --git a/test/cli/td_out.dist b/test/cli/td_out.dist index 82b64321..76c6e6ec 100644 --- a/test/cli/td_out.dist +++ b/test/cli/td_out.dist @@ -21,7 +21,7 @@ edge or even a wee bit outside (#141). -5.5001 52.0 -5.500100000000 52.000000000000 0.000000000000 -5.5 52.0 -5.498893534472 52.000109529716 0.000000000000 -5.5000000000001 52.0000000000001 -5.498893534472 52.000109529717 0.000000000000 --5.4999 51.9999 -5.498793541695 52.000009529743 0.000000000000 +-5.4999 51.9999 -5.498793593803 52.000009531513 0.000000000000 -5.5001 52.0 -5.500100000000 52.000000000000 0.000000000000 ############################################################## NAD27 -> NAD83: 1st through ntv1, 2nd through conus diff --git a/test/cli/testntv2 b/test/cli/testntv2 index 2a31304e..d15ad7aa 100755 --- a/test/cli/testntv2 +++ b/test/cli/testntv2 @@ -60,6 +60,17 @@ $EXE +proj=latlong +datum=NAD83 +to +proj=latlong +ellps=clrk66 +nadgrids=ntv2_0 -112.5839956 49.4914451 0 EOF +echo "##############################################################" >> ${OUT} +echo "Interpolating very close (and sometimes a bit outside) to the edges a NTv2 subgrid (#209)" >> ${OUT} +$EXE +proj=latlong +datum=NAD83 +to +proj=latlong +ellps=clrk66 +nadgrids=ntv2_0.gsb -E -d 8 >>${OUT} <<EOF +-115.58333333 51.25000000 0 +-115.58333333 51.25000010 0 +-115.58333334 51.25000000 0 +-115.49166667 51.07500000 0 +-115.49166668 51.07500000 0 +-115.49166667 51.07499990 0 +EOF + # ############################################################################## # Done! diff --git a/test/gie/geotiff_grids.gie b/test/gie/geotiff_grids.gie index 85c0cbe9..62a5b16d 100644 --- a/test/gie/geotiff_grids.gie +++ b/test/gie/geotiff_grids.gie @@ -277,6 +277,20 @@ accept -80.5041667 44.5458333 0 expect -80.50401615833 44.5458827236 0 ------------------------------------------------------------------------------- +# Check a nested grid of a nested grid only based on spatial extent analysis +------------------------------------------------------------------------------- +operation +proj=hgridshift +grids=tests/test_hgrid_with_two_level_of_subgrids_no_grid_name.tif +------------------------------------------------------------------------------- +accept -45.0 22.5 +accept -44.9983333334 22.5013888889 + +# Check a nested grid of a nested grid only based on spatial extent analysis +------------------------------------------------------------------------------- +operation +proj=vgridshift +grids=tests/test_hgrid_with_two_level_of_subgrids_no_grid_name.tif +multiplier=1 +------------------------------------------------------------------------------- +accept -45.0 22.5 0 +accept -45.0 22.5 5 + ------------------------------------------------------------------------------- operation +proj=hgridshift +grids=tests/test_vgrid.tif ------------------------------------------------------------------------------- diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index e1eefcf0..e14d4c70 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -128,14 +128,20 @@ add_executable(proj_test_cpp_api test_operation.cpp test_datum.cpp test_factory.cpp - test_c_api.cpp) + test_c_api.cpp + test_grids.cpp) target_link_libraries(proj_test_cpp_api GTest::gtest ${PROJ_LIBRARIES} ${SQLITE3_LIBRARY}) add_test(NAME proj_test_cpp_api COMMAND proj_test_cpp_api) -set_property(TEST proj_test_cpp_api - PROPERTY ENVIRONMENT "PROJ_IGNORE_USER_WRITABLE_DIRECTORY=YES;PROJ_LIB=${PROJECT_BINARY_DIR}/data") +if(MSVC) + set_property(TEST proj_test_cpp_api + PROPERTY ENVIRONMENT "PROJ_IGNORE_USER_WRITABLE_DIRECTORY=YES;PROJ_LIB=${PROJECT_BINARY_DIR}/data\\;${PROJECT_SOURCE_DIR}/data") +else() + set_property(TEST proj_test_cpp_api + PROPERTY ENVIRONMENT "PROJ_IGNORE_USER_WRITABLE_DIRECTORY=YES;PROJ_LIB=${PROJECT_BINARY_DIR}/data:${PROJECT_SOURCE_DIR}/data") +endif() add_executable(gie_self_tests diff --git a/test/unit/Makefile.am b/test/unit/Makefile.am index 7ffb06ae..b7a26d4e 100644 --- a/test/unit/Makefile.am +++ b/test/unit/Makefile.am @@ -49,7 +49,7 @@ proj_context_test_LDADD = ../../src/libproj.la @GTEST_LIBS@ proj_context_test-check: proj_context_test PROJ_IGNORE_USER_WRITABLE_DIRECTORY=YES ./proj_context_test -test_cpp_api_SOURCES = test_util.cpp test_common.cpp test_crs.cpp test_metadata.cpp test_io.cpp test_operation.cpp test_datum.cpp test_factory.cpp test_c_api.cpp main.cpp +test_cpp_api_SOURCES = test_util.cpp test_common.cpp test_crs.cpp test_metadata.cpp test_io.cpp test_operation.cpp test_datum.cpp test_factory.cpp test_c_api.cpp test_grids.cpp main.cpp test_cpp_api_LDADD = ../../src/libproj.la @GTEST_LIBS@ @SQLITE3_LIBS@ test_cpp_api-check: test_cpp_api diff --git a/test/unit/test_c_api.cpp b/test/unit/test_c_api.cpp index f87f6589..8871f679 100644 --- a/test/unit/test_c_api.cpp +++ b/test/unit/test_c_api.cpp @@ -4434,4 +4434,26 @@ TEST_F(CApi, proj_create_derived_geographic_crs) { "+o_lat_p=-2 +lon_0=3 +datum=WGS84 +no_defs " "+type=crs")); } + +// --------------------------------------------------------------------------- + +TEST_F(CApi, proj_context_set_sqlite3_vfs_name) { + + PJ_CONTEXT *ctx = proj_context_create(); + proj_log_func(ctx, nullptr, [](void *, int, const char *) -> void {}); + + // Set a dummy VFS and check it is taken into account + // (failure to open proj.db) + proj_context_set_sqlite3_vfs_name(ctx, "dummy_vfs_name"); + ASSERT_EQ(proj_create(ctx, "EPSG:4326"), nullptr); + + // Restore default VFS + proj_context_set_sqlite3_vfs_name(ctx, nullptr); + PJ *crs_4326 = proj_create(ctx, "EPSG:4326"); + ASSERT_NE(crs_4326, nullptr); + proj_destroy(crs_4326); + + proj_context_destroy(ctx); +} + } // namespace diff --git a/test/unit/test_grids.cpp b/test/unit/test_grids.cpp new file mode 100644 index 00000000..5240949e --- /dev/null +++ b/test/unit/test_grids.cpp @@ -0,0 +1,227 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: Test grids.hpp + * Author: Even Rouault <even dot rouault at spatialys dot com> + * + ****************************************************************************** + * Copyright (c) 2020, Even Rouault <even dot rouault at spatialys dot com> + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#include "gtest_include.h" + +#include "grids.hpp" + +#include "proj_internal.h" // M_PI + +namespace { + +// --------------------------------------------------------------------------- + +class GridTest : public ::testing::Test { + + static void DummyLogFunction(void *, int, const char *) {} + + protected: + void SetUp() override { + m_ctxt = proj_context_create(); + proj_log_func(m_ctxt, nullptr, DummyLogFunction); + m_ctxt2 = proj_context_create(); + proj_log_func(m_ctxt2, nullptr, DummyLogFunction); + } + + void TearDown() override { + proj_context_destroy(m_ctxt); + proj_context_destroy(m_ctxt2); + } + + PJ_CONTEXT *m_ctxt = nullptr; + PJ_CONTEXT *m_ctxt2 = nullptr; +}; + +// --------------------------------------------------------------------------- + +TEST_F(GridTest, VerticalShiftGridSet_null) { + auto gridSet = NS_PROJ::VerticalShiftGridSet::open(m_ctxt, "null"); + ASSERT_NE(gridSet, nullptr); + auto grid = gridSet->gridAt(0.0, 0.0); + ASSERT_NE(grid, nullptr); + EXPECT_EQ(grid->width(), 3); + EXPECT_EQ(grid->height(), 3); + EXPECT_EQ(grid->extentAndRes().westLon, -M_PI); + EXPECT_TRUE(grid->isNullGrid()); + EXPECT_FALSE(grid->hasChanged()); + float out = -1.0f; + EXPECT_TRUE(grid->valueAt(0, 0, out)); + EXPECT_EQ(out, 0.0f); + EXPECT_FALSE(grid->isNodata(0.0f, 0.0)); + gridSet->reassign_context(m_ctxt2); + gridSet->reopen(m_ctxt2); +} + +// --------------------------------------------------------------------------- + +TEST_F(GridTest, VerticalShiftGridSet_gtx) { + ASSERT_EQ(NS_PROJ::VerticalShiftGridSet::open(m_ctxt, "foobar"), nullptr); + auto gridSet = + NS_PROJ::VerticalShiftGridSet::open(m_ctxt, "tests/test_nodata.gtx"); + ASSERT_NE(gridSet, nullptr); + ASSERT_EQ(gridSet->gridAt(-100, -100), nullptr); + auto grid = gridSet->gridAt(4.15 / 180 * M_PI, 52.15 / 180 * M_PI); + ASSERT_NE(grid, nullptr); + EXPECT_TRUE(grid->isNodata(-88.8888f, 1.0)); + gridSet->reassign_context(m_ctxt2); + gridSet->reopen(m_ctxt2); + grid = gridSet->gridAt(4.15 / 180 * M_PI, 52.15 / 180 * M_PI); + EXPECT_NE(grid, nullptr); +} + +// --------------------------------------------------------------------------- + +TEST_F(GridTest, HorizontalShiftGridSet_null) { + auto gridSet = NS_PROJ::HorizontalShiftGridSet::open(m_ctxt, "null"); + ASSERT_NE(gridSet, nullptr); + auto grid = gridSet->gridAt(0.0, 0.0); + ASSERT_NE(grid, nullptr); + EXPECT_EQ(grid->width(), 3); + EXPECT_EQ(grid->height(), 3); + EXPECT_EQ(grid->extentAndRes().westLon, -M_PI); + EXPECT_TRUE(grid->isNullGrid()); + EXPECT_FALSE(grid->hasChanged()); + float out1 = -1.0f; + float out2 = -1.0f; + EXPECT_TRUE(grid->valueAt(0, 0, false, out1, out2)); + EXPECT_EQ(out1, 0.0f); + EXPECT_EQ(out2, 0.0f); + gridSet->reassign_context(m_ctxt2); + gridSet->reopen(m_ctxt2); +} + +// --------------------------------------------------------------------------- + +TEST_F(GridTest, HorizontalShiftGridSet_gtiff) { + auto gridSet = + NS_PROJ::HorizontalShiftGridSet::open(m_ctxt, "tests/test_hgrid.tif"); + ASSERT_NE(gridSet, nullptr); + EXPECT_EQ(gridSet->format(), "gtiff"); + EXPECT_TRUE(gridSet->name().find("tests/test_hgrid.tif") != + std::string::npos) + << gridSet->name(); + EXPECT_EQ(gridSet->grids().size(), 1U); + ASSERT_EQ(gridSet->gridAt(-100, -100), nullptr); + auto grid = gridSet->gridAt(5.5 / 180 * M_PI, 53.5 / 180 * M_PI); + ASSERT_NE(grid, nullptr); + EXPECT_EQ(grid->width(), 4); + EXPECT_EQ(grid->height(), 4); + EXPECT_EQ(grid->extentAndRes().westLon, 4.0 / 180 * M_PI); + EXPECT_FALSE(grid->isNullGrid()); + EXPECT_FALSE(grid->hasChanged()); + float out1 = -1.0f; + float out2 = -1.0f; + EXPECT_TRUE(grid->valueAt(0, 3, false, out1, out2)); + EXPECT_EQ(out1, static_cast<float>(14400.0 / 3600. / 180 * M_PI)); + EXPECT_EQ(out2, static_cast<float>(900.0 / 3600. / 180 * M_PI)); + gridSet->reassign_context(m_ctxt2); + gridSet->reopen(m_ctxt2); + grid = gridSet->gridAt(5.5 / 180 * M_PI, 53.5 / 180 * M_PI); + EXPECT_NE(grid, nullptr); +} + +// --------------------------------------------------------------------------- + +TEST_F(GridTest, GenericShiftGridSet_null) { + auto gridSet = NS_PROJ::GenericShiftGridSet::open(m_ctxt, "null"); + ASSERT_NE(gridSet, nullptr); + auto grid = gridSet->gridAt(0.0, 0.0); + ASSERT_NE(grid, nullptr); + EXPECT_EQ(grid->width(), 3); + EXPECT_EQ(grid->height(), 3); + EXPECT_EQ(grid->extentAndRes().westLon, -M_PI); + EXPECT_TRUE(grid->isNullGrid()); + EXPECT_FALSE(grid->hasChanged()); + float out = -1.0f; + EXPECT_TRUE(grid->valueAt(0, 0, 0, out)); + EXPECT_EQ(out, 0.0f); + EXPECT_EQ(grid->unit(0), ""); + EXPECT_EQ(grid->description(0), ""); + EXPECT_EQ(grid->metadataItem("foo"), ""); + EXPECT_EQ(grid->samplesPerPixel(), 0); + gridSet->reassign_context(m_ctxt2); + gridSet->reopen(m_ctxt2); +} + +// --------------------------------------------------------------------------- + +TEST_F(GridTest, GenericShiftGridSet_gtiff) { + ASSERT_EQ(NS_PROJ::GenericShiftGridSet::open(m_ctxt, "foobar"), nullptr); + auto gridSet = NS_PROJ::GenericShiftGridSet::open( + m_ctxt, "tests/nkgrf03vel_realigned_extract.tif"); + ASSERT_NE(gridSet, nullptr); + ASSERT_EQ(gridSet->gridAt(-100, -100), nullptr); + auto grid = gridSet->gridAt(21.3333333 / 180 * M_PI, 63.0 / 180 * M_PI); + ASSERT_NE(grid, nullptr); + EXPECT_EQ(grid->width(), 5); + EXPECT_EQ(grid->height(), 5); + EXPECT_EQ(grid->extentAndRes().westLon, 21.0 / 180 * M_PI); + EXPECT_FALSE(grid->isNullGrid()); + EXPECT_FALSE(grid->hasChanged()); + float out = -1.0f; + EXPECT_FALSE(grid->valueAt(0, 0, grid->samplesPerPixel(), out)); + EXPECT_EQ(grid->metadataItem("area_of_use"), "Nordic and Baltic countries"); + EXPECT_EQ(grid->metadataItem("non_existing"), std::string()); + EXPECT_EQ(grid->metadataItem("non_existing", 1), std::string()); + EXPECT_EQ(grid->metadataItem("non_existing", 10), std::string()); + gridSet->reassign_context(m_ctxt2); + gridSet->reopen(m_ctxt2); + grid = gridSet->gridAt(21.3333333 / 180 * M_PI, 63.0 / 180 * M_PI); + EXPECT_NE(grid, nullptr); +} + +// --------------------------------------------------------------------------- + +TEST_F(GridTest, GenericShiftGridSet_gtiff_with_subgrid) { + auto gridSet = NS_PROJ::GenericShiftGridSet::open( + m_ctxt, "tests/test_hgrid_with_subgrid.tif"); + ASSERT_NE(gridSet, nullptr); + ASSERT_EQ(gridSet->gridAt(-100, -100), nullptr); + auto grid = + gridSet->gridAt(-115.5416667 / 180 * M_PI, 51.1666667 / 180 * M_PI); + ASSERT_NE(grid, nullptr); + EXPECT_EQ(grid->width(), 11); + EXPECT_EQ(grid->height(), 21); + EXPECT_EQ(grid->metadataItem("grid_name"), "ALbanff"); +} + +// --------------------------------------------------------------------------- + +TEST_F(GridTest, + GenericShiftGridSet_gtiff_with_two_level_of_subgrids_no_grid_name) { + auto gridSet = NS_PROJ::GenericShiftGridSet::open( + m_ctxt, "tests/test_hgrid_with_two_level_of_subgrids_no_grid_name.tif"); + ASSERT_NE(gridSet, nullptr); + ASSERT_EQ(gridSet->gridAt(-100, -100), nullptr); + auto grid = gridSet->gridAt(-45.5 / 180 * M_PI, 22.5 / 180 * M_PI); + ASSERT_NE(grid, nullptr); + EXPECT_EQ(grid->width(), 8); + EXPECT_EQ(grid->height(), 8); +} + +} // namespace diff --git a/test/unit/test_network.cpp b/test/unit/test_network.cpp index 4e66d8c5..688e61e5 100644 --- a/test/unit/test_network.cpp +++ b/test/unit/test_network.cpp @@ -1574,27 +1574,45 @@ TEST(networking, download_whole_files) { proj_cleanup(); unlink("proj_test_tmp/cache.db"); - unlink("proj_test_tmp/ntf_r93.tif"); + unlink("proj_test_tmp/dvr90.tif"); rmdir("proj_test_tmp"); putenv(const_cast<char *>("PROJ_IGNORE_USER_WRITABLE_DIRECTORY=")); putenv(const_cast<char *>("PROJ_USER_WRITABLE_DIRECTORY=./proj_test_tmp")); - putenv(const_cast<char *>("PROJ_FULL_FILE_CHUNK_SIZE=30000")); + putenv(const_cast<char *>("PROJ_FULL_FILE_CHUNK_SIZE=100000")); auto ctx = proj_context_create(); proj_context_set_enable_network(ctx, true); - ASSERT_TRUE(proj_is_download_needed(ctx, "ntf_r93.gsb", false)); + ASSERT_TRUE(proj_is_download_needed(ctx, "dvr90.gtx", false)); - ASSERT_TRUE( - proj_download_file(ctx, "ntf_r93.gsb", false, nullptr, nullptr)); + ASSERT_TRUE(proj_download_file(ctx, "dvr90.gtx", false, nullptr, nullptr)); - FILE *f = fopen("proj_test_tmp/ntf_r93.tif", "rb"); + FILE *f = fopen("proj_test_tmp/dvr90.tif", "rb"); ASSERT_NE(f, nullptr); - fseek(f, 0, SEEK_END); - ASSERT_EQ(ftell(f), 93581); fclose(f); - ASSERT_FALSE(proj_is_download_needed(ctx, "ntf_r93.gsb", false)); + proj_context_set_enable_network(ctx, false); + + const char *pipeline = + "+proj=pipeline " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=dvr90.gtx +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"; + + auto P = proj_create(ctx, pipeline); + ASSERT_NE(P, nullptr); + + double lon = 12; + double lat = 56; + double z = 0; + proj_trans_generic(P, PJ_FWD, &lon, sizeof(double), 1, &lat, sizeof(double), + 1, &z, sizeof(double), 1, nullptr, 0, 0); + EXPECT_NEAR(z, 36.5909996032715, 1e-10); + proj_destroy(P); + + proj_context_set_enable_network(ctx, true); + + ASSERT_FALSE(proj_is_download_needed(ctx, "dvr90.gtx", false)); { sqlite3 *hDB = nullptr; @@ -1613,7 +1631,7 @@ TEST(networking, download_whole_files) { } // If we ignore TTL settings, then no network access will be done - ASSERT_FALSE(proj_is_download_needed(ctx, "ntf_r93.gsb", true)); + ASSERT_FALSE(proj_is_download_needed(ctx, "dvr90.gtx", true)); { sqlite3 *hDB = nullptr; @@ -1633,7 +1651,7 @@ TEST(networking, download_whole_files) { } // Should recheck from the CDN, update last_checked and do nothing - ASSERT_FALSE(proj_is_download_needed(ctx, "ntf_r93.gsb", false)); + ASSERT_FALSE(proj_is_download_needed(ctx, "dvr90.gtx", false)); { sqlite3 *hDB = nullptr; @@ -1662,10 +1680,10 @@ TEST(networking, download_whole_files) { sqlite3_close(hDB); } - ASSERT_TRUE(proj_is_download_needed(ctx, "ntf_r93.gsb", false)); + ASSERT_TRUE(proj_is_download_needed(ctx, "dvr90.gtx", false)); // Redo download with a progress callback this time. - unlink("proj_test_tmp/ntf_r93.tif"); + unlink("proj_test_tmp/dvr90.tif"); const auto cbk = [](PJ_CONTEXT *l_ctx, double pct, void *user_data) -> int { auto vect = static_cast<std::vector<std::pair<PJ_CONTEXT *, double>> *>( @@ -1675,7 +1693,7 @@ TEST(networking, download_whole_files) { }; std::vector<std::pair<PJ_CONTEXT *, double>> vectPct; - ASSERT_TRUE(proj_download_file(ctx, "ntf_r93.gsb", false, cbk, &vectPct)); + ASSERT_TRUE(proj_download_file(ctx, "dvr90.gtx", false, cbk, &vectPct)); ASSERT_EQ(vectPct.size(), 3U); ASSERT_EQ(vectPct.back().first, ctx); ASSERT_EQ(vectPct.back().second, 1.0); @@ -1685,7 +1703,151 @@ TEST(networking, download_whole_files) { putenv(const_cast<char *>("PROJ_USER_WRITABLE_DIRECTORY=")); putenv(const_cast<char *>("PROJ_FULL_FILE_CHUNK_SIZE=")); unlink("proj_test_tmp/cache.db"); - unlink("proj_test_tmp/ntf_r93.tif"); + unlink("proj_test_tmp/dvr90.tif"); + rmdir("proj_test_tmp"); +} + +// --------------------------------------------------------------------------- + +TEST(networking, file_api) { + if (!networkAccessOK) { + return; + } + + proj_cleanup(); + unlink("proj_test_tmp/cache.db"); + unlink("proj_test_tmp/dvr90.tif"); + rmdir("proj_test_tmp"); + + putenv(const_cast<char *>("PROJ_IGNORE_USER_WRITABLE_DIRECTORY=")); + putenv(const_cast<char *>("PROJ_USER_WRITABLE_DIRECTORY=./proj_test_tmp")); + putenv(const_cast<char *>("PROJ_FULL_FILE_CHUNK_SIZE=30000")); + auto ctx = proj_context_create(); + proj_context_set_enable_network(ctx, true); + + struct UserData { + bool in_open = false; + bool in_read = false; + bool in_write = false; + bool in_seek = false; + bool in_tell = false; + bool in_close = false; + bool in_exists = false; + bool in_mkdir = false; + bool in_unlink = false; + bool in_rename = false; + }; + + struct PROJ_FILE_API api; + api.version = 1; + api.open_cbk = [](PJ_CONTEXT *, const char *filename, + PROJ_OPEN_ACCESS access, + void *user_data) -> PROJ_FILE_HANDLE * { + static_cast<UserData *>(user_data)->in_open = true; + return reinterpret_cast<PROJ_FILE_HANDLE *>(fopen( + filename, + access == PROJ_OPEN_ACCESS_READ_ONLY + ? "rb" + : access == PROJ_OPEN_ACCESS_READ_UPDATE ? "r+b" : "w+b")); + }; + api.read_cbk = [](PJ_CONTEXT *, PROJ_FILE_HANDLE *handle, void *buffer, + size_t sizeBytes, void *user_data) -> size_t { + static_cast<UserData *>(user_data)->in_read = true; + return fread(buffer, 1, sizeBytes, reinterpret_cast<FILE *>(handle)); + }; + api.write_cbk = [](PJ_CONTEXT *, PROJ_FILE_HANDLE *handle, + const void *buffer, size_t sizeBytes, + void *user_data) -> size_t { + static_cast<UserData *>(user_data)->in_write = true; + return fwrite(buffer, 1, sizeBytes, reinterpret_cast<FILE *>(handle)); + }; + api.seek_cbk = [](PJ_CONTEXT *, PROJ_FILE_HANDLE *handle, long long offset, + int whence, void *user_data) -> int { + static_cast<UserData *>(user_data)->in_seek = true; + return fseek(reinterpret_cast<FILE *>(handle), + static_cast<long>(offset), whence) == 0; + }; + api.tell_cbk = [](PJ_CONTEXT *, PROJ_FILE_HANDLE *handle, + void *user_data) -> unsigned long long { + static_cast<UserData *>(user_data)->in_tell = true; + return ftell(reinterpret_cast<FILE *>(handle)); + }; + api.close_cbk = [](PJ_CONTEXT *, PROJ_FILE_HANDLE *handle, + void *user_data) -> void { + static_cast<UserData *>(user_data)->in_close = true; + fclose(reinterpret_cast<FILE *>(handle)); + }; + api.exists_cbk = [](PJ_CONTEXT *, const char *filename, + void *user_data) -> int { + static_cast<UserData *>(user_data)->in_exists = true; + struct stat buf; + return stat(filename, &buf) == 0; + }; + api.mkdir_cbk = [](PJ_CONTEXT *, const char *filename, + void *user_data) -> int { + static_cast<UserData *>(user_data)->in_mkdir = true; +#ifdef _WIN32 + return mkdir(filename) == 0; +#else + return mkdir(filename, 0755) == 0; +#endif + }; + api.unlink_cbk = [](PJ_CONTEXT *, const char *filename, + void *user_data) -> int { + static_cast<UserData *>(user_data)->in_unlink = true; + return unlink(filename) == 0; + }; + api.rename_cbk = [](PJ_CONTEXT *, const char *oldPath, const char *newPath, + void *user_data) -> int { + static_cast<UserData *>(user_data)->in_rename = true; + return rename(oldPath, newPath) == 0; + }; + + UserData userData; + ASSERT_TRUE(proj_context_set_fileapi(ctx, &api, &userData)); + + ASSERT_TRUE(proj_is_download_needed(ctx, "dvr90.gtx", false)); + + ASSERT_TRUE(proj_download_file(ctx, "dvr90.gtx", false, nullptr, nullptr)); + + ASSERT_TRUE(userData.in_open); + ASSERT_FALSE(userData.in_read); + ASSERT_TRUE(userData.in_write); + ASSERT_TRUE(userData.in_close); + ASSERT_TRUE(userData.in_exists); + ASSERT_TRUE(userData.in_mkdir); + ASSERT_TRUE(userData.in_unlink); + ASSERT_TRUE(userData.in_rename); + + proj_context_set_enable_network(ctx, false); + + const char *pipeline = + "+proj=pipeline " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=dvr90.gtx +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"; + + auto P = proj_create(ctx, pipeline); + ASSERT_NE(P, nullptr); + + double lon = 12; + double lat = 56; + double z = 0; + proj_trans_generic(P, PJ_FWD, &lon, sizeof(double), 1, &lat, sizeof(double), + 1, &z, sizeof(double), 1, nullptr, 0, 0); + EXPECT_NEAR(z, 36.5909996032715, 1e-10); + + proj_destroy(P); + + ASSERT_TRUE(userData.in_read); + ASSERT_TRUE(userData.in_seek); + + proj_context_destroy(ctx); + putenv(const_cast<char *>("PROJ_IGNORE_USER_WRITABLE_DIRECTORY=YES")); + putenv(const_cast<char *>("PROJ_USER_WRITABLE_DIRECTORY=")); + putenv(const_cast<char *>("PROJ_FULL_FILE_CHUNK_SIZE=")); + unlink("proj_test_tmp/cache.db"); + unlink("proj_test_tmp/dvr90.tif"); rmdir("proj_test_tmp"); } diff --git a/travis/mingw32/install.sh b/travis/mingw32/install.sh index abe79c7b..8c756ced 100755 --- a/travis/mingw32/install.sh +++ b/travis/mingw32/install.sh @@ -53,6 +53,7 @@ make install make dist-all find /tmp/proj_autoconf_install (cd test; make -j2) +cp -r ../data/tests /tmp/proj_autoconf_install/share/proj test/unit/test_cpp_api.exe cd .. # Now with grids |
