aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--scripts/reference_exported_symbols.txt2
-rw-r--r--src/filemanager.cpp206
-rw-r--r--src/iso19111/factory.cpp28
-rw-r--r--src/proj.h61
-rw-r--r--src/proj_internal.h20
-rw-r--r--test/unit/test_c_api.cpp22
-rw-r--r--test/unit/test_network.cpp144
7 files changed, 461 insertions, 22 deletions
diff --git a/scripts/reference_exported_symbols.txt b/scripts/reference_exported_symbols.txt
index d10f3bc1..dd66197a 100644
--- a/scripts/reference_exported_symbols.txt
+++ b/scripts/reference_exported_symbols.txt
@@ -798,10 +798,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/filemanager.cpp b/src/filemanager.cpp
index d2fce822..1857e397 100644
--- a/src/filemanager.cpp
+++ b/src/filemanager.cpp
@@ -834,6 +834,92 @@ FileLegacyAdapter::open(PJ_CONTEXT *ctx, const char *filename, FileAccess) {
// ---------------------------------------------------------------------------
+class FileApiAdapter : public File {
+ PJ_CONTEXT *m_ctx;
+ PROJ_FILE_HANDLE *m_fp;
+
+ FileApiAdapter(const FileApiAdapter &) = delete;
+ FileApiAdapter &operator=(const FileApiAdapter &) = delete;
+
+ protected:
+ FileApiAdapter(const std::string &name, PJ_CONTEXT *ctx,
+ PROJ_FILE_HANDLE *fp)
+ : File(name), m_ctx(ctx), m_fp(fp) {}
+
+ public:
+ ~FileApiAdapter() override;
+
+ size_t read(void *buffer, size_t sizeBytes) 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 { m_ctx = ctx; }
+
+ // We may lie, but the real use case is only for network files
+ bool hasChanged() const override { return false; }
+
+ static std::unique_ptr<File> open(PJ_CONTEXT *ctx, const char *filename,
+ FileAccess access);
+};
+
+// ---------------------------------------------------------------------------
+
+FileApiAdapter::~FileApiAdapter() {
+ m_ctx->fileApi.close_cbk(m_ctx, m_fp, m_ctx->fileApi.user_data);
+}
+
+// ---------------------------------------------------------------------------
+
+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 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 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 FileApiAdapter::tell() {
+ return m_ctx->fileApi.tell_cbk(m_ctx, m_fp, m_ctx->fileApi.user_data);
+}
+
+// ---------------------------------------------------------------------------
+
+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);
+}
+
+// ---------------------------------------------------------------------------
+
constexpr size_t DOWNLOAD_CHUNK_SIZE = 16 * 1024;
constexpr int MAX_CHUNKS = 64;
@@ -990,12 +1076,20 @@ std::unique_ptr<DiskChunkCache> DiskChunkCache::open(PJ_CONTEXT *ctx) {
// ---------------------------------------------------------------------------
DiskChunkCache::DiskChunkCache(PJ_CONTEXT *ctx, const std::string &path)
- : ctx_(ctx), path_(path), vfs_(SQLite3VFS::create(true, false, false)) {
- if (vfs_ == nullptr) {
- return;
+ : ctx_(ctx), path_(path) {
+ std::string vfsName;
+ if (ctx->custom_sqlite3_vfs_name.empty()) {
+ vfs_ = SQLite3VFS::create(true, false, false);
+ if (vfs_ == nullptr) {
+ return;
+ }
+ vfsName = vfs_->name();
+ } else {
+ vfsName = ctx->custom_sqlite3_vfs_name;
}
sqlite3_open_v2(path.c_str(), &hDB_,
- SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, vfs_->name());
+ SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
+ vfsName.c_str());
if (!hDB_) {
return;
}
@@ -2206,12 +2300,6 @@ void NetworkFile::reassign_context(PJ_CONTEXT *ctx) {
std::unique_ptr<File> FileManager::open(PJ_CONTEXT *ctx, const char *filename,
FileAccess access) {
-#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);
- }
-#endif
if (starts_with(filename, "http://") || starts_with(filename, "https://")) {
if (!pj_context_is_network_enabled(ctx)) {
pj_log(
@@ -2223,6 +2311,15 @@ std::unique_ptr<File> FileManager::open(PJ_CONTEXT *ctx, const char *filename,
}
return NetworkFile::open(ctx, 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, access);
+ }
+#endif
+ if (ctx->fileApi.open_cbk != nullptr) {
+ return FileApiAdapter::open(ctx, filename, access);
+ }
#ifdef _WIN32
return FileWin32::open(ctx, filename, access);
#else
@@ -2233,6 +2330,11 @@ std::unique_ptr<File> FileManager::open(PJ_CONTEXT *ctx, const char *filename,
// ---------------------------------------------------------------------------
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;
+ }
+
#ifdef _WIN32
struct __stat64 buf;
try {
@@ -2251,6 +2353,11 @@ bool FileManager::exists(PJ_CONTEXT *ctx, const char *filename) {
// ---------------------------------------------------------------------------
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;
+ }
+
#ifdef _WIN32
try {
return _wmkdir(UTF8ToWString(filename).c_str()) == 0;
@@ -2267,6 +2374,11 @@ bool FileManager::mkdir(PJ_CONTEXT *ctx, const char *filename) {
// ---------------------------------------------------------------------------
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;
+ }
+
#ifdef _WIN32
try {
return _wunlink(UTF8ToWString(filename).c_str()) == 0;
@@ -2284,6 +2396,11 @@ bool FileManager::unlink(PJ_CONTEXT *ctx, const char *filename) {
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;
+ }
+
#ifdef _WIN32
try {
return _wrename(UTF8ToWString(oldPath).c_str(),
@@ -2750,6 +2867,75 @@ NS_PROJ_END
// ---------------------------------------------------------------------------
+/** 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.
+ *
+ * \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 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.
+ */
+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 (!fileapi) {
+ return false;
+ }
+ if (fileapi->version != 1) {
+ return false;
+ }
+ 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;
+ }
+ 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;
+}
+
+// ---------------------------------------------------------------------------
+
+/** 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.
+ */
+void proj_context_set_sqlite3_vfs_name(PJ_CONTEXT *ctx, const char *name) {
+ if (ctx == nullptr) {
+ ctx = pj_get_default_ctx();
+ }
+ ctx->custom_sqlite3_vfs_name = name ? name : std::string();
+}
+
+// ---------------------------------------------------------------------------
+
/** Define a custom set of callbacks for network access.
*
* All callbacks should be provided (non NULL pointers).
diff --git a/src/iso19111/factory.cpp b/src/iso19111/factory.cpp
index 7fb248c6..6345ecf4 100644
--- a/src/iso19111/factory.cpp
+++ b/src/iso19111/factory.cpp
@@ -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/proj.h b/src/proj.h
index 8add29ac..db436173 100644
--- a/src/proj.h
+++ b/src/proj.h
@@ -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
+{
+ /** 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 fb8f294c..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;
@@ -707,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/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_network.cpp b/test/unit/test_network.cpp
index e959fd54..688e61e5 100644
--- a/test/unit/test_network.cpp
+++ b/test/unit/test_network.cpp
@@ -1707,6 +1707,150 @@ TEST(networking, download_whole_files) {
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");
+}
+
#endif
} // namespace