• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-core / 1752

12 Oct 2023 07:28AM UTC coverage: 91.595% (+0.004%) from 91.591%
1752

push

Evergreen

web-flow
SyncFileManager::preferred_realm_path_without_suffix did not remove the suffix (#7044)

94310 of 173488 branches covered (0.0%)

36 of 37 new or added lines in 2 files covered. (97.3%)

44 existing lines in 11 files now uncovered.

230526 of 251680 relevant lines covered (91.59%)

6869596.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.49
/src/realm/object-store/sync/impl/sync_file.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2016 Realm Inc.
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
#include <realm/object-store/sync/impl/sync_file.hpp>
20

21
#include <realm/db.hpp>
22
#include <realm/util/file.hpp>
23
#include <realm/util/hex_dump.hpp>
24
#include <realm/util/sha_crypto.hpp>
25
#include <realm/util/time.hpp>
26
#include <realm/util/scope_exit.hpp>
27

28
#include <iomanip>
29
#include <sstream>
30
#include <system_error>
31
#include <fstream>
32

33
#ifdef _WIN32
34
#include <io.h>
35
#include <fcntl.h>
36

37
inline static int mkstemp(char* _template)
38
{
39
    return _open(_mktemp(_template), _O_CREAT | _O_TEMPORARY, _S_IREAD | _S_IWRITE);
40
}
41
#else
42
#include <unistd.h>
43
#endif
44

45

46
using File = realm::util::File;
47

48
namespace realm {
49

50
namespace {
51

52
uint8_t value_of_hex_digit(char hex_digit)
53
{
76✔
54
    if (hex_digit >= '0' && hex_digit <= '9') {
76✔
55
        return hex_digit - '0';
40✔
56
    }
40✔
57
    else if (hex_digit >= 'A' && hex_digit <= 'F') {
36✔
58
        return 10 + hex_digit - 'A';
36✔
59
    }
36✔
60
    else if (hex_digit >= 'a' && hex_digit <= 'f') {
×
61
        return 10 + hex_digit - 'a';
×
62
    }
×
63
    else {
×
64
        throw std::invalid_argument("Cannot get the value of a character that isn't a hex digit.");
×
65
    }
×
66
}
76✔
67

68
bool filename_is_reserved(const std::string& filename)
69
{
5,291✔
70
    return (filename == "." || filename == "..");
5,291✔
71
}
5,291✔
72

73
bool character_is_unreserved(char character)
74
{
53,033✔
75
    bool is_capital_letter = (character >= 'A' && character <= 'Z');
53,033✔
76
    bool is_lowercase_letter = (character >= 'a' && character <= 'z');
53,033✔
77
    bool is_number = (character >= '0' && character <= '9');
53,033✔
78
    bool is_allowed_symbol = (character == '-' || character == '_' || character == '.');
53,033✔
79
    return is_capital_letter || is_lowercase_letter || is_number || is_allowed_symbol;
53,033✔
80
}
53,033✔
81

82
char decoded_char_for(const std::string& percent_encoding, size_t index)
83
{
38✔
84
    if (index + 2 >= percent_encoding.length()) {
38✔
85
        throw std::invalid_argument("Malformed string: not enough characters after '%' before end of string.");
×
86
    }
×
87
    REALM_ASSERT(percent_encoding[index] == '%');
38✔
88
    return (16 * value_of_hex_digit(percent_encoding[index + 1])) + value_of_hex_digit(percent_encoding[index + 2]);
38✔
89
}
38✔
90

91
} // namespace
92

93
namespace util {
94

95
std::string make_percent_encoded_string(const std::string& raw_string)
96
{
5,373✔
97
    std::string buffer;
5,373✔
98
    buffer.reserve(raw_string.size());
5,373✔
99
    for (size_t i = 0; i < raw_string.size(); i++) {
58,338✔
100
        unsigned char character = raw_string[i];
52,965✔
101
        if (character_is_unreserved(character)) {
52,965✔
102
            buffer.push_back(character);
51,463✔
103
        }
51,463✔
104
        else {
1,502✔
105
            buffer.resize(buffer.size() + 3);
1,502✔
106
            // Format string must resolve to exactly 3 characters.
751✔
107
            snprintf(&buffer.back() - 2, 4, "%%%2X", character);
1,502✔
108
        }
1,502✔
109
    }
52,965✔
110
    return buffer;
5,373✔
111
}
5,373✔
112

113
std::string make_raw_string(const std::string& percent_encoded_string)
114
{
4✔
115
    std::string buffer;
4✔
116
    size_t input_len = percent_encoded_string.length();
4✔
117
    buffer.reserve(input_len);
4✔
118
    size_t idx = 0;
4✔
119
    while (idx < input_len) {
110✔
120
        char current = percent_encoded_string[idx];
106✔
121
        if (current == '%') {
106✔
122
            // Decode. +3.
19✔
123
            buffer.push_back(decoded_char_for(percent_encoded_string, idx));
38✔
124
            idx += 3;
38✔
125
        }
38✔
126
        else {
68✔
127
            // No need to decode. +1.
34✔
128
            if (!character_is_unreserved(current)) {
68✔
129
                throw std::invalid_argument("Input string is invalid: contains reserved characters.");
×
130
            }
×
131
            buffer.push_back(current);
68✔
132
            idx++;
68✔
133
        }
68✔
134
    }
106✔
135
    return buffer;
4✔
136
}
4✔
137

138
std::string file_path_by_appending_component(const std::string& path, const std::string& component,
139
                                             FilePathType path_type)
140
{
22,573✔
141
#ifdef _WIN32
142
    const char separator = '\\';
143
#else
144
    const char separator = '/';
22,573✔
145
#endif
22,573✔
146
    std::string buffer;
22,573✔
147
    buffer.reserve(2 + path.length() + component.length());
22,573✔
148
    buffer.append(path);
22,573✔
149
    std::string terminal = "";
22,573✔
150
    if (path_type == FilePathType::Directory && component[component.length() - 1] != separator) {
22,573✔
151
        terminal = separator;
17,534✔
152
    }
17,534✔
153
    char path_last = path[path.length() - 1];
22,573✔
154
    char component_first = component[0];
22,573✔
155
    if (path_last == separator && component_first == separator) {
22,573✔
156
        buffer.append(component.substr(1));
×
157
        buffer.append(terminal);
×
158
    }
×
159
    else if (path_last == separator || component_first == separator) {
22,573✔
160
        buffer.append(component);
18,338✔
161
        buffer.append(terminal);
18,338✔
162
    }
18,338✔
163
    else {
4,235✔
164
        buffer.append(std::string(1, separator));
4,235✔
165
        buffer.append(component);
4,235✔
166
        buffer.append(terminal);
4,235✔
167
    }
4,235✔
168
    return buffer;
22,573✔
169
}
22,573✔
170

171
std::string file_path_by_appending_extension(const std::string& path, const std::string& extension)
172
{
6✔
173
    std::string buffer;
6✔
174
    buffer.reserve(1 + path.length() + extension.length());
6✔
175
    buffer.append(path);
6✔
176
    char path_last = path[path.length() - 1];
6✔
177
    char extension_first = extension[0];
6✔
178
    if (path_last == '.' && extension_first == '.') {
6✔
179
        buffer.append(extension.substr(1));
2✔
180
    }
2✔
181
    else if (path_last == '.' || extension_first == '.') {
4✔
182
        buffer.append(extension);
4✔
183
    }
4✔
184
    else {
×
185
        buffer.append(".");
×
186
        buffer.append(extension);
×
187
    }
×
188
    return buffer;
6✔
189
}
6✔
190

191
std::string create_timestamped_template(const std::string& prefix, int wildcard_count)
192
{
70✔
193
    constexpr int WILDCARD_MAX = 20;
70✔
194
    constexpr int WILDCARD_MIN = 6;
70✔
195
    wildcard_count = std::min(WILDCARD_MAX, std::max(WILDCARD_MIN, wildcard_count));
70✔
196
    std::time_t time = std::time(nullptr);
70✔
197
    std::stringstream stream;
70✔
198
    stream << prefix << "-" << util::format_local_time(time, "%Y%m%d-%H%M%S") << "-"
70✔
199
           << std::string(wildcard_count, 'X');
70✔
200
    return stream.str();
70✔
201
}
70✔
202

203
std::string reserve_unique_file_name(const std::string& path, const std::string& template_string)
204
{
70✔
205
    REALM_ASSERT_DEBUG(template_string.find("XXXXXX") != std::string::npos);
70✔
206
    std::string path_buffer = file_path_by_appending_component(path, template_string, FilePathType::File);
70✔
207
    int fd = mkstemp(&path_buffer[0]);
70✔
208
    if (fd < 0) {
70✔
209
        int err = errno;
×
210
        throw std::system_error(err, std::system_category());
×
211
    }
×
212
    // Remove the file so we can use the name for our own file.
35✔
213
#ifdef _WIN32
214
    _close(fd);
215
    _unlink(path_buffer.c_str());
216
#else
217
    close(fd);
70✔
218
    unlink(path_buffer.c_str());
70✔
219
#endif
70✔
220
    return path_buffer;
70✔
221
}
70✔
222

223
static std::string validate_and_clean_path(const std::string& path)
224
{
5,291✔
225
    REALM_ASSERT(path.length() > 0);
5,291✔
226
    std::string escaped_path = util::make_percent_encoded_string(path);
5,291✔
227
    if (filename_is_reserved(escaped_path))
5,291✔
228
        throw std::invalid_argument(
×
229
            util::format("A path can't have an identifier reserved by the filesystem: '%1'", escaped_path));
×
230
    return escaped_path;
5,291✔
231
}
5,291✔
232

233
} // namespace util
234

235
SyncFileManager::SyncFileManager(const std::string& base_path, const std::string& app_id)
236
    : m_base_path(util::file_path_by_appending_component(base_path, c_sync_directory, util::FilePathType::Directory))
237
    , m_app_path(util::file_path_by_appending_component(m_base_path, util::validate_and_clean_path(app_id),
238
                                                        util::FilePathType::Directory))
239
{
4,225✔
240
    util::try_make_dir(m_base_path);
4,225✔
241
    util::try_make_dir(m_app_path);
4,225✔
242
}
4,225✔
243

244
std::string SyncFileManager::get_special_directory(std::string directory_name) const
245
{
4,243✔
246
    auto dir_path = file_path_by_appending_component(m_app_path, directory_name, util::FilePathType::Directory);
4,243✔
247
    util::try_make_dir(dir_path);
4,243✔
248
    return dir_path;
4,243✔
249
}
4,243✔
250

251
std::string SyncFileManager::user_directory(const std::string& user_identity) const
252
{
322✔
253
    std::string user_path = get_user_directory_path(user_identity);
322✔
254
    util::try_make_dir(user_path);
322✔
255
    return user_path;
322✔
256
}
322✔
257

258
void SyncFileManager::remove_user_realms(const std::string& user_identity,
259
                                         const std::vector<std::string>& realm_paths) const
260
{
24✔
261
    for (auto& path : realm_paths) {
26✔
262
        remove_realm(path);
26✔
263
    }
26✔
264
    // The following is redundant except for apps built before file tracking.
12✔
265
    std::string user_path = get_user_directory_path(user_identity);
24✔
266
    util::try_remove_dir_recursive(user_path);
24✔
267
}
24✔
268

269
bool SyncFileManager::remove_realm(const std::string& absolute_path) const
270
{
64✔
271
    REALM_ASSERT(absolute_path.length() > 0);
64✔
272
    bool success = true;
64✔
273
    try {
64✔
274
        constexpr bool delete_lockfile = true;
64✔
275
        realm::DB::delete_files(absolute_path, &success, delete_lockfile);
64✔
276
    }
64✔
277
    catch (FileAccessError const&) {
33✔
278
        success = false;
2✔
279
    }
2✔
280
    return success;
64✔
281
}
64✔
282

283
bool SyncFileManager::copy_realm_file(const std::string& old_path, const std::string& new_path) const
284
{
22✔
285
    REALM_ASSERT(old_path.length() > 0);
22✔
286
    try {
22✔
287
        if (File::exists(new_path)) {
22✔
288
            return false;
×
289
        }
×
290
        File::copy(old_path, new_path);
22✔
291
    }
22✔
292
    catch (FileAccessError const&) {
11✔
293
        return false;
×
294
    }
×
295
    return true;
22✔
296
}
22✔
297

298
bool SyncFileManager::remove_realm(const std::string& user_identity,
299
                                   const std::vector<std::string>& legacy_user_identities,
300
                                   const std::string& raw_realm_path, const std::string& partition) const
301
{
4✔
302
    auto existing = get_existing_realm_file_path(user_identity, legacy_user_identities, raw_realm_path, partition);
4✔
303
    if (existing) {
4✔
304
        return remove_realm(*existing);
2✔
305
    }
2✔
306
    return false; // if there is nothing to remove this is considered to be not successful
2✔
307
}
2✔
308

309
bool SyncFileManager::try_file_exists(const std::string& path) noexcept
310
{
794✔
311
    try {
794✔
312
        // May throw; for example when the path is too long
397✔
313
        return util::File::exists(path);
794✔
314
    }
794✔
315
    catch (const std::exception&) {
8✔
316
        return false;
8✔
317
    }
8✔
318
}
794✔
319

320
static bool try_file_remove(const std::string& path) noexcept
321
{
152✔
322
    try {
152✔
323
        return util::File::try_remove(path);
152✔
324
    }
152✔
325
    catch (const std::exception&) {
4✔
326
        return false;
4✔
327
    }
4✔
328
}
152✔
329

330
util::Optional<std::string>
331
SyncFileManager::get_existing_realm_file_path(const std::string& user_identity,
332
                                              const std::vector<std::string>& legacy_user_identities,
333
                                              const std::string& realm_file_name, const std::string& partition) const
334
{
174✔
335
    std::string preferred_name_without_suffix = preferred_realm_path_without_suffix(user_identity, realm_file_name);
174✔
336
    if (try_file_exists(preferred_name_without_suffix)) {
174✔
NEW
337
        return preferred_name_without_suffix;
×
UNCOV
338
    }
×
339

87✔
340
    std::string preferred_name_with_suffix = preferred_name_without_suffix + c_realm_file_suffix;
174✔
341
    if (try_file_exists(preferred_name_with_suffix)) {
174✔
342
        return preferred_name_with_suffix;
4✔
343
    }
4✔
344

85✔
345
    // Shorten the Realm path to just `<rootDir>/<hashedAbsolutePath>.realm`
85✔
346
    std::string hashed_name = fallback_hashed_realm_file_path(preferred_name_without_suffix);
170✔
347
    std::string hashed_path = hashed_name + c_realm_file_suffix;
170✔
348
    if (try_file_exists(hashed_path)) {
170✔
349
        // detected that the hashed fallback has been used previously
3✔
350
        // it was created for a reason so keep using it
3✔
351
        return hashed_path;
6✔
352
    }
6✔
353

82✔
354
    // The legacy fallback paths are not applicable to flexible sync
82✔
355
    if (partition.empty()) {
164✔
356
        return util::none;
10✔
357
    }
10✔
358

77✔
359
    // We used to hash the string value of the partition. For compatibility, check that SHA256
77✔
360
    // hash file name exists, and if it does, continue to use it.
77✔
361
    if (!partition.empty()) {
154✔
362
        std::string hashed_partition_path = legacy_hashed_partition_path(user_identity, partition);
154✔
363
        if (try_file_exists(hashed_partition_path)) {
154✔
364
            return hashed_partition_path;
×
365
        }
×
366
    }
154✔
367

77✔
368
    for (auto& legacy_identity : legacy_user_identities) {
154✔
369
        // retain support for legacy paths
32✔
370
        std::string old_path = legacy_realm_file_path(legacy_identity, realm_file_name);
64✔
371
        if (try_file_exists(old_path)) {
64✔
372
            return old_path;
6✔
373
        }
6✔
374
        // retain support for legacy local identity paths
29✔
375
        std::string old_local_identity_path = legacy_local_identity_path(legacy_identity, partition);
58✔
376
        if (try_file_exists(old_local_identity_path)) {
58✔
377
            return old_local_identity_path;
8✔
378
        }
8✔
379
    }
58✔
380

77✔
381
    return util::none;
147✔
382
}
154✔
383

384
std::string SyncFileManager::realm_file_path(const std::string& user_identity,
385
                                             const std::vector<std::string>& legacy_user_identities,
386
                                             const std::string& realm_file_name, const std::string& partition) const
387
{
170✔
388
    auto existing_path =
170✔
389
        get_existing_realm_file_path(user_identity, legacy_user_identities, realm_file_name, partition);
170✔
390
    if (existing_path) {
170✔
391
        return *existing_path;
22✔
392
    }
22✔
393

74✔
394
    // since this appears to be a new file, test the normal location
74✔
395
    // we use a test file with the same name and a suffix of the
74✔
396
    // same length, so we can catch "filename too long" errors on windows
74✔
397
    std::string preferred_name_without_suffix = preferred_realm_path_without_suffix(user_identity, realm_file_name);
148✔
398
    std::string preferred_name_with_suffix = preferred_name_without_suffix + c_realm_file_suffix;
148✔
399
    try {
148✔
400
        std::string test_path = preferred_name_without_suffix + c_realm_file_test_suffix;
148✔
401
        auto defer = util::make_scope_exit([test_path]() noexcept {
148✔
402
            try_file_remove(test_path);
148✔
403
        });
148✔
404
        util::File f(test_path, util::File::Mode::mode_Write);
148✔
405
        // if the test file succeeds, delete it and return the preferred location
74✔
406
    }
148✔
407
    catch (const FileAccessError&) {
76✔
408
        // the preferred test failed, test the hashed path
2✔
409
        std::string hashed_name = fallback_hashed_realm_file_path(preferred_name_without_suffix);
4✔
410
        std::string hashed_path = hashed_name + c_realm_file_suffix;
4✔
411
        try {
4✔
412
            std::string test_hashed_path = hashed_name + c_realm_file_test_suffix;
4✔
413
            auto defer = util::make_scope_exit([test_hashed_path]() noexcept {
4✔
414
                try_file_remove(test_hashed_path);
4✔
415
            });
4✔
416
            util::File f(test_hashed_path, util::File::Mode::mode_Write);
4✔
417
            // at this point the create succeeded, clean up the test file and return the hashed path
2✔
418
            return hashed_path;
4✔
419
        }
4✔
420
        catch (const FileAccessError& e_hashed) {
×
421
            // hashed test path also failed, give up and report error to user.
422
            throw std::logic_error(util::format("A valid realm path cannot be created for the "
×
423
                                                "Realm identity '%1' at neither '%2' nor '%3'. %4",
×
424
                                                realm_file_name, preferred_name_with_suffix, hashed_path,
×
425
                                                e_hashed.what()));
×
426
        }
×
427
    }
144✔
428

72✔
429
    return preferred_name_with_suffix;
144✔
430
}
144✔
431

432
std::string SyncFileManager::metadata_path() const
433
{
4,149✔
434
    auto dir_path = file_path_by_appending_component(get_utility_directory(), c_metadata_directory,
4,149✔
435
                                                     util::FilePathType::Directory);
4,149✔
436
    util::try_make_dir(dir_path);
4,149✔
437
    return util::file_path_by_appending_component(dir_path, c_metadata_realm);
4,149✔
438
}
4,149✔
439

440
bool SyncFileManager::remove_metadata_realm() const
441
{
2✔
442
    auto dir_path = file_path_by_appending_component(get_utility_directory(), c_metadata_directory,
2✔
443
                                                     util::FilePathType::Directory);
2✔
444
    try {
2✔
445
        util::try_remove_dir_recursive(dir_path);
2✔
446
        return true;
2✔
447
    }
2✔
448
    catch (FileAccessError const&) {
×
449
        return false;
×
450
    }
×
451
}
2✔
452

453
std::string SyncFileManager::preferred_realm_path_without_suffix(const std::string& user_identity,
454
                                                                 const std::string& realm_file_name) const
455
{
322✔
456
    auto escaped_file_name = util::validate_and_clean_path(realm_file_name);
322✔
457
    std::string preferred_name =
322✔
458
        util::file_path_by_appending_component(user_directory(user_identity), escaped_file_name);
322✔
459
    if (StringData(preferred_name).ends_with(c_realm_file_suffix)) {
322✔
460
        preferred_name = preferred_name.substr(0, preferred_name.size() - strlen(c_realm_file_suffix));
10✔
461
    }
10✔
462
    return preferred_name;
322✔
463
}
322✔
464

465
std::string SyncFileManager::fallback_hashed_realm_file_path(const std::string& preferred_path) const
466
{
174✔
467
    std::array<unsigned char, 32> hash;
174✔
468
    util::sha256(preferred_path.data(), preferred_path.size(), hash.data());
174✔
469
    std::string hashed_name =
174✔
470
        util::file_path_by_appending_component(m_app_path, util::hex_dump(hash.data(), hash.size(), ""));
174✔
471
    return hashed_name;
174✔
472
}
174✔
473

474
std::string SyncFileManager::legacy_hashed_partition_path(const std::string& user_identity,
475
                                                          const std::string& partition) const
476
{
154✔
477
    std::array<unsigned char, 32> hash;
154✔
478
    util::sha256(partition.data(), partition.size(), hash.data());
154✔
479
    std::string legacy_hashed_file_name = util::hex_dump(hash.data(), hash.size(), "");
154✔
480
    std::string legacy_partition_path = util::file_path_by_appending_component(
154✔
481
        get_user_directory_path(user_identity), legacy_hashed_file_name + c_realm_file_suffix);
154✔
482
    return legacy_partition_path;
154✔
483
}
154✔
484

485
std::string SyncFileManager::legacy_realm_file_path(const std::string& local_user_identity,
486
                                                    const std::string& realm_file_name) const
487
{
64✔
488
    auto path =
64✔
489
        util::file_path_by_appending_component(m_app_path, c_legacy_sync_directory, util::FilePathType::Directory);
64✔
490
    path = util::file_path_by_appending_component(path, util::validate_and_clean_path(local_user_identity),
64✔
491
                                                  util::FilePathType::Directory);
64✔
492
    path = util::file_path_by_appending_component(path, util::validate_and_clean_path(realm_file_name));
64✔
493
    return path;
64✔
494
}
64✔
495

496
std::string SyncFileManager::legacy_local_identity_path(const std::string& local_user_identity,
497
                                                        const std::string& realm_file_name) const
498
{
58✔
499
    auto escaped_file_name = util::validate_and_clean_path(realm_file_name);
58✔
500
    std::string user_path = get_user_directory_path(local_user_identity);
58✔
501
    std::string path_name = util::file_path_by_appending_component(user_path, escaped_file_name);
58✔
502
    std::string path = path_name + c_realm_file_suffix;
58✔
503

29✔
504
    return path;
58✔
505
}
58✔
506

507
std::string SyncFileManager::get_user_directory_path(const std::string& user_identity) const
508
{
558✔
509
    return file_path_by_appending_component(m_app_path, util::validate_and_clean_path(user_identity),
558✔
510
                                            util::FilePathType::Directory);
558✔
511
}
558✔
512

513
} // namespace realm
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc