Coverage Report

Created: 2026-03-22 06:35

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/PROJ/src/networkfilemanager.cpp
Line
Count
Source
1
/******************************************************************************
2
 * Project:  PROJ
3
 * Purpose:  Functionality related to network access and caching
4
 * Author:   Even Rouault, <even.rouault at spatialys.com>
5
 *
6
 ******************************************************************************
7
 * Copyright (c) 2019-2020, Even Rouault, <even.rouault at spatialys.com>
8
 *
9
 * Permission is hereby granted, free of charge, to any person obtaining a
10
 * copy of this software and associated documentation files (the "Software"),
11
 * to deal in the Software without restriction, including without limitation
12
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
13
 * and/or sell copies of the Software, and to permit persons to whom the
14
 * Software is furnished to do so, subject to the following conditions:
15
 *
16
 * The above copyright notice and this permission notice shall be included
17
 * in all copies or substantial portions of the Software.
18
 *
19
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
22
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25
 * DEALINGS IN THE SOFTWARE.
26
 *****************************************************************************/
27
28
#ifndef FROM_PROJ_CPP
29
#define FROM_PROJ_CPP
30
#endif
31
#define LRU11_DO_NOT_DEFINE_OUT_OF_CLASS_METHODS
32
33
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(_GNU_SOURCE)
34
// For usleep() on Cygwin
35
#define _GNU_SOURCE
36
#endif
37
38
#if defined(EMSCRIPTEN_FETCH_ENABLED)
39
#ifndef __EMSCRIPTEN_PTHREADS__
40
#error "__EMSCRIPTEN_PTHREADS__ not defined. Are you setting -pthread?"
41
#endif
42
#ifdef CURL_ENABLED
43
#error "EMSCRIPTEN_FETCH is not compatible with CURL. Use only one."
44
#endif
45
#define DO_EMSCRIPTEN_FETCH
46
#endif
47
48
#include <stdlib.h>
49
50
#include <algorithm>
51
#include <limits>
52
#include <mutex>
53
#include <string>
54
55
#include "filemanager.hpp"
56
#include "proj.h"
57
#include "proj/internal/internal.hpp"
58
#include "proj/internal/io_internal.hpp"
59
#include "proj/internal/lru_cache.hpp"
60
#include "proj_internal.h"
61
#include "sqlite3_utils.hpp"
62
63
#ifdef CURL_ENABLED
64
#include <curl/curl.h>
65
#include <sqlite3.h> // for sqlite3_snprintf
66
#endif
67
68
#ifdef DO_EMSCRIPTEN_FETCH
69
#include <emscripten/fetch.h>
70
#include <iostream>
71
#include <sqlite3.h> // for sqlite3_snprintf
72
#include <unordered_map>
73
#endif
74
75
#include <sys/stat.h>
76
77
#ifdef _WIN32
78
#include <shlobj.h>
79
#else
80
#include <sys/types.h>
81
#include <unistd.h>
82
#endif
83
84
#if defined(_WIN32)
85
#include <windows.h>
86
#elif defined(__MACH__) && defined(__APPLE__)
87
#include <mach-o/dyld.h>
88
#elif defined(__FreeBSD__)
89
#include <sys/sysctl.h>
90
#include <sys/types.h>
91
#endif
92
93
#include <time.h>
94
95
//! @cond Doxygen_Suppress
96
97
0
#define STR_HELPER(x) #x
98
0
#define STR(x) STR_HELPER(x)
99
100
using namespace NS_PROJ::internal;
101
102
NS_PROJ_START
103
104
// ---------------------------------------------------------------------------
105
106
0
static void sleep_ms(int ms) {
107
#ifdef _WIN32
108
    Sleep(ms);
109
#else
110
0
    usleep(ms * 1000);
111
0
#endif
112
0
}
113
114
// ---------------------------------------------------------------------------
115
116
constexpr size_t DOWNLOAD_CHUNK_SIZE = 16 * 1024;
117
constexpr int MAX_CHUNKS = 64;
118
119
struct FileProperties {
120
    unsigned long long size = 0;
121
    time_t lastChecked = 0;
122
    std::string lastModified{};
123
    std::string etag{};
124
};
125
126
class NetworkChunkCache {
127
  public:
128
    void insert(PJ_CONTEXT *ctx, const std::string &url,
129
                unsigned long long chunkIdx, std::vector<unsigned char> &&data);
130
131
    std::shared_ptr<std::vector<unsigned char>>
132
    get(PJ_CONTEXT *ctx, const std::string &url, unsigned long long chunkIdx);
133
134
    std::shared_ptr<std::vector<unsigned char>> get(PJ_CONTEXT *ctx,
135
                                                    const std::string &url,
136
                                                    unsigned long long chunkIdx,
137
                                                    FileProperties &props);
138
139
    void clearMemoryCache();
140
141
    static void clearDiskChunkCache(PJ_CONTEXT *ctx);
142
143
  private:
144
    struct Key {
145
        std::string url;
146
        unsigned long long chunkIdx;
147
148
        Key(const std::string &urlIn, unsigned long long chunkIdxIn)
149
0
            : url(urlIn), chunkIdx(chunkIdxIn) {}
150
0
        bool operator==(const Key &other) const {
151
0
            return url == other.url && chunkIdx == other.chunkIdx;
152
0
        }
153
    };
154
155
    struct KeyHasher {
156
0
        std::size_t operator()(const Key &k) const {
157
0
            return std::hash<std::string>{}(k.url) ^
158
0
                   (std::hash<unsigned long long>{}(k.chunkIdx) << 1);
159
0
        }
160
    };
161
162
    lru11::Cache<
163
        Key, std::shared_ptr<std::vector<unsigned char>>, std::mutex,
164
        std::unordered_map<
165
            Key,
166
            typename std::list<lru11::KeyValuePair<
167
                Key, std::shared_ptr<std::vector<unsigned char>>>>::iterator,
168
            KeyHasher>>
169
        cache_{MAX_CHUNKS};
170
};
171
172
// ---------------------------------------------------------------------------
173
174
static NetworkChunkCache gNetworkChunkCache{};
175
176
// ---------------------------------------------------------------------------
177
178
class NetworkFilePropertiesCache {
179
  public:
180
    void insert(PJ_CONTEXT *ctx, const std::string &url, FileProperties &props);
181
182
    bool tryGet(PJ_CONTEXT *ctx, const std::string &url, FileProperties &props);
183
184
    void clearMemoryCache();
185
186
  private:
187
    lru11::Cache<std::string, FileProperties, std::mutex> cache_{};
188
};
189
190
// ---------------------------------------------------------------------------
191
192
static NetworkFilePropertiesCache gNetworkFileProperties{};
193
194
// ---------------------------------------------------------------------------
195
196
class DiskChunkCache {
197
    PJ_CONTEXT *ctx_ = nullptr;
198
    std::string path_{};
199
    sqlite3 *hDB_ = nullptr;
200
    std::unique_ptr<SQLite3VFS> vfs_{};
201
202
    explicit DiskChunkCache(PJ_CONTEXT *ctx, const std::string &path);
203
204
    bool initialize();
205
    void commitAndClose();
206
207
    bool createDBStructure();
208
    bool checkConsistency();
209
    bool get_links(sqlite3_int64 chunk_id, sqlite3_int64 &link_id,
210
                   sqlite3_int64 &prev, sqlite3_int64 &next,
211
                   sqlite3_int64 &head, sqlite3_int64 &tail);
212
    bool update_links_of_prev_and_next_links(sqlite3_int64 prev,
213
                                             sqlite3_int64 next);
214
    bool update_linked_chunks(sqlite3_int64 link_id, sqlite3_int64 prev,
215
                              sqlite3_int64 next);
216
    bool update_linked_chunks_head_tail(sqlite3_int64 head, sqlite3_int64 tail);
217
218
    DiskChunkCache(const DiskChunkCache &) = delete;
219
    DiskChunkCache &operator=(const DiskChunkCache &) = delete;
220
221
  public:
222
    static std::unique_ptr<DiskChunkCache> open(PJ_CONTEXT *ctx);
223
    ~DiskChunkCache();
224
225
0
    sqlite3 *handle() { return hDB_; }
226
    std::unique_ptr<SQLiteStatement> prepare(const char *sql);
227
    bool move_to_head(sqlite3_int64 chunk_id);
228
    bool move_to_tail(sqlite3_int64 chunk_id);
229
    void closeAndUnlink();
230
};
231
232
// ---------------------------------------------------------------------------
233
234
0
static bool pj_context_get_grid_cache_is_enabled(PJ_CONTEXT *ctx) {
235
0
    pj_load_ini(ctx);
236
0
    return ctx->gridChunkCache.enabled;
237
0
}
238
239
// ---------------------------------------------------------------------------
240
241
0
static long long pj_context_get_grid_cache_max_size(PJ_CONTEXT *ctx) {
242
0
    pj_load_ini(ctx);
243
0
    return ctx->gridChunkCache.max_size;
244
0
}
245
246
// ---------------------------------------------------------------------------
247
248
0
static int pj_context_get_grid_cache_ttl(PJ_CONTEXT *ctx) {
249
0
    pj_load_ini(ctx);
250
0
    return ctx->gridChunkCache.ttl;
251
0
}
252
253
// ---------------------------------------------------------------------------
254
255
0
std::unique_ptr<DiskChunkCache> DiskChunkCache::open(PJ_CONTEXT *ctx) {
256
0
    if (!pj_context_get_grid_cache_is_enabled(ctx)) {
257
0
        return nullptr;
258
0
    }
259
0
    const auto cachePath = pj_context_get_grid_cache_filename(ctx);
260
0
    if (cachePath.empty()) {
261
0
        return nullptr;
262
0
    }
263
264
0
    auto diskCache =
265
0
        std::unique_ptr<DiskChunkCache>(new DiskChunkCache(ctx, cachePath));
266
0
    if (!diskCache->initialize())
267
0
        diskCache.reset();
268
0
    return diskCache;
269
0
}
270
271
// ---------------------------------------------------------------------------
272
273
DiskChunkCache::DiskChunkCache(PJ_CONTEXT *ctx, const std::string &path)
274
0
    : ctx_(ctx), path_(path) {}
275
276
// ---------------------------------------------------------------------------
277
278
0
bool DiskChunkCache::initialize() {
279
0
    std::string vfsName;
280
0
    if (ctx_->custom_sqlite3_vfs_name.empty()) {
281
0
        vfs_ = SQLite3VFS::create(true, false, false);
282
0
        if (vfs_ == nullptr) {
283
0
            return false;
284
0
        }
285
0
        vfsName = vfs_->name();
286
0
    } else {
287
0
        vfsName = ctx_->custom_sqlite3_vfs_name;
288
0
    }
289
0
    sqlite3_open_v2(path_.c_str(), &hDB_,
290
0
                    SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
291
0
                    vfsName.c_str());
292
0
    if (!hDB_) {
293
0
        pj_log(ctx_, PJ_LOG_ERROR, "Cannot open %s", path_.c_str());
294
0
        return false;
295
0
    }
296
297
    // Cannot run more than 30 times / a bit more than one second.
298
0
    for (int i = 0;; i++) {
299
0
        int ret =
300
0
            sqlite3_exec(hDB_, "BEGIN EXCLUSIVE", nullptr, nullptr, nullptr);
301
0
        if (ret == SQLITE_OK) {
302
0
            break;
303
0
        }
304
0
        if (ret != SQLITE_BUSY) {
305
0
            pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_));
306
0
            sqlite3_close(hDB_);
307
0
            hDB_ = nullptr;
308
0
            return false;
309
0
        }
310
0
        const char *max_iters = getenv("PROJ_LOCK_MAX_ITERS");
311
0
        if (i >= (max_iters && max_iters[0] ? atoi(max_iters)
312
0
                                            : 30)) { // A bit more than 1 second
313
0
            pj_log(ctx_, PJ_LOG_ERROR, "Cannot take exclusive lock on %s",
314
0
                   path_.c_str());
315
0
            sqlite3_close(hDB_);
316
0
            hDB_ = nullptr;
317
0
            return false;
318
0
        }
319
0
        pj_log(ctx_, PJ_LOG_TRACE, "Lock taken on cache. Waiting a bit...");
320
        // Retry every 5 ms for 50 ms, then every 10 ms for 100 ms, then
321
        // every 100 ms
322
0
        sleep_ms(i < 10 ? 5 : i < 20 ? 10 : 100);
323
0
    }
324
0
    char **pasResult = nullptr;
325
0
    int nRows = 0;
326
0
    int nCols = 0;
327
0
    sqlite3_get_table(hDB_,
328
0
                      "SELECT 1 FROM sqlite_master WHERE name = 'properties'",
329
0
                      &pasResult, &nRows, &nCols, nullptr);
330
0
    sqlite3_free_table(pasResult);
331
0
    if (nRows == 0) {
332
0
        if (!createDBStructure()) {
333
0
            sqlite3_close(hDB_);
334
0
            hDB_ = nullptr;
335
0
            return false;
336
0
        }
337
0
    }
338
339
0
    if (getenv("PROJ_CHECK_CACHE_CONSISTENCY")) {
340
0
        checkConsistency();
341
0
    }
342
0
    return true;
343
0
}
344
345
// ---------------------------------------------------------------------------
346
347
static const char *cache_db_structure_sql =
348
    "CREATE TABLE properties("
349
    " url          TEXT PRIMARY KEY NOT NULL,"
350
    " lastChecked  TIMESTAMP NOT NULL,"
351
    " fileSize     INTEGER NOT NULL,"
352
    " lastModified TEXT,"
353
    " etag         TEXT"
354
    ");"
355
    "CREATE TABLE downloaded_file_properties("
356
    " url          TEXT PRIMARY KEY NOT NULL,"
357
    " lastChecked  TIMESTAMP NOT NULL,"
358
    " fileSize     INTEGER NOT NULL,"
359
    " lastModified TEXT,"
360
    " etag         TEXT"
361
    ");"
362
    "CREATE TABLE chunk_data("
363
    " id        INTEGER PRIMARY KEY AUTOINCREMENT CHECK (id > 0),"
364
    " data      BLOB NOT NULL"
365
    ");"
366
    "CREATE TABLE chunks("
367
    " id        INTEGER PRIMARY KEY AUTOINCREMENT CHECK (id > 0),"
368
    " url       TEXT NOT NULL,"
369
    " offset    INTEGER NOT NULL,"
370
    " data_id   INTEGER NOT NULL,"
371
    " data_size INTEGER NOT NULL,"
372
    " CONSTRAINT fk_chunks_url FOREIGN KEY (url) REFERENCES properties(url),"
373
    " CONSTRAINT fk_chunks_data FOREIGN KEY (data_id) REFERENCES chunk_data(id)"
374
    ");"
375
    "CREATE INDEX idx_chunks ON chunks(url, offset);"
376
    "CREATE TABLE linked_chunks("
377
    " id        INTEGER PRIMARY KEY AUTOINCREMENT CHECK (id > 0),"
378
    " chunk_id  INTEGER NOT NULL,"
379
    " prev      INTEGER,"
380
    " next      INTEGER,"
381
    " CONSTRAINT fk_links_chunkid FOREIGN KEY (chunk_id) REFERENCES chunks(id),"
382
    " CONSTRAINT fk_links_prev FOREIGN KEY (prev) REFERENCES linked_chunks(id),"
383
    " CONSTRAINT fk_links_next FOREIGN KEY (next) REFERENCES linked_chunks(id)"
384
    ");"
385
    "CREATE INDEX idx_linked_chunks_chunk_id ON linked_chunks(chunk_id);"
386
    "CREATE TABLE linked_chunks_head_tail("
387
    "  head       INTEGER,"
388
    "  tail       INTEGER,"
389
    "  CONSTRAINT lht_head FOREIGN KEY (head) REFERENCES linked_chunks(id),"
390
    "  CONSTRAINT lht_tail FOREIGN KEY (tail) REFERENCES linked_chunks(id)"
391
    ");"
392
    "INSERT INTO linked_chunks_head_tail VALUES (NULL, NULL);";
393
394
0
bool DiskChunkCache::createDBStructure() {
395
396
0
    pj_log(ctx_, PJ_LOG_TRACE, "Creating cache DB structure");
397
0
    if (sqlite3_exec(hDB_, cache_db_structure_sql, nullptr, nullptr, nullptr) !=
398
0
        SQLITE_OK) {
399
0
        pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_));
400
0
        return false;
401
0
    }
402
0
    return true;
403
0
}
404
405
// ---------------------------------------------------------------------------
406
407
// Used by checkConsistency() and insert()
408
0
#define INVALIDATED_SQL_LITERAL "'invalidated'"
409
410
0
bool DiskChunkCache::checkConsistency() {
411
412
0
    auto stmt = prepare("SELECT * FROM chunk_data WHERE id NOT IN (SELECT "
413
0
                        "data_id FROM chunks)");
414
0
    if (!stmt) {
415
0
        return false;
416
0
    }
417
0
    if (stmt->execute() != SQLITE_DONE) {
418
0
        fprintf(stderr, "Rows in chunk_data not referenced by chunks.\n");
419
0
        return false;
420
0
    }
421
422
0
    stmt = prepare("SELECT * FROM chunks WHERE id NOT IN (SELECT chunk_id FROM "
423
0
                   "linked_chunks)");
424
0
    if (!stmt) {
425
0
        return false;
426
0
    }
427
0
    if (stmt->execute() != SQLITE_DONE) {
428
0
        fprintf(stderr, "Rows in chunks not referenced by linked_chunks.\n");
429
0
        return false;
430
0
    }
431
432
0
    stmt = prepare("SELECT * FROM chunks WHERE url <> " INVALIDATED_SQL_LITERAL
433
0
                   " AND url "
434
0
                   "NOT IN (SELECT url FROM properties)");
435
0
    if (!stmt) {
436
0
        return false;
437
0
    }
438
0
    if (stmt->execute() != SQLITE_DONE) {
439
0
        fprintf(stderr, "url values in chunks not referenced by properties.\n");
440
0
        return false;
441
0
    }
442
443
0
    stmt = prepare("SELECT head, tail FROM linked_chunks_head_tail");
444
0
    if (!stmt) {
445
0
        return false;
446
0
    }
447
0
    if (stmt->execute() != SQLITE_ROW) {
448
0
        fprintf(stderr, "linked_chunks_head_tail empty.\n");
449
0
        return false;
450
0
    }
451
0
    const auto head = stmt->getInt64();
452
0
    const auto tail = stmt->getInt64();
453
0
    if (stmt->execute() != SQLITE_DONE) {
454
0
        fprintf(stderr, "linked_chunks_head_tail has more than one row.\n");
455
0
        return false;
456
0
    }
457
458
0
    stmt = prepare("SELECT COUNT(*) FROM linked_chunks");
459
0
    if (!stmt) {
460
0
        return false;
461
0
    }
462
0
    if (stmt->execute() != SQLITE_ROW) {
463
0
        fprintf(stderr, "linked_chunks_head_tail empty.\n");
464
0
        return false;
465
0
    }
466
0
    const auto count_linked_chunks = stmt->getInt64();
467
468
0
    if (head) {
469
0
        auto id = head;
470
0
        std::set<sqlite3_int64> visitedIds;
471
0
        stmt = prepare("SELECT next FROM linked_chunks WHERE id = ?");
472
0
        if (!stmt) {
473
0
            return false;
474
0
        }
475
0
        while (true) {
476
0
            visitedIds.insert(id);
477
0
            stmt->reset();
478
0
            stmt->bindInt64(id);
479
0
            if (stmt->execute() != SQLITE_ROW) {
480
0
                fprintf(stderr, "cannot find linked_chunks.id = %d.\n",
481
0
                        static_cast<int>(id));
482
0
                return false;
483
0
            }
484
0
            auto next = stmt->getInt64();
485
0
            if (next == 0) {
486
0
                if (id != tail) {
487
0
                    fprintf(stderr,
488
0
                            "last item when following next is not tail.\n");
489
0
                    return false;
490
0
                }
491
0
                break;
492
0
            }
493
0
            if (visitedIds.find(next) != visitedIds.end()) {
494
0
                fprintf(stderr, "found cycle on linked_chunks.next = %d.\n",
495
0
                        static_cast<int>(next));
496
0
                return false;
497
0
            }
498
0
            id = next;
499
0
        }
500
0
        if (visitedIds.size() != static_cast<size_t>(count_linked_chunks)) {
501
0
            fprintf(stderr,
502
0
                    "ghost items in linked_chunks when following next.\n");
503
0
            return false;
504
0
        }
505
0
    } else if (count_linked_chunks) {
506
0
        fprintf(stderr, "linked_chunks_head_tail.head = NULL but linked_chunks "
507
0
                        "not empty.\n");
508
0
        return false;
509
0
    }
510
511
0
    if (tail) {
512
0
        auto id = tail;
513
0
        std::set<sqlite3_int64> visitedIds;
514
0
        stmt = prepare("SELECT prev FROM linked_chunks WHERE id = ?");
515
0
        if (!stmt) {
516
0
            return false;
517
0
        }
518
0
        while (true) {
519
0
            visitedIds.insert(id);
520
0
            stmt->reset();
521
0
            stmt->bindInt64(id);
522
0
            if (stmt->execute() != SQLITE_ROW) {
523
0
                fprintf(stderr, "cannot find linked_chunks.id = %d.\n",
524
0
                        static_cast<int>(id));
525
0
                return false;
526
0
            }
527
0
            auto prev = stmt->getInt64();
528
0
            if (prev == 0) {
529
0
                if (id != head) {
530
0
                    fprintf(stderr,
531
0
                            "last item when following prev is not head.\n");
532
0
                    return false;
533
0
                }
534
0
                break;
535
0
            }
536
0
            if (visitedIds.find(prev) != visitedIds.end()) {
537
0
                fprintf(stderr, "found cycle on linked_chunks.prev = %d.\n",
538
0
                        static_cast<int>(prev));
539
0
                return false;
540
0
            }
541
0
            id = prev;
542
0
        }
543
0
        if (visitedIds.size() != static_cast<size_t>(count_linked_chunks)) {
544
0
            fprintf(stderr,
545
0
                    "ghost items in linked_chunks when following prev.\n");
546
0
            return false;
547
0
        }
548
0
    } else if (count_linked_chunks) {
549
0
        fprintf(stderr, "linked_chunks_head_tail.tail = NULL but linked_chunks "
550
0
                        "not empty.\n");
551
0
        return false;
552
0
    }
553
554
0
    fprintf(stderr, "check ok\n");
555
0
    return true;
556
0
}
557
558
// ---------------------------------------------------------------------------
559
560
0
void DiskChunkCache::commitAndClose() {
561
0
    if (hDB_) {
562
0
        if (sqlite3_exec(hDB_, "COMMIT", nullptr, nullptr, nullptr) !=
563
0
            SQLITE_OK) {
564
0
            pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_));
565
0
        }
566
0
        sqlite3_close(hDB_);
567
0
        hDB_ = nullptr;
568
0
    }
569
0
}
570
571
// ---------------------------------------------------------------------------
572
573
0
DiskChunkCache::~DiskChunkCache() { commitAndClose(); }
574
575
// ---------------------------------------------------------------------------
576
577
0
void DiskChunkCache::closeAndUnlink() {
578
0
    commitAndClose();
579
0
    if (vfs_) {
580
0
        vfs_->raw()->xDelete(vfs_->raw(), path_.c_str(), 0);
581
0
    }
582
0
}
583
584
// ---------------------------------------------------------------------------
585
586
0
std::unique_ptr<SQLiteStatement> DiskChunkCache::prepare(const char *sql) {
587
0
    sqlite3_stmt *hStmt = nullptr;
588
0
    sqlite3_prepare_v2(hDB_, sql, -1, &hStmt, nullptr);
589
0
    if (!hStmt) {
590
0
        pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_));
591
0
        return nullptr;
592
0
    }
593
0
    return std::unique_ptr<SQLiteStatement>(new SQLiteStatement(hStmt));
594
0
}
595
596
// ---------------------------------------------------------------------------
597
598
bool DiskChunkCache::get_links(sqlite3_int64 chunk_id, sqlite3_int64 &link_id,
599
                               sqlite3_int64 &prev, sqlite3_int64 &next,
600
0
                               sqlite3_int64 &head, sqlite3_int64 &tail) {
601
0
    auto stmt =
602
0
        prepare("SELECT id, prev, next FROM linked_chunks WHERE chunk_id = ?");
603
0
    if (!stmt)
604
0
        return false;
605
0
    stmt->bindInt64(chunk_id);
606
0
    {
607
0
        const auto ret = stmt->execute();
608
0
        if (ret != SQLITE_ROW) {
609
0
            pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_));
610
0
            return false;
611
0
        }
612
0
    }
613
0
    link_id = stmt->getInt64();
614
0
    prev = stmt->getInt64();
615
0
    next = stmt->getInt64();
616
617
0
    stmt = prepare("SELECT head, tail FROM linked_chunks_head_tail");
618
0
    {
619
0
        const auto ret = stmt->execute();
620
0
        if (ret != SQLITE_ROW) {
621
0
            pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_));
622
0
            return false;
623
0
        }
624
0
    }
625
0
    head = stmt->getInt64();
626
0
    tail = stmt->getInt64();
627
0
    return true;
628
0
}
629
630
// ---------------------------------------------------------------------------
631
632
bool DiskChunkCache::update_links_of_prev_and_next_links(sqlite3_int64 prev,
633
0
                                                         sqlite3_int64 next) {
634
0
    if (prev) {
635
0
        auto stmt = prepare("UPDATE linked_chunks SET next = ? WHERE id = ?");
636
0
        if (!stmt)
637
0
            return false;
638
0
        if (next)
639
0
            stmt->bindInt64(next);
640
0
        else
641
0
            stmt->bindNull();
642
0
        stmt->bindInt64(prev);
643
0
        const auto ret = stmt->execute();
644
0
        if (ret != SQLITE_DONE) {
645
0
            pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_));
646
0
            return false;
647
0
        }
648
0
    }
649
650
0
    if (next) {
651
0
        auto stmt = prepare("UPDATE linked_chunks SET prev = ? WHERE id = ?");
652
0
        if (!stmt)
653
0
            return false;
654
0
        if (prev)
655
0
            stmt->bindInt64(prev);
656
0
        else
657
0
            stmt->bindNull();
658
0
        stmt->bindInt64(next);
659
0
        const auto ret = stmt->execute();
660
0
        if (ret != SQLITE_DONE) {
661
0
            pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_));
662
0
            return false;
663
0
        }
664
0
    }
665
0
    return true;
666
0
}
667
668
// ---------------------------------------------------------------------------
669
670
bool DiskChunkCache::update_linked_chunks(sqlite3_int64 link_id,
671
                                          sqlite3_int64 prev,
672
0
                                          sqlite3_int64 next) {
673
0
    auto stmt =
674
0
        prepare("UPDATE linked_chunks SET prev = ?, next = ? WHERE id = ?");
675
0
    if (!stmt)
676
0
        return false;
677
0
    if (prev)
678
0
        stmt->bindInt64(prev);
679
0
    else
680
0
        stmt->bindNull();
681
0
    if (next)
682
0
        stmt->bindInt64(next);
683
0
    else
684
0
        stmt->bindNull();
685
0
    stmt->bindInt64(link_id);
686
0
    const auto ret = stmt->execute();
687
0
    if (ret != SQLITE_DONE) {
688
0
        pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_));
689
0
        return false;
690
0
    }
691
0
    return true;
692
0
}
693
694
// ---------------------------------------------------------------------------
695
696
bool DiskChunkCache::update_linked_chunks_head_tail(sqlite3_int64 head,
697
0
                                                    sqlite3_int64 tail) {
698
0
    auto stmt =
699
0
        prepare("UPDATE linked_chunks_head_tail SET head = ?, tail = ?");
700
0
    if (!stmt)
701
0
        return false;
702
0
    if (head)
703
0
        stmt->bindInt64(head);
704
0
    else
705
0
        stmt->bindNull(); // shouldn't happen normally
706
0
    if (tail)
707
0
        stmt->bindInt64(tail);
708
0
    else
709
0
        stmt->bindNull(); // shouldn't happen normally
710
0
    const auto ret = stmt->execute();
711
0
    if (ret != SQLITE_DONE) {
712
0
        pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_));
713
0
        return false;
714
0
    }
715
0
    return true;
716
0
}
717
718
// ---------------------------------------------------------------------------
719
720
0
bool DiskChunkCache::move_to_head(sqlite3_int64 chunk_id) {
721
722
0
    sqlite3_int64 link_id = 0;
723
0
    sqlite3_int64 prev = 0;
724
0
    sqlite3_int64 next = 0;
725
0
    sqlite3_int64 head = 0;
726
0
    sqlite3_int64 tail = 0;
727
0
    if (!get_links(chunk_id, link_id, prev, next, head, tail)) {
728
0
        return false;
729
0
    }
730
731
0
    if (link_id == head) {
732
0
        return true;
733
0
    }
734
735
0
    if (!update_links_of_prev_and_next_links(prev, next)) {
736
0
        return false;
737
0
    }
738
739
0
    if (head) {
740
0
        auto stmt = prepare("UPDATE linked_chunks SET prev = ? WHERE id = ?");
741
0
        if (!stmt)
742
0
            return false;
743
0
        stmt->bindInt64(link_id);
744
0
        stmt->bindInt64(head);
745
0
        const auto ret = stmt->execute();
746
0
        if (ret != SQLITE_DONE) {
747
0
            pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_));
748
0
            return false;
749
0
        }
750
0
    }
751
752
0
    return update_linked_chunks(link_id, 0, head) &&
753
0
           update_linked_chunks_head_tail(link_id,
754
0
                                          (link_id == tail) ? prev : tail);
755
0
}
756
757
// ---------------------------------------------------------------------------
758
759
0
bool DiskChunkCache::move_to_tail(sqlite3_int64 chunk_id) {
760
0
    sqlite3_int64 link_id = 0;
761
0
    sqlite3_int64 prev = 0;
762
0
    sqlite3_int64 next = 0;
763
0
    sqlite3_int64 head = 0;
764
0
    sqlite3_int64 tail = 0;
765
0
    if (!get_links(chunk_id, link_id, prev, next, head, tail)) {
766
0
        return false;
767
0
    }
768
769
0
    if (link_id == tail) {
770
0
        return true;
771
0
    }
772
773
0
    if (!update_links_of_prev_and_next_links(prev, next)) {
774
0
        return false;
775
0
    }
776
777
0
    if (tail) {
778
0
        auto stmt = prepare("UPDATE linked_chunks SET next = ? WHERE id = ?");
779
0
        if (!stmt)
780
0
            return false;
781
0
        stmt->bindInt64(link_id);
782
0
        stmt->bindInt64(tail);
783
0
        const auto ret = stmt->execute();
784
0
        if (ret != SQLITE_DONE) {
785
0
            pj_log(ctx_, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB_));
786
0
            return false;
787
0
        }
788
0
    }
789
790
0
    return update_linked_chunks(link_id, tail, 0) &&
791
0
           update_linked_chunks_head_tail((link_id == head) ? next : head,
792
0
                                          link_id);
793
0
}
794
795
// ---------------------------------------------------------------------------
796
797
void NetworkChunkCache::insert(PJ_CONTEXT *ctx, const std::string &url,
798
                               unsigned long long chunkIdx,
799
0
                               std::vector<unsigned char> &&data) {
800
0
    auto dataPtr(std::make_shared<std::vector<unsigned char>>(std::move(data)));
801
0
    cache_.insert(Key(url, chunkIdx), dataPtr);
802
803
0
    auto diskCache = DiskChunkCache::open(ctx);
804
0
    if (!diskCache)
805
0
        return;
806
0
    auto hDB = diskCache->handle();
807
808
    // Always insert DOWNLOAD_CHUNK_SIZE bytes to avoid fragmentation
809
0
    std::vector<unsigned char> blob(*dataPtr);
810
0
    assert(blob.size() <= DOWNLOAD_CHUNK_SIZE);
811
0
    blob.resize(DOWNLOAD_CHUNK_SIZE);
812
813
    // Check if there is an existing entry for that URL and offset
814
0
    auto stmt = diskCache->prepare(
815
0
        "SELECT id, data_id FROM chunks WHERE url = ? AND offset = ?");
816
0
    if (!stmt)
817
0
        return;
818
0
    stmt->bindText(url.c_str());
819
0
    stmt->bindInt64(chunkIdx * DOWNLOAD_CHUNK_SIZE);
820
821
0
    const auto mainRet = stmt->execute();
822
0
    if (mainRet == SQLITE_ROW) {
823
0
        const auto chunk_id = stmt->getInt64();
824
0
        const auto data_id = stmt->getInt64();
825
0
        stmt =
826
0
            diskCache->prepare("UPDATE chunk_data SET data = ? WHERE id = ?");
827
0
        if (!stmt)
828
0
            return;
829
0
        stmt->bindBlob(blob.data(), blob.size());
830
0
        stmt->bindInt64(data_id);
831
0
        {
832
0
            const auto ret = stmt->execute();
833
0
            if (ret != SQLITE_DONE) {
834
0
                pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
835
0
                return;
836
0
            }
837
0
        }
838
839
0
        diskCache->move_to_head(chunk_id);
840
841
0
        return;
842
0
    } else if (mainRet != SQLITE_DONE) {
843
0
        pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
844
0
        return;
845
0
    }
846
847
    // Lambda to recycle an existing entry that was either invalidated, or
848
    // least recently used.
849
0
    const auto reuseExistingEntry =
850
0
        [ctx, &blob, &diskCache, hDB, &url, chunkIdx,
851
0
         &dataPtr](std::unique_ptr<SQLiteStatement> &stmtIn) {
852
0
            const auto chunk_id = stmtIn->getInt64();
853
0
            const auto data_id = stmtIn->getInt64();
854
0
            if (data_id <= 0) {
855
0
                pj_log(ctx, PJ_LOG_ERROR, "data_id <= 0");
856
0
                return;
857
0
            }
858
859
0
            auto l_stmt = diskCache->prepare(
860
0
                "UPDATE chunk_data SET data = ? WHERE id = ?");
861
0
            if (!l_stmt)
862
0
                return;
863
0
            l_stmt->bindBlob(blob.data(), blob.size());
864
0
            l_stmt->bindInt64(data_id);
865
0
            {
866
0
                const auto ret2 = l_stmt->execute();
867
0
                if (ret2 != SQLITE_DONE) {
868
0
                    pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
869
0
                    return;
870
0
                }
871
0
            }
872
873
0
            l_stmt =
874
0
                diskCache->prepare("UPDATE chunks SET url = ?, "
875
0
                                   "offset = ?, data_size = ?, data_id = ? "
876
0
                                   "WHERE id = ?");
877
0
            if (!l_stmt)
878
0
                return;
879
0
            l_stmt->bindText(url.c_str());
880
0
            l_stmt->bindInt64(chunkIdx * DOWNLOAD_CHUNK_SIZE);
881
0
            l_stmt->bindInt64(dataPtr->size());
882
0
            l_stmt->bindInt64(data_id);
883
0
            l_stmt->bindInt64(chunk_id);
884
0
            {
885
0
                const auto ret2 = l_stmt->execute();
886
0
                if (ret2 != SQLITE_DONE) {
887
0
                    pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
888
0
                    return;
889
0
                }
890
0
            }
891
892
0
            diskCache->move_to_head(chunk_id);
893
0
        };
894
895
    // Find if there is an invalidated chunk we can reuse
896
0
    stmt = diskCache->prepare(
897
0
        "SELECT id, data_id FROM chunks "
898
0
        "WHERE id = (SELECT tail FROM linked_chunks_head_tail) AND "
899
0
        "url = " INVALIDATED_SQL_LITERAL);
900
0
    if (!stmt)
901
0
        return;
902
0
    {
903
0
        const auto ret = stmt->execute();
904
0
        if (ret == SQLITE_ROW) {
905
0
            reuseExistingEntry(stmt);
906
0
            return;
907
0
        } else if (ret != SQLITE_DONE) {
908
0
            pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
909
0
            return;
910
0
        }
911
0
    }
912
913
    // Check if we have not reached the max size of the cache
914
0
    stmt = diskCache->prepare("SELECT COUNT(*) FROM chunks");
915
0
    if (!stmt)
916
0
        return;
917
0
    {
918
0
        const auto ret = stmt->execute();
919
0
        if (ret != SQLITE_ROW) {
920
0
            pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
921
0
            return;
922
0
        }
923
0
    }
924
925
0
    const auto max_size = pj_context_get_grid_cache_max_size(ctx);
926
0
    if (max_size > 0 &&
927
0
        static_cast<long long>(stmt->getInt64() * DOWNLOAD_CHUNK_SIZE) >=
928
0
            max_size) {
929
0
        stmt = diskCache->prepare(
930
0
            "SELECT id, data_id FROM chunks "
931
0
            "WHERE id = (SELECT tail FROM linked_chunks_head_tail)");
932
0
        if (!stmt)
933
0
            return;
934
935
0
        const auto ret = stmt->execute();
936
0
        if (ret != SQLITE_ROW) {
937
0
            pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
938
0
            return;
939
0
        }
940
0
        reuseExistingEntry(stmt);
941
0
        return;
942
0
    }
943
944
    // Otherwise just append a new entry
945
0
    stmt = diskCache->prepare("INSERT INTO chunk_data(data) VALUES (?)");
946
0
    if (!stmt)
947
0
        return;
948
0
    stmt->bindBlob(blob.data(), blob.size());
949
0
    {
950
0
        const auto ret = stmt->execute();
951
0
        if (ret != SQLITE_DONE) {
952
0
            pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
953
0
            return;
954
0
        }
955
0
    }
956
957
0
    const auto chunk_data_id = sqlite3_last_insert_rowid(hDB);
958
959
0
    stmt = diskCache->prepare("INSERT INTO chunks(url, offset, data_id, "
960
0
                              "data_size) VALUES (?,?,?,?)");
961
0
    if (!stmt)
962
0
        return;
963
0
    stmt->bindText(url.c_str());
964
0
    stmt->bindInt64(chunkIdx * DOWNLOAD_CHUNK_SIZE);
965
0
    stmt->bindInt64(chunk_data_id);
966
0
    stmt->bindInt64(dataPtr->size());
967
0
    {
968
0
        const auto ret = stmt->execute();
969
0
        if (ret != SQLITE_DONE) {
970
0
            pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
971
0
            return;
972
0
        }
973
0
    }
974
975
0
    const auto chunk_id = sqlite3_last_insert_rowid(hDB);
976
977
0
    stmt = diskCache->prepare(
978
0
        "INSERT INTO linked_chunks(chunk_id, prev, next) VALUES (?,NULL,NULL)");
979
0
    if (!stmt)
980
0
        return;
981
0
    stmt->bindInt64(chunk_id);
982
0
    if (stmt->execute() != SQLITE_DONE) {
983
0
        pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
984
0
        return;
985
0
    }
986
987
0
    stmt = diskCache->prepare("SELECT head FROM linked_chunks_head_tail");
988
0
    if (!stmt)
989
0
        return;
990
0
    if (stmt->execute() != SQLITE_ROW) {
991
0
        pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
992
0
        return;
993
0
    }
994
0
    if (stmt->getInt64() == 0) {
995
0
        stmt = diskCache->prepare(
996
0
            "UPDATE linked_chunks_head_tail SET head = ?, tail = ?");
997
0
        if (!stmt)
998
0
            return;
999
0
        stmt->bindInt64(chunk_id);
1000
0
        stmt->bindInt64(chunk_id);
1001
0
        if (stmt->execute() != SQLITE_DONE) {
1002
0
            pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
1003
0
            return;
1004
0
        }
1005
0
    }
1006
1007
0
    diskCache->move_to_head(chunk_id);
1008
0
}
1009
1010
// ---------------------------------------------------------------------------
1011
1012
std::shared_ptr<std::vector<unsigned char>>
1013
NetworkChunkCache::get(PJ_CONTEXT *ctx, const std::string &url,
1014
0
                       unsigned long long chunkIdx) {
1015
0
    std::shared_ptr<std::vector<unsigned char>> ret;
1016
0
    if (cache_.tryGet(Key(url, chunkIdx), ret)) {
1017
0
        return ret;
1018
0
    }
1019
1020
0
    auto diskCache = DiskChunkCache::open(ctx);
1021
0
    if (!diskCache)
1022
0
        return ret;
1023
0
    auto hDB = diskCache->handle();
1024
1025
0
    auto stmt = diskCache->prepare(
1026
0
        "SELECT chunks.id, chunks.data_size, chunk_data.data FROM chunks "
1027
0
        "JOIN chunk_data ON chunks.id = chunk_data.id "
1028
0
        "WHERE chunks.url = ? AND chunks.offset = ?");
1029
0
    if (!stmt)
1030
0
        return ret;
1031
1032
0
    stmt->bindText(url.c_str());
1033
0
    stmt->bindInt64(chunkIdx * DOWNLOAD_CHUNK_SIZE);
1034
1035
0
    const auto mainRet = stmt->execute();
1036
0
    if (mainRet == SQLITE_ROW) {
1037
0
        const auto chunk_id = stmt->getInt64();
1038
0
        const auto data_size = stmt->getInt64();
1039
0
        int blob_size = 0;
1040
0
        const void *blob = stmt->getBlob(blob_size);
1041
0
        if (blob_size < data_size) {
1042
0
            pj_log(ctx, PJ_LOG_ERROR,
1043
0
                   "blob_size=%d < data_size for chunk_id=%d", blob_size,
1044
0
                   static_cast<int>(chunk_id));
1045
0
            return ret;
1046
0
        }
1047
0
        if (data_size > static_cast<sqlite3_int64>(DOWNLOAD_CHUNK_SIZE)) {
1048
0
            pj_log(ctx, PJ_LOG_ERROR, "data_size > DOWNLOAD_CHUNK_SIZE");
1049
0
            return ret;
1050
0
        }
1051
0
        ret.reset(new std::vector<unsigned char>());
1052
0
        ret->assign(reinterpret_cast<const unsigned char *>(blob),
1053
0
                    reinterpret_cast<const unsigned char *>(blob) +
1054
0
                        static_cast<size_t>(data_size));
1055
0
        cache_.insert(Key(url, chunkIdx), ret);
1056
1057
0
        if (!diskCache->move_to_head(chunk_id))
1058
0
            return ret;
1059
0
    } else if (mainRet != SQLITE_DONE) {
1060
0
        pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
1061
0
    }
1062
1063
0
    return ret;
1064
0
}
1065
1066
// ---------------------------------------------------------------------------
1067
1068
std::shared_ptr<std::vector<unsigned char>>
1069
NetworkChunkCache::get(PJ_CONTEXT *ctx, const std::string &url,
1070
0
                       unsigned long long chunkIdx, FileProperties &props) {
1071
0
    if (!gNetworkFileProperties.tryGet(ctx, url, props)) {
1072
0
        return nullptr;
1073
0
    }
1074
1075
0
    return get(ctx, url, chunkIdx);
1076
0
}
1077
1078
// ---------------------------------------------------------------------------
1079
1080
10.9k
void NetworkChunkCache::clearMemoryCache() { cache_.clear(); }
1081
1082
// ---------------------------------------------------------------------------
1083
1084
0
void NetworkChunkCache::clearDiskChunkCache(PJ_CONTEXT *ctx) {
1085
0
    auto diskCache = DiskChunkCache::open(ctx);
1086
0
    if (!diskCache)
1087
0
        return;
1088
0
    diskCache->closeAndUnlink();
1089
0
}
1090
1091
// ---------------------------------------------------------------------------
1092
1093
void NetworkFilePropertiesCache::insert(PJ_CONTEXT *ctx, const std::string &url,
1094
0
                                        FileProperties &props) {
1095
0
    time(&props.lastChecked);
1096
0
    cache_.insert(url, props);
1097
1098
0
    auto diskCache = DiskChunkCache::open(ctx);
1099
0
    if (!diskCache)
1100
0
        return;
1101
0
    auto hDB = diskCache->handle();
1102
0
    auto stmt = diskCache->prepare("SELECT fileSize, lastModified, etag "
1103
0
                                   "FROM properties WHERE url = ?");
1104
0
    if (!stmt)
1105
0
        return;
1106
0
    stmt->bindText(url.c_str());
1107
0
    if (stmt->execute() == SQLITE_ROW) {
1108
0
        FileProperties cachedProps;
1109
0
        cachedProps.size = stmt->getInt64();
1110
0
        const char *lastModified = stmt->getText();
1111
0
        cachedProps.lastModified = lastModified ? lastModified : std::string();
1112
0
        const char *etag = stmt->getText();
1113
0
        cachedProps.etag = etag ? etag : std::string();
1114
0
        if (props.size != cachedProps.size ||
1115
0
            props.lastModified != cachedProps.lastModified ||
1116
0
            props.etag != cachedProps.etag) {
1117
1118
            // If cached properties don't match recent fresh ones, invalidate
1119
            // cached chunks
1120
0
            stmt = diskCache->prepare("SELECT id FROM chunks WHERE url = ?");
1121
0
            if (!stmt)
1122
0
                return;
1123
0
            stmt->bindText(url.c_str());
1124
0
            std::vector<sqlite3_int64> ids;
1125
0
            while (stmt->execute() == SQLITE_ROW) {
1126
0
                ids.emplace_back(stmt->getInt64());
1127
0
                stmt->resetResIndex();
1128
0
            }
1129
1130
0
            for (const auto id : ids) {
1131
0
                diskCache->move_to_tail(id);
1132
0
            }
1133
1134
0
            stmt = diskCache->prepare(
1135
0
                "UPDATE chunks SET url = " INVALIDATED_SQL_LITERAL ", "
1136
0
                "offset = -1, data_size = 0 WHERE url = ?");
1137
0
            if (!stmt)
1138
0
                return;
1139
0
            stmt->bindText(url.c_str());
1140
0
            if (stmt->execute() != SQLITE_DONE) {
1141
0
                pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
1142
0
                return;
1143
0
            }
1144
0
        }
1145
1146
0
        stmt = diskCache->prepare("UPDATE properties SET lastChecked = ?, "
1147
0
                                  "fileSize = ?, lastModified = ?, etag = ? "
1148
0
                                  "WHERE url = ?");
1149
0
        if (!stmt)
1150
0
            return;
1151
0
        stmt->bindInt64(props.lastChecked);
1152
0
        stmt->bindInt64(props.size);
1153
0
        if (props.lastModified.empty())
1154
0
            stmt->bindNull();
1155
0
        else
1156
0
            stmt->bindText(props.lastModified.c_str());
1157
0
        if (props.etag.empty())
1158
0
            stmt->bindNull();
1159
0
        else
1160
0
            stmt->bindText(props.etag.c_str());
1161
0
        stmt->bindText(url.c_str());
1162
0
        if (stmt->execute() != SQLITE_DONE) {
1163
0
            pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
1164
0
            return;
1165
0
        }
1166
0
    } else {
1167
0
        stmt = diskCache->prepare("INSERT INTO properties (url, lastChecked, "
1168
0
                                  "fileSize, lastModified, etag) VALUES "
1169
0
                                  "(?,?,?,?,?)");
1170
0
        if (!stmt)
1171
0
            return;
1172
0
        stmt->bindText(url.c_str());
1173
0
        stmt->bindInt64(props.lastChecked);
1174
0
        stmt->bindInt64(props.size);
1175
0
        if (props.lastModified.empty())
1176
0
            stmt->bindNull();
1177
0
        else
1178
0
            stmt->bindText(props.lastModified.c_str());
1179
0
        if (props.etag.empty())
1180
0
            stmt->bindNull();
1181
0
        else
1182
0
            stmt->bindText(props.etag.c_str());
1183
0
        if (stmt->execute() != SQLITE_DONE) {
1184
0
            pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
1185
0
            return;
1186
0
        }
1187
0
    }
1188
0
}
1189
1190
// ---------------------------------------------------------------------------
1191
1192
bool NetworkFilePropertiesCache::tryGet(PJ_CONTEXT *ctx, const std::string &url,
1193
0
                                        FileProperties &props) {
1194
0
    if (cache_.tryGet(url, props)) {
1195
0
        return true;
1196
0
    }
1197
1198
0
    auto diskCache = DiskChunkCache::open(ctx);
1199
0
    if (!diskCache)
1200
0
        return false;
1201
0
    auto stmt =
1202
0
        diskCache->prepare("SELECT lastChecked, fileSize, lastModified, etag "
1203
0
                           "FROM properties WHERE url = ?");
1204
0
    if (!stmt)
1205
0
        return false;
1206
0
    stmt->bindText(url.c_str());
1207
0
    if (stmt->execute() != SQLITE_ROW) {
1208
0
        return false;
1209
0
    }
1210
0
    props.lastChecked = static_cast<time_t>(stmt->getInt64());
1211
0
    props.size = stmt->getInt64();
1212
0
    const char *lastModified = stmt->getText();
1213
0
    props.lastModified = lastModified ? lastModified : std::string();
1214
0
    const char *etag = stmt->getText();
1215
0
    props.etag = etag ? etag : std::string();
1216
1217
0
    const auto ttl = pj_context_get_grid_cache_ttl(ctx);
1218
0
    if (ttl > 0) {
1219
0
        time_t curTime;
1220
0
        time(&curTime);
1221
0
        if (curTime > props.lastChecked + ttl) {
1222
0
            props = FileProperties();
1223
0
            return false;
1224
0
        }
1225
0
    }
1226
0
    cache_.insert(url, props);
1227
0
    return true;
1228
0
}
1229
1230
// ---------------------------------------------------------------------------
1231
1232
10.9k
void NetworkFilePropertiesCache::clearMemoryCache() { cache_.clear(); }
1233
1234
// ---------------------------------------------------------------------------
1235
1236
class NetworkFile : public File {
1237
    PJ_CONTEXT *m_ctx;
1238
    std::string m_url;
1239
    PROJ_NETWORK_HANDLE *m_handle;
1240
    unsigned long long m_pos = 0;
1241
    size_t m_nBlocksToDownload = 1;
1242
    unsigned long long m_lastDownloadedOffset;
1243
    FileProperties m_props;
1244
    proj_network_close_cbk_type m_closeCbk;
1245
    bool m_hasChanged = false;
1246
1247
    NetworkFile(const NetworkFile &) = delete;
1248
    NetworkFile &operator=(const NetworkFile &) = delete;
1249
1250
  protected:
1251
    NetworkFile(PJ_CONTEXT *ctx, const std::string &url,
1252
                PROJ_NETWORK_HANDLE *handle,
1253
                unsigned long long lastDownloadOffset,
1254
                const FileProperties &props)
1255
0
        : File(url), m_ctx(ctx), m_url(url), m_handle(handle),
1256
0
          m_lastDownloadedOffset(lastDownloadOffset), m_props(props),
1257
0
          m_closeCbk(ctx->networking.close) {}
1258
1259
  public:
1260
    ~NetworkFile() override;
1261
1262
    size_t read(void *buffer, size_t sizeBytes) override;
1263
0
    size_t write(const void *, size_t) override { return 0; }
1264
    bool seek(unsigned long long offset, int whence) override;
1265
    unsigned long long tell() override;
1266
    void reassign_context(PJ_CONTEXT *ctx) override;
1267
0
    bool hasChanged() const override { return m_hasChanged; }
1268
1269
    static std::unique_ptr<File> open(PJ_CONTEXT *ctx, const char *filename);
1270
1271
    static bool get_props_from_headers(PJ_CONTEXT *ctx,
1272
                                       PROJ_NETWORK_HANDLE *handle,
1273
                                       FileProperties &props);
1274
};
1275
1276
// ---------------------------------------------------------------------------
1277
1278
bool NetworkFile::get_props_from_headers(PJ_CONTEXT *ctx,
1279
                                         PROJ_NETWORK_HANDLE *handle,
1280
0
                                         FileProperties &props) {
1281
0
    const char *contentRange = ctx->networking.get_header_value(
1282
0
        ctx, handle, "Content-Range", ctx->networking.user_data);
1283
0
    if (contentRange) {
1284
0
        const char *slash = strchr(contentRange, '/');
1285
0
        if (slash) {
1286
0
            props.size = std::stoull(slash + 1);
1287
1288
0
            const char *lastModified = ctx->networking.get_header_value(
1289
0
                ctx, handle, "Last-Modified", ctx->networking.user_data);
1290
0
            if (lastModified)
1291
0
                props.lastModified = lastModified;
1292
1293
0
            const char *etag = ctx->networking.get_header_value(
1294
0
                ctx, handle, "ETag", ctx->networking.user_data);
1295
0
            if (etag)
1296
0
                props.etag = etag;
1297
1298
0
            return true;
1299
0
        }
1300
0
    }
1301
0
    return false;
1302
0
}
1303
1304
// ---------------------------------------------------------------------------
1305
1306
0
std::unique_ptr<File> NetworkFile::open(PJ_CONTEXT *ctx, const char *filename) {
1307
0
    FileProperties props;
1308
0
    if (gNetworkChunkCache.get(ctx, filename, 0, props)) {
1309
0
        return std::unique_ptr<File>(new NetworkFile(
1310
0
            ctx, filename, nullptr,
1311
0
            std::numeric_limits<unsigned long long>::max(), props));
1312
0
    } else {
1313
0
        std::vector<unsigned char> buffer(DOWNLOAD_CHUNK_SIZE);
1314
0
        size_t size_read = 0;
1315
0
        std::string errorBuffer;
1316
0
        errorBuffer.resize(1024);
1317
1318
0
        auto handle = ctx->networking.open(
1319
0
            ctx, filename, 0, buffer.size(), buffer.data(), &size_read,
1320
0
            errorBuffer.size(), &errorBuffer[0], ctx->networking.user_data);
1321
0
        if (!handle) {
1322
0
            errorBuffer.resize(strlen(errorBuffer.data()));
1323
0
            pj_log(ctx, PJ_LOG_ERROR, "Cannot open %s: %s", filename,
1324
0
                   errorBuffer.c_str());
1325
0
            proj_context_errno_set(ctx, PROJ_ERR_OTHER_NETWORK_ERROR);
1326
0
        } else if (get_props_from_headers(ctx, handle, props)) {
1327
0
            gNetworkFileProperties.insert(ctx, filename, props);
1328
0
            buffer.resize(size_read);
1329
0
            gNetworkChunkCache.insert(ctx, filename, 0, std::move(buffer));
1330
0
            return std::unique_ptr<File>(
1331
0
                new NetworkFile(ctx, filename, handle, size_read, props));
1332
0
        } else {
1333
0
            ctx->networking.close(ctx, handle, ctx->networking.user_data);
1334
0
        }
1335
1336
0
        return std::unique_ptr<File>(nullptr);
1337
0
    }
1338
0
}
1339
1340
// ---------------------------------------------------------------------------
1341
1342
std::unique_ptr<File> pj_network_file_open(PJ_CONTEXT *ctx,
1343
0
                                           const char *filename) {
1344
0
    return NetworkFile::open(ctx, filename);
1345
0
}
1346
1347
// ---------------------------------------------------------------------------
1348
1349
0
size_t NetworkFile::read(void *buffer, size_t sizeBytes) {
1350
1351
0
    if (sizeBytes == 0)
1352
0
        return 0;
1353
1354
0
    auto iterOffset = m_pos;
1355
0
    while (sizeBytes) {
1356
0
        const auto chunkIdxToDownload = iterOffset / DOWNLOAD_CHUNK_SIZE;
1357
0
        const auto offsetToDownload = chunkIdxToDownload * DOWNLOAD_CHUNK_SIZE;
1358
0
        std::vector<unsigned char> region;
1359
0
        auto pChunk = gNetworkChunkCache.get(m_ctx, m_url, chunkIdxToDownload);
1360
0
        if (pChunk != nullptr) {
1361
0
            region = *pChunk;
1362
0
        } else {
1363
0
            if (offsetToDownload == m_lastDownloadedOffset) {
1364
                // In case of consecutive reads (of small size), we use a
1365
                // heuristic that we will read the file sequentially, so
1366
                // we double the requested size to decrease the number of
1367
                // client/server roundtrips.
1368
0
                if (m_nBlocksToDownload < 100)
1369
0
                    m_nBlocksToDownload *= 2;
1370
0
            } else {
1371
                // Random reads. Cancel the above heuristics.
1372
0
                m_nBlocksToDownload = 1;
1373
0
            }
1374
1375
            // Ensure that we will request at least the number of blocks
1376
            // to satisfy the remaining buffer size to read.
1377
0
            const auto endOffsetToDownload =
1378
0
                ((iterOffset + sizeBytes + DOWNLOAD_CHUNK_SIZE - 1) /
1379
0
                 DOWNLOAD_CHUNK_SIZE) *
1380
0
                DOWNLOAD_CHUNK_SIZE;
1381
0
            const auto nMinBlocksToDownload = static_cast<size_t>(
1382
0
                (endOffsetToDownload - offsetToDownload) / DOWNLOAD_CHUNK_SIZE);
1383
0
            if (m_nBlocksToDownload < nMinBlocksToDownload)
1384
0
                m_nBlocksToDownload = nMinBlocksToDownload;
1385
1386
            // Avoid reading already cached data.
1387
            // Note: this might get evicted if concurrent reads are done, but
1388
            // this should not cause bugs. Just missed optimization.
1389
0
            for (size_t i = 1; i < m_nBlocksToDownload; i++) {
1390
0
                if (gNetworkChunkCache.get(m_ctx, m_url,
1391
0
                                           chunkIdxToDownload + i) != nullptr) {
1392
0
                    m_nBlocksToDownload = i;
1393
0
                    break;
1394
0
                }
1395
0
            }
1396
1397
0
            if (m_nBlocksToDownload > MAX_CHUNKS)
1398
0
                m_nBlocksToDownload = MAX_CHUNKS;
1399
1400
0
            region.resize(m_nBlocksToDownload * DOWNLOAD_CHUNK_SIZE);
1401
0
            size_t nRead = 0;
1402
0
            std::string errorBuffer;
1403
0
            errorBuffer.resize(1024);
1404
0
            if (!m_handle) {
1405
0
                m_handle = m_ctx->networking.open(
1406
0
                    m_ctx, m_url.c_str(), offsetToDownload,
1407
0
                    m_nBlocksToDownload * DOWNLOAD_CHUNK_SIZE, &region[0],
1408
0
                    &nRead, errorBuffer.size(), &errorBuffer[0],
1409
0
                    m_ctx->networking.user_data);
1410
0
                if (!m_handle) {
1411
0
                    proj_context_errno_set(m_ctx, PROJ_ERR_OTHER_NETWORK_ERROR);
1412
0
                    return 0;
1413
0
                }
1414
0
            } else {
1415
0
                nRead = m_ctx->networking.read_range(
1416
0
                    m_ctx, m_handle, offsetToDownload,
1417
0
                    m_nBlocksToDownload * DOWNLOAD_CHUNK_SIZE, &region[0],
1418
0
                    errorBuffer.size(), &errorBuffer[0],
1419
0
                    m_ctx->networking.user_data);
1420
0
            }
1421
0
            if (nRead == 0) {
1422
0
                errorBuffer.resize(strlen(errorBuffer.data()));
1423
0
                if (!errorBuffer.empty()) {
1424
0
                    pj_log(m_ctx, PJ_LOG_ERROR, "Cannot read in %s: %s",
1425
0
                           m_url.c_str(), errorBuffer.c_str());
1426
0
                }
1427
0
                proj_context_errno_set(m_ctx, PROJ_ERR_OTHER_NETWORK_ERROR);
1428
0
                return 0;
1429
0
            }
1430
1431
0
            if (!m_hasChanged) {
1432
0
                FileProperties props;
1433
0
                if (get_props_from_headers(m_ctx, m_handle, props)) {
1434
0
                    if (props.size != m_props.size ||
1435
0
                        props.lastModified != m_props.lastModified ||
1436
0
                        props.etag != m_props.etag) {
1437
0
                        gNetworkFileProperties.insert(m_ctx, m_url, props);
1438
0
                        gNetworkChunkCache.clearMemoryCache();
1439
0
                        m_hasChanged = true;
1440
0
                    }
1441
0
                }
1442
0
            }
1443
1444
0
            region.resize(nRead);
1445
0
            m_lastDownloadedOffset = offsetToDownload + nRead;
1446
1447
0
            const auto nChunks =
1448
0
                (region.size() + DOWNLOAD_CHUNK_SIZE - 1) / DOWNLOAD_CHUNK_SIZE;
1449
0
            for (size_t i = 0; i < nChunks; i++) {
1450
0
                std::vector<unsigned char> chunk(
1451
0
                    region.data() + i * DOWNLOAD_CHUNK_SIZE,
1452
0
                    region.data() +
1453
0
                        std::min((i + 1) * DOWNLOAD_CHUNK_SIZE, region.size()));
1454
0
                gNetworkChunkCache.insert(m_ctx, m_url, chunkIdxToDownload + i,
1455
0
                                          std::move(chunk));
1456
0
            }
1457
0
        }
1458
0
        const size_t nToCopy = static_cast<size_t>(
1459
0
            std::min(static_cast<unsigned long long>(sizeBytes),
1460
0
                     region.size() - (iterOffset - offsetToDownload)));
1461
0
        memcpy(buffer, region.data() + iterOffset - offsetToDownload, nToCopy);
1462
0
        buffer = static_cast<char *>(buffer) + nToCopy;
1463
0
        iterOffset += nToCopy;
1464
0
        sizeBytes -= nToCopy;
1465
0
        if (region.size() < static_cast<size_t>(DOWNLOAD_CHUNK_SIZE) &&
1466
0
            sizeBytes != 0) {
1467
0
            break;
1468
0
        }
1469
0
    }
1470
1471
0
    size_t nRead = static_cast<size_t>(iterOffset - m_pos);
1472
0
    m_pos = iterOffset;
1473
0
    return nRead;
1474
0
}
1475
1476
// ---------------------------------------------------------------------------
1477
1478
0
bool NetworkFile::seek(unsigned long long offset, int whence) {
1479
0
    if (whence == SEEK_SET) {
1480
0
        m_pos = offset;
1481
0
    } else if (whence == SEEK_CUR) {
1482
0
        m_pos += offset;
1483
0
    } else {
1484
0
        if (offset != 0)
1485
0
            return false;
1486
0
        m_pos = m_props.size;
1487
0
    }
1488
0
    return true;
1489
0
}
1490
1491
// ---------------------------------------------------------------------------
1492
1493
0
unsigned long long NetworkFile::tell() { return m_pos; }
1494
1495
// ---------------------------------------------------------------------------
1496
1497
0
NetworkFile::~NetworkFile() {
1498
0
    if (m_handle) {
1499
0
        m_ctx->networking.close(m_ctx, m_handle, m_ctx->networking.user_data);
1500
0
    }
1501
0
}
1502
1503
// ---------------------------------------------------------------------------
1504
1505
0
void NetworkFile::reassign_context(PJ_CONTEXT *ctx) {
1506
0
    m_ctx = ctx;
1507
0
    if (m_closeCbk != m_ctx->networking.close) {
1508
0
        pj_log(m_ctx, PJ_LOG_ERROR,
1509
0
               "Networking close callback has changed following context "
1510
0
               "reassignment ! This is highly suspicious");
1511
0
    }
1512
0
}
1513
1514
// ---------------------------------------------------------------------------
1515
1516
#ifdef CURL_ENABLED
1517
1518
struct CurlFileHandle {
1519
    std::string m_url;
1520
    CURL *m_handle;
1521
    std::string m_headers{};
1522
    std::string m_lastval{};
1523
    std::string m_useragent{};
1524
    char m_szCurlErrBuf[CURL_ERROR_SIZE + 1] = {};
1525
1526
    CurlFileHandle(const CurlFileHandle &) = delete;
1527
    CurlFileHandle &operator=(const CurlFileHandle &) = delete;
1528
1529
    explicit CurlFileHandle(PJ_CONTEXT *ctx, const char *url, CURL *handle);
1530
    ~CurlFileHandle();
1531
1532
    static PROJ_NETWORK_HANDLE *
1533
    open(PJ_CONTEXT *, const char *url, unsigned long long offset,
1534
         size_t size_to_read, void *buffer, size_t *out_size_read,
1535
         size_t error_string_max_size, char *out_error_string, void *);
1536
};
1537
1538
// ---------------------------------------------------------------------------
1539
1540
0
static std::string GetExecutableName() {
1541
0
#if defined(__linux)
1542
0
    std::string path;
1543
0
    path.resize(1024);
1544
0
    const auto ret = readlink("/proc/self/exe", &path[0], path.size());
1545
0
    if (ret > 0) {
1546
0
        path.resize(ret);
1547
0
        const auto pos = path.rfind('/');
1548
0
        if (pos != std::string::npos) {
1549
0
            path = path.substr(pos + 1);
1550
0
        }
1551
0
        return path;
1552
0
    }
1553
#elif defined(_WIN32)
1554
    std::string path;
1555
    path.resize(1024);
1556
    if (GetModuleFileNameA(nullptr, &path[0],
1557
                           static_cast<DWORD>(path.size()))) {
1558
        path.resize(strlen(path.c_str()));
1559
        const auto pos = path.rfind('\\');
1560
        if (pos != std::string::npos) {
1561
            path = path.substr(pos + 1);
1562
        }
1563
        return path;
1564
    }
1565
#elif defined(__MACH__) && defined(__APPLE__)
1566
    std::string path;
1567
    path.resize(1024);
1568
    uint32_t size = static_cast<uint32_t>(path.size());
1569
    if (_NSGetExecutablePath(&path[0], &size) == 0) {
1570
        path.resize(strlen(path.c_str()));
1571
        const auto pos = path.rfind('/');
1572
        if (pos != std::string::npos) {
1573
            path = path.substr(pos + 1);
1574
        }
1575
        return path;
1576
    }
1577
#elif defined(__FreeBSD__)
1578
    int mib[4];
1579
    mib[0] = CTL_KERN;
1580
    mib[1] = KERN_PROC;
1581
    mib[2] = KERN_PROC_PATHNAME;
1582
    mib[3] = -1;
1583
    std::string path;
1584
    path.resize(1024);
1585
    size_t size = path.size();
1586
    if (sysctl(mib, 4, &path[0], &size, nullptr, 0) == 0) {
1587
        path.resize(strlen(path.c_str()));
1588
        const auto pos = path.rfind('/');
1589
        if (pos != std::string::npos) {
1590
            path = path.substr(pos + 1);
1591
        }
1592
        return path;
1593
    }
1594
#endif
1595
1596
0
    return std::string();
1597
0
}
1598
1599
// ---------------------------------------------------------------------------
1600
1601
0
static void checkRet(PJ_CONTEXT *ctx, CURLcode code, int line) {
1602
0
    if (code != CURLE_OK) {
1603
0
        pj_log(ctx, PJ_LOG_ERROR, "curl_easy_setopt at line %d failed", line);
1604
0
    }
1605
0
}
1606
1607
0
#define CHECK_RET(ctx, code) checkRet(ctx, code, __LINE__)
1608
1609
// ---------------------------------------------------------------------------
1610
1611
0
static std::string pj_context_get_bundle_path(PJ_CONTEXT *ctx) {
1612
0
    pj_load_ini(ctx);
1613
0
    return ctx->ca_bundle_path;
1614
0
}
1615
1616
#if CURL_AT_LEAST_VERSION(7, 71, 0)
1617
0
static bool pj_context_get_native_ca(PJ_CONTEXT *ctx) {
1618
0
    pj_load_ini(ctx);
1619
0
    return ctx->native_ca;
1620
0
}
1621
#endif
1622
1623
// ---------------------------------------------------------------------------
1624
1625
CurlFileHandle::CurlFileHandle(PJ_CONTEXT *ctx, const char *url, CURL *handle)
1626
0
    : m_url(url), m_handle(handle) {
1627
0
    CHECK_RET(ctx, curl_easy_setopt(handle, CURLOPT_URL, m_url.c_str()));
1628
1629
0
    if (getenv("PROJ_CURL_VERBOSE"))
1630
0
        CHECK_RET(ctx, curl_easy_setopt(handle, CURLOPT_VERBOSE, 1));
1631
1632
// CURLOPT_SUPPRESS_CONNECT_HEADERS is defined in curl 7.54.0 or newer.
1633
0
#if LIBCURL_VERSION_NUM >= 0x073600
1634
0
    CHECK_RET(ctx,
1635
0
              curl_easy_setopt(handle, CURLOPT_SUPPRESS_CONNECT_HEADERS, 1L));
1636
0
#endif
1637
1638
    // Enable following redirections.  Requires libcurl 7.10.1 at least.
1639
0
    CHECK_RET(ctx, curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1));
1640
0
    CHECK_RET(ctx, curl_easy_setopt(handle, CURLOPT_MAXREDIRS, 10));
1641
1642
0
    if (getenv("PROJ_UNSAFE_SSL")) {
1643
0
        CHECK_RET(ctx, curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 0L));
1644
0
        CHECK_RET(ctx, curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 0L));
1645
0
    }
1646
1647
#if defined(SSL_OPTIONS)
1648
    // https://curl.se/libcurl/c/CURLOPT_SSL_OPTIONS.html
1649
    auto ssl_options = static_cast<long>(SSL_OPTIONS);
1650
#if CURL_AT_LEAST_VERSION(7, 71, 0)
1651
    if (pj_context_get_native_ca(ctx)) {
1652
        ssl_options = ssl_options | CURLSSLOPT_NATIVE_CA;
1653
    }
1654
#endif
1655
    CHECK_RET(ctx, curl_easy_setopt(handle, CURLOPT_SSL_OPTIONS, ssl_options));
1656
#else
1657
0
#if CURL_AT_LEAST_VERSION(7, 71, 0)
1658
0
    if (pj_context_get_native_ca(ctx)) {
1659
0
        CHECK_RET(ctx, curl_easy_setopt(handle, CURLOPT_SSL_OPTIONS,
1660
0
                                        (long)CURLSSLOPT_NATIVE_CA));
1661
0
    }
1662
0
#endif
1663
0
#endif
1664
1665
0
    const auto ca_bundle_path = pj_context_get_bundle_path(ctx);
1666
0
    if (!ca_bundle_path.empty()) {
1667
0
        CHECK_RET(ctx, curl_easy_setopt(handle, CURLOPT_CAINFO,
1668
0
                                        ca_bundle_path.c_str()));
1669
0
    }
1670
1671
0
    CHECK_RET(ctx,
1672
0
              curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, m_szCurlErrBuf));
1673
1674
0
    if (getenv("PROJ_NO_USERAGENT") == nullptr) {
1675
0
        m_useragent = "PROJ " STR(PROJ_VERSION_MAJOR) "." STR(
1676
0
            PROJ_VERSION_MINOR) "." STR(PROJ_VERSION_PATCH);
1677
0
        const auto exeName = GetExecutableName();
1678
0
        if (!exeName.empty()) {
1679
0
            m_useragent = exeName + " using " + m_useragent;
1680
0
        }
1681
0
        CHECK_RET(ctx, curl_easy_setopt(handle, CURLOPT_USERAGENT,
1682
0
                                        m_useragent.data()));
1683
0
    }
1684
0
}
1685
1686
// ---------------------------------------------------------------------------
1687
1688
0
CurlFileHandle::~CurlFileHandle() { curl_easy_cleanup(m_handle); }
1689
1690
// ---------------------------------------------------------------------------
1691
1692
static size_t pj_curl_write_func(void *buffer, size_t count, size_t nmemb,
1693
0
                                 void *req) {
1694
0
    const size_t nSize = count * nmemb;
1695
0
    auto pStr = static_cast<std::string *>(req);
1696
0
    if (pStr->size() + nSize > pStr->capacity()) {
1697
        // to avoid servers not honouring Range to cause excessive memory
1698
        // allocation
1699
0
        return 0;
1700
0
    }
1701
0
    pStr->append(static_cast<const char *>(buffer), nSize);
1702
0
    return nmemb;
1703
0
}
1704
1705
// ---------------------------------------------------------------------------
1706
1707
static double GetNewRetryDelay(int response_code, double dfOldDelay,
1708
                               const char *pszErrBuf,
1709
0
                               const char *pszCurlError) {
1710
0
    if (response_code == 429 || response_code == 500 ||
1711
0
        (response_code >= 502 && response_code <= 504) ||
1712
        // S3 sends some client timeout errors as 400 Client Error
1713
0
        (response_code == 400 && pszErrBuf &&
1714
0
         strstr(pszErrBuf, "RequestTimeout")) ||
1715
0
        (pszCurlError && strstr(pszCurlError, "Connection reset by peer")) ||
1716
0
        (pszCurlError && strstr(pszCurlError, "Connection timed out")) ||
1717
0
        (pszCurlError && strstr(pszCurlError, "SSL connection timeout"))) {
1718
        // Use an exponential backoff factor of 2 plus some random jitter
1719
        // We don't care about cryptographic quality randomness, hence:
1720
        // coverity[dont_call]
1721
0
        return dfOldDelay * (2 + rand() * 0.5 / RAND_MAX);
1722
0
    } else {
1723
0
        return 0;
1724
0
    }
1725
0
}
1726
1727
// ---------------------------------------------------------------------------
1728
1729
constexpr double MIN_RETRY_DELAY_MS = 500;
1730
constexpr double MAX_RETRY_DELAY_MS = 60000;
1731
1732
PROJ_NETWORK_HANDLE *CurlFileHandle::open(PJ_CONTEXT *ctx, const char *url,
1733
                                          unsigned long long offset,
1734
                                          size_t size_to_read, void *buffer,
1735
                                          size_t *out_size_read,
1736
                                          size_t error_string_max_size,
1737
0
                                          char *out_error_string, void *) {
1738
0
    CURL *hCurlHandle = curl_easy_init();
1739
0
    if (!hCurlHandle)
1740
0
        return nullptr;
1741
1742
0
    auto file = std::unique_ptr<CurlFileHandle>(
1743
0
        new CurlFileHandle(ctx, url, hCurlHandle));
1744
1745
0
    double oldDelay = MIN_RETRY_DELAY_MS;
1746
0
    std::string headers;
1747
0
    std::string body;
1748
1749
0
    char szBuffer[128];
1750
0
    sqlite3_snprintf(sizeof(szBuffer), szBuffer, "%llu-%llu", offset,
1751
0
                     offset + size_to_read - 1);
1752
1753
0
    while (true) {
1754
0
        CHECK_RET(ctx, curl_easy_setopt(hCurlHandle, CURLOPT_RANGE, szBuffer));
1755
1756
0
        headers.clear();
1757
0
        headers.reserve(16 * 1024);
1758
0
        CHECK_RET(ctx,
1759
0
                  curl_easy_setopt(hCurlHandle, CURLOPT_HEADERDATA, &headers));
1760
0
        CHECK_RET(ctx, curl_easy_setopt(hCurlHandle, CURLOPT_HEADERFUNCTION,
1761
0
                                        pj_curl_write_func));
1762
1763
0
        body.clear();
1764
0
        body.reserve(size_to_read);
1765
0
        CHECK_RET(ctx, curl_easy_setopt(hCurlHandle, CURLOPT_WRITEDATA, &body));
1766
0
        CHECK_RET(ctx, curl_easy_setopt(hCurlHandle, CURLOPT_WRITEFUNCTION,
1767
0
                                        pj_curl_write_func));
1768
1769
0
        file->m_szCurlErrBuf[0] = '\0';
1770
1771
0
        curl_easy_perform(hCurlHandle);
1772
1773
0
        long response_code = 0;
1774
0
        curl_easy_getinfo(hCurlHandle, CURLINFO_HTTP_CODE, &response_code);
1775
1776
0
        CHECK_RET(ctx,
1777
0
                  curl_easy_setopt(hCurlHandle, CURLOPT_HEADERDATA, nullptr));
1778
0
        CHECK_RET(ctx, curl_easy_setopt(hCurlHandle, CURLOPT_HEADERFUNCTION,
1779
0
                                        nullptr));
1780
1781
0
        CHECK_RET(ctx,
1782
0
                  curl_easy_setopt(hCurlHandle, CURLOPT_WRITEDATA, nullptr));
1783
0
        CHECK_RET(
1784
0
            ctx, curl_easy_setopt(hCurlHandle, CURLOPT_WRITEFUNCTION, nullptr));
1785
1786
0
        if (response_code == 0 || response_code >= 300) {
1787
0
            const double delay =
1788
0
                GetNewRetryDelay(static_cast<int>(response_code), oldDelay,
1789
0
                                 body.c_str(), file->m_szCurlErrBuf);
1790
0
            if (delay != 0 && delay < MAX_RETRY_DELAY_MS) {
1791
0
                pj_log(ctx, PJ_LOG_TRACE,
1792
0
                       "Got a HTTP %ld error. Retrying in %d ms", response_code,
1793
0
                       static_cast<int>(delay));
1794
0
                sleep_ms(static_cast<int>(delay));
1795
0
                oldDelay = delay;
1796
0
            } else {
1797
0
                if (out_error_string) {
1798
0
                    if (file->m_szCurlErrBuf[0]) {
1799
0
                        snprintf(out_error_string, error_string_max_size, "%s",
1800
0
                                 file->m_szCurlErrBuf);
1801
0
                    } else {
1802
0
                        snprintf(out_error_string, error_string_max_size,
1803
0
                                 "HTTP error %ld: %s", response_code,
1804
0
                                 body.c_str());
1805
0
                    }
1806
0
                }
1807
0
                return nullptr;
1808
0
            }
1809
0
        } else {
1810
0
            break;
1811
0
        }
1812
0
    }
1813
1814
0
    if (out_error_string && error_string_max_size) {
1815
0
        out_error_string[0] = '\0';
1816
0
    }
1817
1818
0
    if (!body.empty()) {
1819
0
        memcpy(buffer, body.data(), std::min(size_to_read, body.size()));
1820
0
    }
1821
0
    *out_size_read = std::min(size_to_read, body.size());
1822
1823
0
    file->m_headers = std::move(headers);
1824
0
    return reinterpret_cast<PROJ_NETWORK_HANDLE *>(file.release());
1825
0
}
1826
1827
// ---------------------------------------------------------------------------
1828
1829
static void pj_curl_close(PJ_CONTEXT *, PROJ_NETWORK_HANDLE *handle,
1830
0
                          void * /*user_data*/) {
1831
0
    delete reinterpret_cast<CurlFileHandle *>(handle);
1832
0
}
1833
1834
// ---------------------------------------------------------------------------
1835
1836
static size_t pj_curl_read_range(PJ_CONTEXT *ctx,
1837
                                 PROJ_NETWORK_HANDLE *raw_handle,
1838
                                 unsigned long long offset, size_t size_to_read,
1839
                                 void *buffer, size_t error_string_max_size,
1840
0
                                 char *out_error_string, void *) {
1841
0
    auto handle = reinterpret_cast<CurlFileHandle *>(raw_handle);
1842
0
    auto hCurlHandle = handle->m_handle;
1843
1844
0
    double oldDelay = MIN_RETRY_DELAY_MS;
1845
0
    std::string headers;
1846
0
    std::string body;
1847
1848
0
    char szBuffer[128];
1849
0
    sqlite3_snprintf(sizeof(szBuffer), szBuffer, "%llu-%llu", offset,
1850
0
                     offset + size_to_read - 1);
1851
1852
0
    while (true) {
1853
0
        CHECK_RET(ctx, curl_easy_setopt(hCurlHandle, CURLOPT_RANGE, szBuffer));
1854
1855
0
        headers.clear();
1856
0
        headers.reserve(16 * 1024);
1857
0
        CHECK_RET(ctx,
1858
0
                  curl_easy_setopt(hCurlHandle, CURLOPT_HEADERDATA, &headers));
1859
0
        CHECK_RET(ctx, curl_easy_setopt(hCurlHandle, CURLOPT_HEADERFUNCTION,
1860
0
                                        pj_curl_write_func));
1861
1862
0
        body.clear();
1863
0
        body.reserve(size_to_read);
1864
0
        CHECK_RET(ctx, curl_easy_setopt(hCurlHandle, CURLOPT_WRITEDATA, &body));
1865
0
        CHECK_RET(ctx, curl_easy_setopt(hCurlHandle, CURLOPT_WRITEFUNCTION,
1866
0
                                        pj_curl_write_func));
1867
1868
0
        handle->m_szCurlErrBuf[0] = '\0';
1869
1870
0
        curl_easy_perform(hCurlHandle);
1871
1872
0
        long response_code = 0;
1873
0
        curl_easy_getinfo(hCurlHandle, CURLINFO_HTTP_CODE, &response_code);
1874
1875
0
        CHECK_RET(ctx,
1876
0
                  curl_easy_setopt(hCurlHandle, CURLOPT_WRITEDATA, nullptr));
1877
0
        CHECK_RET(
1878
0
            ctx, curl_easy_setopt(hCurlHandle, CURLOPT_WRITEFUNCTION, nullptr));
1879
1880
0
        if (response_code == 0 || response_code >= 300) {
1881
0
            const double delay =
1882
0
                GetNewRetryDelay(static_cast<int>(response_code), oldDelay,
1883
0
                                 body.c_str(), handle->m_szCurlErrBuf);
1884
0
            if (delay != 0 && delay < MAX_RETRY_DELAY_MS) {
1885
0
                pj_log(ctx, PJ_LOG_TRACE,
1886
0
                       "Got a HTTP %ld error. Retrying in %d ms", response_code,
1887
0
                       static_cast<int>(delay));
1888
0
                sleep_ms(static_cast<int>(delay));
1889
0
                oldDelay = delay;
1890
0
            } else {
1891
0
                if (out_error_string) {
1892
0
                    if (handle->m_szCurlErrBuf[0]) {
1893
0
                        snprintf(out_error_string, error_string_max_size, "%s",
1894
0
                                 handle->m_szCurlErrBuf);
1895
0
                    } else {
1896
0
                        snprintf(out_error_string, error_string_max_size,
1897
0
                                 "HTTP error %ld: %s", response_code,
1898
0
                                 body.c_str());
1899
0
                    }
1900
0
                }
1901
0
                return 0;
1902
0
            }
1903
0
        } else {
1904
0
            break;
1905
0
        }
1906
0
    }
1907
0
    if (out_error_string && error_string_max_size) {
1908
0
        out_error_string[0] = '\0';
1909
0
    }
1910
1911
0
    if (!body.empty()) {
1912
0
        memcpy(buffer, body.data(), std::min(size_to_read, body.size()));
1913
0
    }
1914
0
    handle->m_headers = std::move(headers);
1915
1916
0
    return std::min(size_to_read, body.size());
1917
0
}
1918
1919
// ---------------------------------------------------------------------------
1920
1921
static const char *pj_curl_get_header_value(PJ_CONTEXT *,
1922
                                            PROJ_NETWORK_HANDLE *raw_handle,
1923
0
                                            const char *header_name, void *) {
1924
0
    auto handle = reinterpret_cast<CurlFileHandle *>(raw_handle);
1925
0
    auto pos = ci_find(handle->m_headers, header_name);
1926
0
    if (pos == std::string::npos)
1927
0
        return nullptr;
1928
0
    pos += strlen(header_name);
1929
0
    const char *c_str = handle->m_headers.c_str();
1930
0
    if (c_str[pos] == ':')
1931
0
        pos++;
1932
0
    while (c_str[pos] == ' ')
1933
0
        pos++;
1934
0
    auto posEnd = pos;
1935
0
    while (c_str[posEnd] != '\r' && c_str[posEnd] != '\n' &&
1936
0
           c_str[posEnd] != '\0')
1937
0
        posEnd++;
1938
0
    handle->m_lastval = handle->m_headers.substr(pos, posEnd - pos);
1939
0
    return handle->m_lastval.c_str();
1940
0
}
1941
1942
#elif defined(DO_EMSCRIPTEN_FETCH)
1943
1944
constexpr double MIN_RETRY_DELAY_MS = 500;
1945
constexpr double MAX_RETRY_DELAY_MS = 60000;
1946
struct EmscriptenFileHandle {
1947
    PJ_CONTEXT *m_ctx; // for logging? TODO
1948
    std::string m_url;
1949
    std::unordered_map<std::string, std::string> m_headers;
1950
    std::string m_lastval{};
1951
    std::string m_useragent{};
1952
1953
    EmscriptenFileHandle(const EmscriptenFileHandle &) = delete;
1954
    EmscriptenFileHandle &operator=(const EmscriptenFileHandle &) = delete;
1955
    explicit EmscriptenFileHandle(PJ_CONTEXT *ctx, const char *url);
1956
    ~EmscriptenFileHandle();
1957
1958
    static PROJ_NETWORK_HANDLE *
1959
    open(PJ_CONTEXT *, const char *url, unsigned long long offset,
1960
         size_t size_to_read, void *buffer, size_t *out_size_read,
1961
         size_t error_string_max_size, char *out_error_string, void *);
1962
};
1963
1964
EmscriptenFileHandle::EmscriptenFileHandle(PJ_CONTEXT *ctx, const char *url)
1965
    : m_ctx(ctx), m_url(url) {
1966
1967
    pj_log(ctx, PJ_LOG_DEBUG, "EmscriptenFileHandle created with url %s", url);
1968
    if (getenv("PROJ_NO_USERAGENT") == nullptr) {
1969
        m_useragent = "PROJ " STR(PROJ_VERSION_MAJOR) "." STR(
1970
            PROJ_VERSION_MINOR) "." STR(PROJ_VERSION_PATCH);
1971
        const auto exeName = std::string{}; // TODO: GetExecutableName();
1972
        if (!exeName.empty()) {
1973
            m_useragent = exeName + " using " + m_useragent;
1974
        }
1975
    }
1976
}
1977
1978
EmscriptenFileHandle::~EmscriptenFileHandle() {}
1979
1980
// Same as in Curl?
1981
static double GetNewRetryDelay(int response_code, double dfOldDelay,
1982
                               const char *pszErrBuf,
1983
                               const char *pszCurlError) {
1984
    if (response_code == 429 || response_code == 500 ||
1985
        (response_code >= 502 && response_code <= 504) ||
1986
        // S3 sends some client timeout errors as 400 Client Error
1987
        (response_code == 400 && pszErrBuf &&
1988
         strstr(pszErrBuf, "RequestTimeout")) ||
1989
        (pszCurlError && strstr(pszCurlError, "Connection reset by peer")) ||
1990
        (pszCurlError && strstr(pszCurlError, "Connection timed out")) ||
1991
        (pszCurlError && strstr(pszCurlError, "SSL connection timeout"))) {
1992
        // Use an exponential backoff factor of 2 plus some random jitter
1993
        // We don't care about cryptographic quality randomness, hence:
1994
        // coverity[dont_call]
1995
        return dfOldDelay * (2 + rand() * 0.5 / RAND_MAX);
1996
    } else {
1997
        return 0;
1998
    }
1999
}
2000
2001
#define DO_TRACE_FETCH 0
2002
#if DO_TRACE_FETCH != 0
2003
#define TRACE_FETCH(data)                                                      \
2004
    do {                                                                       \
2005
        std::cout << data << std::endl;                                        \
2006
    } while (false)
2007
#else
2008
#define TRACE_FETCH(data)
2009
#endif
2010
2011
static size_t pj_emscripten_read_range(PJ_CONTEXT *ctx,
2012
                                       PROJ_NETWORK_HANDLE *raw_handle,
2013
                                       unsigned long long offset,
2014
                                       size_t size_to_read, void *buffer,
2015
                                       size_t error_string_max_size,
2016
                                       char *out_error_string, void *) {
2017
    auto handle = reinterpret_cast<EmscriptenFileHandle *>(raw_handle);
2018
2019
    double oldDelay = MIN_RETRY_DELAY_MS;
2020
2021
    char szBuffer[128];
2022
    sqlite3_snprintf(sizeof(szBuffer), szBuffer, "bytes=%llu-%llu", offset,
2023
                     offset + size_to_read - 1);
2024
2025
    // To work in the browser, we need to run the fetch part in Web Worker,
2026
    // otherwise we cannot run it synchronous. (at least with my tests)
2027
    // It is easier running all PROJ in the Web Worker (that is the test done).
2028
    // Documentation says compiling with pthread flag is needed.
2029
    // https://emscripten.org/docs/api_reference/fetch.html#synchronous-fetches
2030
    // We encapsulate the code related to empscripten_fetch in a lambda.
2031
    // Some tests running this lambda in a thread were partially successful.
2032
    size_t real_read = 0;
2033
    const std::string url = handle->m_url;
2034
    auto fetching = [&]() {
2035
        emscripten_fetch_t *fetch = nullptr;
2036
        while (true) {
2037
2038
            emscripten_fetch_attr_t attr;
2039
            emscripten_fetch_attr_init(&attr);
2040
            strcpy(attr.requestMethod, "GET");
2041
            const char *requestHeaders[] = {"Range", szBuffer, nullptr};
2042
            attr.requestHeaders = requestHeaders;
2043
            attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY |
2044
                              EMSCRIPTEN_FETCH_SYNCHRONOUS |
2045
                              EMSCRIPTEN_FETCH_REPLACE;
2046
            if (fetch) {
2047
                emscripten_fetch_close(fetch);
2048
            }
2049
2050
            TRACE_FETCH("Pre-fetch, url: " << url << ", range: " << szBuffer);
2051
            fetch = emscripten_fetch(&attr, url.c_str());
2052
            // emscripten_fetch_wait(fetch, -1); // This is deprecated
2053
            TRACE_FETCH("Post-fetch");
2054
2055
            if (!fetch) {
2056
                snprintf(out_error_string, error_string_max_size,
2057
                         "Cannot init emscripten_fetch for url %s",
2058
                         url.c_str());
2059
                return;
2060
            }
2061
            const auto response_code = fetch->status;
2062
            TRACE_FETCH("Received HTTP response code: " << response_code);
2063
            if (response_code == 0 || response_code >= 300) {
2064
                const double delay =
2065
                    GetNewRetryDelay(static_cast<int>(response_code), oldDelay,
2066
                                     fetch->data, fetch->statusText);
2067
                if (delay != 0 && delay < MAX_RETRY_DELAY_MS) {
2068
                    pj_log(ctx, PJ_LOG_TRACE,
2069
                           "Got a HTTP %ld error. Retrying in %d ms",
2070
                           response_code, static_cast<int>(delay));
2071
                    TRACE_FETCH("HTTP error " << response_code
2072
                                              << ", retrying in " << delay
2073
                                              << " ms");
2074
2075
                    sleep_ms(static_cast<int>(delay));
2076
                    oldDelay = delay;
2077
                } else {
2078
                    if (out_error_string) {
2079
                        if (fetch->statusText[0]) {
2080
                            snprintf(out_error_string, error_string_max_size,
2081
                                     "%s", fetch->statusText);
2082
                        } else {
2083
                            snprintf(out_error_string, error_string_max_size,
2084
                                     "HTTP error %hu: %s", response_code,
2085
                                     fetch->data);
2086
                        }
2087
                    }
2088
                    TRACE_FETCH("HTTP error for " << url);
2089
                    emscripten_fetch_close(fetch);
2090
                    return;
2091
                }
2092
            } else {
2093
                break;
2094
            }
2095
        } // end of while(true)
2096
2097
        if (out_error_string && error_string_max_size) {
2098
            out_error_string[0] = '\0';
2099
        }
2100
2101
        const size_t numBytes = static_cast<size_t>(fetch->numBytes);
2102
2103
        TRACE_FETCH("Read bytes: " << numBytes);
2104
        TRACE_FETCH("Size to read: " << size_to_read);
2105
2106
        if (fetch->readyState != 4) {
2107
            std::cout << "fetch ready state (" << fetch->readyState
2108
                      << ") is not 4. That's a problem " << std::endl;
2109
            return;
2110
        }
2111
2112
        real_read = std::min(size_to_read, numBytes);
2113
        if (numBytes) {
2114
            memcpy(buffer, fetch->data, real_read);
2115
        }
2116
        TRACE_FETCH("Real read: " << real_read);
2117
2118
        // get the http headers
2119
        {
2120
            // https://github.com/emscripten-core/emscripten/blob/56c214a/test/fetch/test_fetch_headers_received.c
2121
            size_t headersLengthBytes =
2122
                emscripten_fetch_get_response_headers_length(fetch) + 1;
2123
            TRACE_FETCH("Headers length: " << headersLengthBytes);
2124
            char *headerString = (char *)malloc(headersLengthBytes);
2125
            assert(headerString);
2126
2127
            emscripten_fetch_get_response_headers(fetch, headerString,
2128
                                                  headersLengthBytes);
2129
            TRACE_FETCH("Raw headers:\n" << headerString);
2130
2131
            char **responseHeaders =
2132
                emscripten_fetch_unpack_response_headers(headerString);
2133
            assert(responseHeaders);
2134
2135
            free(headerString);
2136
2137
            TRACE_FETCH("Parsed headers:");
2138
            int numHeaders = 0;
2139
            for (; responseHeaders[numHeaders * 2]; ++numHeaders) {
2140
                // Check both the header and its value are present.
2141
                assert(responseHeaders[(numHeaders * 2) + 1]);
2142
                std::string key(responseHeaders[numHeaders * 2]);
2143
                std::string val(responseHeaders[(numHeaders * 2) + 1]);
2144
                TRACE_FETCH("  " << key << ": " << val);
2145
                std::transform(key.begin(), key.end(), key.begin(), ::tolower);
2146
                handle->m_headers.emplace(key, val);
2147
            }
2148
            TRACE_FETCH("Finished receiving " << numHeaders << " headers");
2149
            emscripten_fetch_free_unpacked_response_headers(responseHeaders);
2150
        }
2151
2152
        TRACE_FETCH("Pre-close");
2153
        emscripten_fetch_close(fetch);
2154
        TRACE_FETCH("Post-close");
2155
        return;
2156
    };
2157
    fetching();
2158
    TRACE_FETCH("Post-fetching");
2159
2160
    if (real_read == 0) {
2161
        std::cout << "Problems in fetch:" << out_error_string << std::endl;
2162
    }
2163
2164
    return real_read;
2165
}
2166
2167
static const char *
2168
pj_emscripten_get_header_value(PJ_CONTEXT *, PROJ_NETWORK_HANDLE *raw_handle,
2169
                               const char *header_name, void *) {
2170
    TRACE_FETCH("EmscriptenFileHandle::header_name " << header_name);
2171
    auto handle = reinterpret_cast<EmscriptenFileHandle *>(raw_handle);
2172
    std::string key(header_name);
2173
    std::transform(key.begin(), key.end(), key.begin(), ::tolower);
2174
    const auto it = handle->m_headers.find(key);
2175
    if (it != handle->m_headers.end()) {
2176
        TRACE_FETCH("EmscriptenFileHandle::header_value " << it->second);
2177
        return it->second.c_str();
2178
    }
2179
    return nullptr;
2180
}
2181
2182
static void pj_emscripten_close(PJ_CONTEXT *, PROJ_NETWORK_HANDLE *handle,
2183
                                void * /*user_data*/) {
2184
    delete reinterpret_cast<EmscriptenFileHandle *>(handle);
2185
}
2186
2187
PROJ_NETWORK_HANDLE *EmscriptenFileHandle::open(
2188
    PJ_CONTEXT *ctx, const char *url, unsigned long long offset,
2189
    size_t size_to_read, void *buffer, size_t *out_size_read,
2190
    size_t error_string_max_size, char *out_error_string, void *) {
2191
2192
    auto file = std::unique_ptr<EmscriptenFileHandle>(
2193
        new EmscriptenFileHandle(ctx, url));
2194
2195
    PROJ_NETWORK_HANDLE *handle =
2196
        reinterpret_cast<PROJ_NETWORK_HANDLE *>(file.get());
2197
    *out_size_read = pj_emscripten_read_range(ctx, handle, offset, size_to_read,
2198
                                              buffer, error_string_max_size,
2199
                                              out_error_string, nullptr);
2200
2201
    return reinterpret_cast<PROJ_NETWORK_HANDLE *>(file.release());
2202
}
2203
2204
#else
2205
2206
// ---------------------------------------------------------------------------
2207
2208
static PROJ_NETWORK_HANDLE *
2209
no_op_network_open(PJ_CONTEXT *, const char * /* url */,
2210
                   unsigned long long, /* offset */
2211
                   size_t,             /* size to read */
2212
                   void *,             /* buffer to update with bytes read*/
2213
                   size_t *,           /* output: size actually read */
2214
                   size_t error_string_max_size, char *out_error_string,
2215
                   void * /*user_data*/) {
2216
    if (out_error_string) {
2217
        snprintf(out_error_string, error_string_max_size, "%s",
2218
                 "Network functionality not available");
2219
    }
2220
    return nullptr;
2221
}
2222
2223
// ---------------------------------------------------------------------------
2224
2225
static void no_op_network_close(PJ_CONTEXT *, PROJ_NETWORK_HANDLE *,
2226
                                void * /*user_data*/) {}
2227
2228
#endif
2229
2230
// ---------------------------------------------------------------------------
2231
2232
1
void FileManager::fillDefaultNetworkInterface(PJ_CONTEXT *ctx) {
2233
1
#ifdef CURL_ENABLED
2234
1
    ctx->networking.open = CurlFileHandle::open;
2235
1
    ctx->networking.close = pj_curl_close;
2236
1
    ctx->networking.read_range = pj_curl_read_range;
2237
1
    ctx->networking.get_header_value = pj_curl_get_header_value;
2238
#elif defined(DO_EMSCRIPTEN_FETCH)
2239
    ctx->networking.open = EmscriptenFileHandle::open;
2240
    ctx->networking.close = pj_emscripten_close;
2241
    ctx->networking.read_range = pj_emscripten_read_range;
2242
    ctx->networking.get_header_value = pj_emscripten_get_header_value;
2243
#else
2244
    ctx->networking.open = no_op_network_open;
2245
    ctx->networking.close = no_op_network_close;
2246
#endif
2247
1
}
2248
2249
// ---------------------------------------------------------------------------
2250
2251
10.9k
void FileManager::clearMemoryCache() {
2252
10.9k
    gNetworkChunkCache.clearMemoryCache();
2253
10.9k
    gNetworkFileProperties.clearMemoryCache();
2254
10.9k
}
2255
2256
NS_PROJ_END
2257
2258
//! @endcond
2259
2260
// ---------------------------------------------------------------------------
2261
2262
#ifdef WIN32
2263
static const char nfm_dir_chars[] = "/\\";
2264
#else
2265
static const char nfm_dir_chars[] = "/";
2266
#endif
2267
2268
0
static bool nfm_is_tilde_slash(const char *name) {
2269
0
    return *name == '~' && strchr(nfm_dir_chars, name[1]);
2270
0
}
2271
2272
0
static bool nfm_is_rel_or_absolute_filename(const char *name) {
2273
0
    return strchr(nfm_dir_chars, *name) ||
2274
0
           (*name == '.' && strchr(nfm_dir_chars, name[1])) ||
2275
0
           (!strncmp(name, "..", 2) && strchr(nfm_dir_chars, name[2])) ||
2276
0
           (name[0] != '\0' && name[1] == ':' &&
2277
0
            strchr(nfm_dir_chars, name[2]));
2278
0
}
2279
2280
0
static std::string build_url(PJ_CONTEXT *ctx, const char *name) {
2281
0
    if (!nfm_is_tilde_slash(name) && !nfm_is_rel_or_absolute_filename(name) &&
2282
0
        !starts_with(name, "http://") && !starts_with(name, "https://")) {
2283
0
        std::string remote_file(proj_context_get_url_endpoint(ctx));
2284
0
        if (!remote_file.empty()) {
2285
0
            if (remote_file.back() != '/') {
2286
0
                remote_file += '/';
2287
0
            }
2288
0
            remote_file += name;
2289
0
        }
2290
0
        return remote_file;
2291
0
    }
2292
0
    return name;
2293
0
}
2294
2295
// ---------------------------------------------------------------------------
2296
2297
/** Define a custom set of callbacks for network access.
2298
 *
2299
 * All callbacks should be provided (non NULL pointers).
2300
 *
2301
 * @param ctx PROJ context, or NULL
2302
 * @param open_cbk Callback to open a remote file given its URL
2303
 * @param close_cbk Callback to close a remote file.
2304
 * @param get_header_value_cbk Callback to get HTTP headers
2305
 * @param read_range_cbk Callback to read a range of bytes inside a remote file.
2306
 * @param user_data Arbitrary pointer provided by the user, and passed to the
2307
 * above callbacks. May be NULL.
2308
 * @return TRUE in case of success.
2309
 * @since 7.0
2310
 */
2311
int proj_context_set_network_callbacks(
2312
    PJ_CONTEXT *ctx, proj_network_open_cbk_type open_cbk,
2313
    proj_network_close_cbk_type close_cbk,
2314
    proj_network_get_header_value_cbk_type get_header_value_cbk,
2315
0
    proj_network_read_range_type read_range_cbk, void *user_data) {
2316
0
    if (ctx == nullptr) {
2317
0
        ctx = pj_get_default_ctx();
2318
0
    }
2319
0
    if (!open_cbk || !close_cbk || !get_header_value_cbk || !read_range_cbk) {
2320
0
        return false;
2321
0
    }
2322
0
    ctx->networking.open = open_cbk;
2323
0
    ctx->networking.close = close_cbk;
2324
0
    ctx->networking.get_header_value = get_header_value_cbk;
2325
0
    ctx->networking.read_range = read_range_cbk;
2326
0
    ctx->networking.user_data = user_data;
2327
0
    return true;
2328
0
}
2329
2330
// ---------------------------------------------------------------------------
2331
2332
/** Enable or disable network access.
2333
 *
2334
 * This overrides the default endpoint in the PROJ configuration file or with
2335
 * the PROJ_NETWORK environment variable.
2336
 *
2337
 * @param ctx PROJ context, or NULL
2338
 * @param enable TRUE if network access is allowed.
2339
 * @return TRUE if network access is possible. That is either libcurl is
2340
 *         available, or an alternate interface has been set.
2341
 * @since 7.0
2342
 */
2343
6
int proj_context_set_enable_network(PJ_CONTEXT *ctx, int enable) {
2344
6
    if (ctx == nullptr) {
2345
0
        ctx = pj_get_default_ctx();
2346
0
    }
2347
    // Load ini file, now so as to override its network settings
2348
6
    pj_load_ini(ctx);
2349
6
    ctx->networking.enabled = enable != FALSE;
2350
6
#ifdef CURL_ENABLED
2351
6
    return ctx->networking.enabled;
2352
#elif defined(DO_EMSCRIPTEN_FETCH)
2353
    return ctx->networking.enabled;
2354
#else
2355
    return ctx->networking.enabled &&
2356
           ctx->networking.open != NS_PROJ::no_op_network_open;
2357
#endif
2358
6
}
2359
2360
// ---------------------------------------------------------------------------
2361
2362
/** Return if network access is enabled.
2363
 *
2364
 * @param ctx PROJ context, or NULL
2365
 * @return TRUE if network access has been enabled
2366
 * @since 7.0
2367
 */
2368
295k
int proj_context_is_network_enabled(PJ_CONTEXT *ctx) {
2369
295k
    if (ctx == nullptr) {
2370
0
        ctx = pj_get_default_ctx();
2371
0
    }
2372
295k
    pj_load_ini(ctx);
2373
295k
    return ctx->networking.enabled;
2374
295k
}
2375
2376
// ---------------------------------------------------------------------------
2377
2378
/** Define the URL endpoint to query for remote grids.
2379
 *
2380
 * This overrides the default endpoint in the PROJ configuration file or with
2381
 * the PROJ_NETWORK_ENDPOINT environment variable.
2382
 *
2383
 * @param ctx PROJ context, or NULL
2384
 * @param url Endpoint URL. Must NOT be NULL.
2385
 * @since 7.0
2386
 */
2387
0
void proj_context_set_url_endpoint(PJ_CONTEXT *ctx, const char *url) {
2388
0
    if (ctx == nullptr) {
2389
0
        ctx = pj_get_default_ctx();
2390
0
    }
2391
    // Load ini file, now so as to override its network settings
2392
0
    pj_load_ini(ctx);
2393
0
    ctx->endpoint = url;
2394
0
}
2395
2396
// ---------------------------------------------------------------------------
2397
2398
/** Enable or disable the local cache of grid chunks
2399
 *
2400
 * This overrides the setting in the PROJ configuration file.
2401
 *
2402
 * @param ctx PROJ context, or NULL
2403
 * @param enabled TRUE if the cache is enabled.
2404
 * @since 7.0
2405
 */
2406
0
void proj_grid_cache_set_enable(PJ_CONTEXT *ctx, int enabled) {
2407
0
    if (ctx == nullptr) {
2408
0
        ctx = pj_get_default_ctx();
2409
0
    }
2410
    // Load ini file, now so as to override its settings
2411
0
    pj_load_ini(ctx);
2412
0
    ctx->gridChunkCache.enabled = enabled != FALSE;
2413
0
}
2414
2415
// ---------------------------------------------------------------------------
2416
2417
/** Override, for the considered context, the path and file of the local
2418
 * cache of grid chunks.
2419
 *
2420
 * @param ctx PROJ context, or NULL
2421
 * @param fullname Full name to the cache (encoded in UTF-8). If set to NULL,
2422
 *                 caching will be disabled.
2423
 * @since 7.0
2424
 */
2425
0
void proj_grid_cache_set_filename(PJ_CONTEXT *ctx, const char *fullname) {
2426
0
    if (ctx == nullptr) {
2427
0
        ctx = pj_get_default_ctx();
2428
0
    }
2429
    // Load ini file, now so as to override its settings
2430
0
    pj_load_ini(ctx);
2431
0
    ctx->gridChunkCache.filename = fullname ? fullname : std::string();
2432
0
}
2433
2434
// ---------------------------------------------------------------------------
2435
2436
/** Override, for the considered context, the maximum size of the local
2437
 * cache of grid chunks.
2438
 *
2439
 * @param ctx PROJ context, or NULL
2440
 * @param max_size_MB Maximum size, in mega-bytes (1024*1024 bytes), or
2441
 *                    negative value to set unlimited size.
2442
 * @since 7.0
2443
 */
2444
0
void proj_grid_cache_set_max_size(PJ_CONTEXT *ctx, int max_size_MB) {
2445
0
    if (ctx == nullptr) {
2446
0
        ctx = pj_get_default_ctx();
2447
0
    }
2448
    // Load ini file, now so as to override its settings
2449
0
    pj_load_ini(ctx);
2450
0
    ctx->gridChunkCache.max_size =
2451
0
        max_size_MB < 0 ? -1
2452
0
                        : static_cast<long long>(max_size_MB) * 1024 * 1024;
2453
0
    if (max_size_MB == 0) {
2454
        // For debug purposes only
2455
0
        const char *env_var = getenv("PROJ_GRID_CACHE_MAX_SIZE_BYTES");
2456
0
        if (env_var && env_var[0] != '\0') {
2457
0
            ctx->gridChunkCache.max_size = atoi(env_var);
2458
0
        }
2459
0
    }
2460
0
}
2461
2462
// ---------------------------------------------------------------------------
2463
2464
/** Override, for the considered context, the time-to-live delay for
2465
 * re-checking if the cached properties of files are still up-to-date.
2466
 *
2467
 * @param ctx PROJ context, or NULL
2468
 * @param ttl_seconds Delay in seconds. Use negative value for no expiration.
2469
 * @since 7.0
2470
 */
2471
0
void proj_grid_cache_set_ttl(PJ_CONTEXT *ctx, int ttl_seconds) {
2472
0
    if (ctx == nullptr) {
2473
0
        ctx = pj_get_default_ctx();
2474
0
    }
2475
    // Load ini file, now so as to override its settings
2476
0
    pj_load_ini(ctx);
2477
0
    ctx->gridChunkCache.ttl = ttl_seconds;
2478
0
}
2479
2480
// ---------------------------------------------------------------------------
2481
2482
/** Clear the local cache of grid chunks.
2483
 *
2484
 * @param ctx PROJ context, or NULL
2485
 * @since 7.0
2486
 */
2487
0
void proj_grid_cache_clear(PJ_CONTEXT *ctx) {
2488
0
    if (ctx == nullptr) {
2489
0
        ctx = pj_get_default_ctx();
2490
0
    }
2491
0
    NS_PROJ::gNetworkChunkCache.clearDiskChunkCache(ctx);
2492
0
}
2493
2494
// ---------------------------------------------------------------------------
2495
2496
/** Return if a file must be downloaded or is already available in the
2497
 * PROJ user-writable directory.
2498
 *
2499
 * The file will be determinted to have to be downloaded if it does not exist
2500
 * yet in the user-writable directory, or if it is determined that a more recent
2501
 * version exists. To determine if a more recent version exists, PROJ will
2502
 * use the "downloaded_file_properties" table of its grid cache database.
2503
 * Consequently files manually placed in the user-writable
2504
 * directory without using this function would be considered as
2505
 * non-existing/obsolete and would be unconditionally downloaded again.
2506
 *
2507
 * This function can only be used if networking is enabled, and either
2508
 * the default curl network API or a custom one have been installed.
2509
 *
2510
 * @param ctx PROJ context, or NULL
2511
 * @param url_or_filename URL or filename (without directory component)
2512
 * @param ignore_ttl_setting If set to FALSE, PROJ will only check the
2513
 *                           recentness of an already downloaded file, if
2514
 *                           the delay between the last time it has been
2515
 *                           verified and the current time exceeds the TTL
2516
 *                           setting. This can save network accesses.
2517
 *                           If set to TRUE, PROJ will unconditionally
2518
 *                           check from the server the recentness of the file.
2519
 * @return TRUE if the file must be downloaded with proj_download_file()
2520
 * @since 7.0
2521
 */
2522
2523
int proj_is_download_needed(PJ_CONTEXT *ctx, const char *url_or_filename,
2524
0
                            int ignore_ttl_setting) {
2525
0
    if (ctx == nullptr) {
2526
0
        ctx = pj_get_default_ctx();
2527
0
    }
2528
0
    if (!proj_context_is_network_enabled(ctx)) {
2529
0
        pj_log(ctx, PJ_LOG_ERROR, "Networking capabilities are not enabled");
2530
0
        return false;
2531
0
    }
2532
2533
0
    const auto url(build_url(ctx, url_or_filename));
2534
0
    const char *filename = strrchr(url.c_str(), '/');
2535
0
    if (filename == nullptr)
2536
0
        return false;
2537
0
    const auto localFilename(
2538
0
        std::string(proj_context_get_user_writable_directory(ctx, false)) +
2539
0
        filename);
2540
2541
0
    auto f = NS_PROJ::FileManager::open(ctx, localFilename.c_str(),
2542
0
                                        NS_PROJ::FileAccess::READ_ONLY);
2543
0
    if (!f) {
2544
0
        return true;
2545
0
    }
2546
0
    f.reset();
2547
2548
0
    auto diskCache = NS_PROJ::DiskChunkCache::open(ctx);
2549
0
    if (!diskCache)
2550
0
        return false;
2551
0
    auto stmt =
2552
0
        diskCache->prepare("SELECT lastChecked, fileSize, lastModified, etag "
2553
0
                           "FROM downloaded_file_properties WHERE url = ?");
2554
0
    if (!stmt)
2555
0
        return true;
2556
0
    stmt->bindText(url.c_str());
2557
0
    if (stmt->execute() != SQLITE_ROW) {
2558
0
        return true;
2559
0
    }
2560
2561
0
    NS_PROJ::FileProperties cachedProps;
2562
0
    cachedProps.lastChecked = static_cast<time_t>(stmt->getInt64());
2563
0
    cachedProps.size = stmt->getInt64();
2564
0
    const char *lastModified = stmt->getText();
2565
0
    cachedProps.lastModified = lastModified ? lastModified : std::string();
2566
0
    const char *etag = stmt->getText();
2567
0
    cachedProps.etag = etag ? etag : std::string();
2568
2569
0
    if (!ignore_ttl_setting) {
2570
0
        const auto ttl = NS_PROJ::pj_context_get_grid_cache_ttl(ctx);
2571
0
        if (ttl > 0) {
2572
0
            time_t curTime;
2573
0
            time(&curTime);
2574
0
            if (curTime > cachedProps.lastChecked + ttl) {
2575
2576
0
                unsigned char dummy;
2577
0
                size_t size_read = 0;
2578
0
                std::string errorBuffer;
2579
0
                errorBuffer.resize(1024);
2580
0
                auto handle = ctx->networking.open(
2581
0
                    ctx, url.c_str(), 0, 1, &dummy, &size_read,
2582
0
                    errorBuffer.size(), &errorBuffer[0],
2583
0
                    ctx->networking.user_data);
2584
0
                if (!handle) {
2585
0
                    errorBuffer.resize(strlen(errorBuffer.data()));
2586
0
                    pj_log(ctx, PJ_LOG_ERROR, "Cannot open %s: %s", url.c_str(),
2587
0
                           errorBuffer.c_str());
2588
0
                    return false;
2589
0
                }
2590
0
                NS_PROJ::FileProperties props;
2591
0
                if (!NS_PROJ::NetworkFile::get_props_from_headers(ctx, handle,
2592
0
                                                                  props)) {
2593
0
                    ctx->networking.close(ctx, handle,
2594
0
                                          ctx->networking.user_data);
2595
0
                    return false;
2596
0
                }
2597
0
                ctx->networking.close(ctx, handle, ctx->networking.user_data);
2598
2599
0
                if (props.size != cachedProps.size ||
2600
0
                    props.lastModified != cachedProps.lastModified ||
2601
0
                    props.etag != cachedProps.etag) {
2602
0
                    return true;
2603
0
                }
2604
2605
0
                stmt = diskCache->prepare(
2606
0
                    "UPDATE downloaded_file_properties SET lastChecked = ? "
2607
0
                    "WHERE url = ?");
2608
0
                if (!stmt)
2609
0
                    return false;
2610
0
                stmt->bindInt64(curTime);
2611
0
                stmt->bindText(url.c_str());
2612
0
                if (stmt->execute() != SQLITE_DONE) {
2613
0
                    auto hDB = diskCache->handle();
2614
0
                    pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
2615
0
                    return false;
2616
0
                }
2617
0
            }
2618
0
        }
2619
0
    }
2620
2621
0
    return false;
2622
0
}
2623
2624
// ---------------------------------------------------------------------------
2625
2626
0
static NS_PROJ::io::DatabaseContextPtr nfm_getDBcontext(PJ_CONTEXT *ctx) {
2627
0
    try {
2628
0
        return ctx->get_cpp_context()->getDatabaseContext().as_nullable();
2629
0
    } catch (const std::exception &e) {
2630
0
        pj_log(ctx, PJ_LOG_DEBUG, "%s", e.what());
2631
0
        return nullptr;
2632
0
    }
2633
0
}
2634
2635
// ---------------------------------------------------------------------------
2636
2637
/** Download a file in the PROJ user-writable directory.
2638
 *
2639
 * The file will only be downloaded if it does not exist yet in the
2640
 * user-writable directory, or if it is determined that a more recent
2641
 * version exists. To determine if a more recent version exists, PROJ will
2642
 * use the "downloaded_file_properties" table of its grid cache database.
2643
 * Consequently files manually placed in the user-writable
2644
 * directory without using this function would be considered as
2645
 * non-existing/obsolete and would be unconditionally downloaded again.
2646
 *
2647
 * This function can only be used if networking is enabled, and either
2648
 * the default curl network API or a custom one have been installed.
2649
 *
2650
 * @param ctx PROJ context, or NULL
2651
 * @param url_or_filename URL or filename (without directory component)
2652
 * @param ignore_ttl_setting If set to FALSE, PROJ will only check the
2653
 *                           recentness of an already downloaded file, if
2654
 *                           the delay between the last time it has been
2655
 *                           verified and the current time exceeds the TTL
2656
 *                           setting. This can save network accesses.
2657
 *                           If set to TRUE, PROJ will unconditionally
2658
 *                           check from the server the recentness of the file.
2659
 * @param progress_cbk Progress callback, or NULL.
2660
 *                     The passed percentage is in the [0, 1] range.
2661
 *                     The progress callback must return TRUE
2662
 *                     if download must be continued.
2663
 * @param user_data User data to provide to the progress callback, or NULL
2664
 * @return TRUE if the download was successful (or not needed)
2665
 * @since 7.0
2666
 */
2667
2668
int proj_download_file(PJ_CONTEXT *ctx, const char *url_or_filename,
2669
                       int ignore_ttl_setting,
2670
                       int (*progress_cbk)(PJ_CONTEXT *, double pct,
2671
                                           void *user_data),
2672
0
                       void *user_data) {
2673
0
    if (ctx == nullptr) {
2674
0
        ctx = pj_get_default_ctx();
2675
0
    }
2676
0
    if (!proj_context_is_network_enabled(ctx)) {
2677
0
        pj_log(ctx, PJ_LOG_ERROR, "Networking capabilities are not enabled");
2678
0
        return false;
2679
0
    }
2680
0
    if (!proj_is_download_needed(ctx, url_or_filename, ignore_ttl_setting)) {
2681
0
        return true;
2682
0
    }
2683
2684
0
    const auto url(build_url(ctx, url_or_filename));
2685
0
    const char *lastSlash = strrchr(url.c_str(), '/');
2686
0
    if (lastSlash == nullptr)
2687
0
        return false;
2688
0
    const auto localFilename(
2689
0
        std::string(proj_context_get_user_writable_directory(ctx, true)) +
2690
0
        lastSlash);
2691
2692
    // Evict potential existing (empty) entry from ctx->lookupedFiles if we
2693
    // have tried previously from accessing the non-existing local file.
2694
    // Cf https://github.com/OSGeo/PROJ/issues/4397
2695
0
    {
2696
0
        const char *short_filename = lastSlash + 1;
2697
0
        auto iter = ctx->lookupedFiles.find(short_filename);
2698
0
        if (iter != ctx->lookupedFiles.end()) {
2699
0
            ctx->lookupedFiles.erase(iter);
2700
0
        }
2701
2702
0
        auto dbContext = nfm_getDBcontext(ctx);
2703
0
        if (dbContext) {
2704
0
            dbContext->invalidateGridInfo(short_filename);
2705
0
        }
2706
0
    }
2707
2708
#ifdef _WIN32
2709
    const int nPID = GetCurrentProcessId();
2710
#else
2711
0
    const int nPID = getpid();
2712
0
#endif
2713
0
    char szUniqueSuffix[128];
2714
0
    snprintf(szUniqueSuffix, sizeof(szUniqueSuffix), "%d_%p", nPID,
2715
0
             static_cast<const void *>(&url));
2716
0
    const auto localFilenameTmp(localFilename + szUniqueSuffix);
2717
0
    auto f = NS_PROJ::FileManager::open(ctx, localFilenameTmp.c_str(),
2718
0
                                        NS_PROJ::FileAccess::CREATE);
2719
0
    if (!f) {
2720
0
        pj_log(ctx, PJ_LOG_ERROR, "Cannot create %s", localFilenameTmp.c_str());
2721
0
        return false;
2722
0
    }
2723
2724
0
    constexpr size_t FULL_FILE_CHUNK_SIZE = 1024 * 1024;
2725
0
    std::vector<unsigned char> buffer(FULL_FILE_CHUNK_SIZE);
2726
    // For testing purposes only
2727
0
    const char *env_var_PROJ_FULL_FILE_CHUNK_SIZE =
2728
0
        getenv("PROJ_FULL_FILE_CHUNK_SIZE");
2729
0
    if (env_var_PROJ_FULL_FILE_CHUNK_SIZE &&
2730
0
        env_var_PROJ_FULL_FILE_CHUNK_SIZE[0] != '\0') {
2731
0
        buffer.resize(atoi(env_var_PROJ_FULL_FILE_CHUNK_SIZE));
2732
0
    }
2733
0
    size_t size_read = 0;
2734
0
    std::string errorBuffer;
2735
0
    errorBuffer.resize(1024);
2736
0
    auto handle = ctx->networking.open(
2737
0
        ctx, url.c_str(), 0, buffer.size(), &buffer[0], &size_read,
2738
0
        errorBuffer.size(), &errorBuffer[0], ctx->networking.user_data);
2739
0
    if (!handle) {
2740
0
        errorBuffer.resize(strlen(errorBuffer.data()));
2741
0
        pj_log(ctx, PJ_LOG_ERROR, "Cannot open %s: %s", url.c_str(),
2742
0
               errorBuffer.c_str());
2743
0
        f.reset();
2744
0
        NS_PROJ::FileManager::unlink(ctx, localFilenameTmp.c_str());
2745
0
        return false;
2746
0
    }
2747
2748
0
    time_t curTime;
2749
0
    time(&curTime);
2750
0
    NS_PROJ::FileProperties props;
2751
0
    if (!NS_PROJ::NetworkFile::get_props_from_headers(ctx, handle, props)) {
2752
0
        ctx->networking.close(ctx, handle, ctx->networking.user_data);
2753
0
        f.reset();
2754
0
        NS_PROJ::FileManager::unlink(ctx, localFilenameTmp.c_str());
2755
0
        return false;
2756
0
    }
2757
2758
0
    if (size_read == 0) {
2759
0
        pj_log(ctx, PJ_LOG_ERROR, "Did not get as many bytes as expected");
2760
0
        ctx->networking.close(ctx, handle, ctx->networking.user_data);
2761
0
        f.reset();
2762
0
        NS_PROJ::FileManager::unlink(ctx, localFilenameTmp.c_str());
2763
0
        return false;
2764
0
    }
2765
0
    if (f->write(buffer.data(), size_read) != size_read) {
2766
0
        pj_log(ctx, PJ_LOG_ERROR, "Write error");
2767
0
        ctx->networking.close(ctx, handle, ctx->networking.user_data);
2768
0
        f.reset();
2769
0
        NS_PROJ::FileManager::unlink(ctx, localFilenameTmp.c_str());
2770
0
        return false;
2771
0
    }
2772
2773
0
    unsigned long long totalDownloaded = size_read;
2774
0
    while (totalDownloaded < props.size) {
2775
0
        if (totalDownloaded + buffer.size() > props.size) {
2776
0
            buffer.resize(static_cast<size_t>(props.size - totalDownloaded));
2777
0
        }
2778
0
        errorBuffer.resize(1024);
2779
0
        size_read = ctx->networking.read_range(
2780
0
            ctx, handle, totalDownloaded, buffer.size(), &buffer[0],
2781
0
            errorBuffer.size(), &errorBuffer[0], ctx->networking.user_data);
2782
2783
0
        if (size_read < buffer.size()) {
2784
0
            pj_log(ctx, PJ_LOG_ERROR, "Did not get as many bytes as expected");
2785
0
            ctx->networking.close(ctx, handle, ctx->networking.user_data);
2786
0
            f.reset();
2787
0
            NS_PROJ::FileManager::unlink(ctx, localFilenameTmp.c_str());
2788
0
            return false;
2789
0
        }
2790
0
        if (f->write(buffer.data(), size_read) != size_read) {
2791
0
            pj_log(ctx, PJ_LOG_ERROR, "Write error");
2792
0
            ctx->networking.close(ctx, handle, ctx->networking.user_data);
2793
0
            f.reset();
2794
0
            NS_PROJ::FileManager::unlink(ctx, localFilenameTmp.c_str());
2795
0
            return false;
2796
0
        }
2797
2798
0
        totalDownloaded += size_read;
2799
0
        if (progress_cbk &&
2800
0
            !progress_cbk(ctx, double(totalDownloaded) / props.size,
2801
0
                          user_data)) {
2802
0
            ctx->networking.close(ctx, handle, ctx->networking.user_data);
2803
0
            f.reset();
2804
0
            NS_PROJ::FileManager::unlink(ctx, localFilenameTmp.c_str());
2805
0
            return false;
2806
0
        }
2807
0
    }
2808
2809
0
    ctx->networking.close(ctx, handle, ctx->networking.user_data);
2810
0
    f.reset();
2811
0
    NS_PROJ::FileManager::unlink(ctx, localFilename.c_str());
2812
0
    if (!NS_PROJ::FileManager::rename(ctx, localFilenameTmp.c_str(),
2813
0
                                      localFilename.c_str())) {
2814
0
        pj_log(ctx, PJ_LOG_ERROR, "Cannot rename %s to %s",
2815
0
               localFilenameTmp.c_str(), localFilename.c_str());
2816
0
        return false;
2817
0
    }
2818
2819
0
    auto diskCache = NS_PROJ::DiskChunkCache::open(ctx);
2820
0
    if (!diskCache)
2821
0
        return false;
2822
0
    auto stmt =
2823
0
        diskCache->prepare("SELECT lastChecked, fileSize, lastModified, etag "
2824
0
                           "FROM downloaded_file_properties WHERE url = ?");
2825
0
    if (!stmt)
2826
0
        return false;
2827
0
    stmt->bindText(url.c_str());
2828
2829
0
    props.lastChecked = curTime;
2830
0
    auto hDB = diskCache->handle();
2831
2832
0
    if (stmt->execute() == SQLITE_ROW) {
2833
0
        stmt = diskCache->prepare(
2834
0
            "UPDATE downloaded_file_properties SET lastChecked = ?, "
2835
0
            "fileSize = ?, lastModified = ?, etag = ? "
2836
0
            "WHERE url = ?");
2837
0
        if (!stmt)
2838
0
            return false;
2839
0
        stmt->bindInt64(props.lastChecked);
2840
0
        stmt->bindInt64(props.size);
2841
0
        if (props.lastModified.empty())
2842
0
            stmt->bindNull();
2843
0
        else
2844
0
            stmt->bindText(props.lastModified.c_str());
2845
0
        if (props.etag.empty())
2846
0
            stmt->bindNull();
2847
0
        else
2848
0
            stmt->bindText(props.etag.c_str());
2849
0
        stmt->bindText(url.c_str());
2850
0
        if (stmt->execute() != SQLITE_DONE) {
2851
0
            pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
2852
0
            return false;
2853
0
        }
2854
0
    } else {
2855
0
        stmt = diskCache->prepare(
2856
0
            "INSERT INTO downloaded_file_properties (url, lastChecked, "
2857
0
            "fileSize, lastModified, etag) VALUES "
2858
0
            "(?,?,?,?,?)");
2859
0
        if (!stmt)
2860
0
            return false;
2861
0
        stmt->bindText(url.c_str());
2862
0
        stmt->bindInt64(props.lastChecked);
2863
0
        stmt->bindInt64(props.size);
2864
0
        if (props.lastModified.empty())
2865
0
            stmt->bindNull();
2866
0
        else
2867
0
            stmt->bindText(props.lastModified.c_str());
2868
0
        if (props.etag.empty())
2869
0
            stmt->bindNull();
2870
0
        else
2871
0
            stmt->bindText(props.etag.c_str());
2872
0
        if (stmt->execute() != SQLITE_DONE) {
2873
0
            pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
2874
0
            return false;
2875
0
        }
2876
0
    }
2877
0
    return true;
2878
0
}
2879
2880
// ---------------------------------------------------------------------------
2881
2882
//! @cond Doxygen_Suppress
2883
2884
// ---------------------------------------------------------------------------
2885
2886
0
std::string pj_context_get_grid_cache_filename(PJ_CONTEXT *ctx) {
2887
0
    pj_load_ini(ctx);
2888
0
    if (!ctx->gridChunkCache.filename.empty()) {
2889
0
        return ctx->gridChunkCache.filename;
2890
0
    }
2891
0
    const std::string path(proj_context_get_user_writable_directory(ctx, true));
2892
0
    ctx->gridChunkCache.filename = path + "/cache.db";
2893
0
    return ctx->gridChunkCache.filename;
2894
0
}
2895
2896
//! @endcond