Coverage Report

Created: 2025-07-11 06:33

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