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

realm / realm-core / 2040

14 Feb 2024 09:17PM UTC coverage: 91.863% (-0.02%) from 91.881%
2040

push

Evergreen

web-flow
Merge pull request #7341 from realm/tg/file-copy-race

File a minor race condition in the backup realm file action

93078 of 171514 branches covered (54.27%)

50 of 52 new or added lines in 6 files covered. (96.15%)

121 existing lines in 10 files now uncovered.

235427 of 256280 relevant lines covered (91.86%)

6415204.04 hits per line

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

87.97
/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 LogicError(ErrorCodes::InvalidArgument, "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,639✔
70
    return (filename == "." || filename == "..");
5,639✔
71
}
5,639✔
72

73
bool character_is_unreserved(char character)
74
{
56,645✔
75
    bool is_capital_letter = (character >= 'A' && character <= 'Z');
56,645✔
76
    bool is_lowercase_letter = (character >= 'a' && character <= 'z');
56,645✔
77
    bool is_number = (character >= '0' && character <= '9');
56,645✔
78
    bool is_allowed_symbol = (character == '-' || character == '_' || character == '.');
56,645✔
79
    return is_capital_letter || is_lowercase_letter || is_number || is_allowed_symbol;
56,645✔
80
}
56,645✔
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 LogicError(ErrorCodes::InvalidArgument,
×
86
                         "Malformed string: not enough characters after '%' before end of string.");
×
87
    }
×
88
    REALM_ASSERT(percent_encoding[index] == '%');
38✔
89
    return (16 * value_of_hex_digit(percent_encoding[index + 1])) + value_of_hex_digit(percent_encoding[index + 2]);
38✔
90
}
38✔
91

92
} // namespace
93

94
namespace util {
95

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

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

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

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

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

205
std::string reserve_unique_file_name(const std::string& path, const std::string& template_string)
206
{
80✔
207
    REALM_ASSERT_DEBUG(template_string.find("XXXXXX") != std::string::npos);
80✔
208
    std::string path_buffer = file_path_by_appending_component(path, template_string, FilePathType::File);
80✔
209
    int fd = mkstemp(&path_buffer[0]);
80✔
210
    if (fd < 0) {
80✔
211
        int err = errno;
×
212
        throw RuntimeError(ErrorCodes::FileOperationFailed,
×
213
                           util::format("Failed to make temporary path: %1 (%2)",
×
214
                                        std::system_error(err, std::system_category()).what(), err));
×
215
    }
×
216
    // Remove the file so we can use the name for our own file.
40✔
217
#ifdef _WIN32
218
    _close(fd);
219
    _unlink(path_buffer.c_str());
220
#else
221
    close(fd);
80✔
222
    unlink(path_buffer.c_str());
80✔
223
#endif
80✔
224
    return path_buffer;
80✔
225
}
80✔
226

227
static std::string validate_and_clean_path(const std::string& path)
228
{
5,639✔
229
    REALM_ASSERT(path.length() > 0);
5,639✔
230
    std::string escaped_path = util::make_percent_encoded_string(path);
5,639✔
231
    if (filename_is_reserved(escaped_path))
5,639✔
232
        throw LogicError(
×
233
            ErrorCodes::InvalidArgument,
×
234
            util::format("A path can't have an identifier reserved by the filesystem: '%1'", escaped_path));
×
235
    return escaped_path;
5,639✔
236
}
5,639✔
237

238
} // namespace util
239

240
SyncFileManager::SyncFileManager(const std::string& base_path, const std::string& app_id)
241
    : m_base_path(util::file_path_by_appending_component(base_path, c_sync_directory, util::FilePathType::Directory))
242
    , m_app_path(util::file_path_by_appending_component(m_base_path, util::validate_and_clean_path(app_id),
243
                                                        util::FilePathType::Directory))
244
{
4,573✔
245
    util::try_make_dir(m_base_path);
4,573✔
246
    util::try_make_dir(m_app_path);
4,573✔
247
}
4,573✔
248

249
std::string SyncFileManager::get_special_directory(std::string directory_name) const
250
{
817✔
251
    auto dir_path = file_path_by_appending_component(m_app_path, directory_name, util::FilePathType::Directory);
817✔
252
    util::try_make_dir(dir_path);
817✔
253
    return dir_path;
817✔
254
}
817✔
255

256
std::string SyncFileManager::user_directory(const std::string& user_identity) const
257
{
322✔
258
    std::string user_path = get_user_directory_path(user_identity);
322✔
259
    util::try_make_dir(user_path);
322✔
260
    return user_path;
322✔
261
}
322✔
262

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

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

288
bool SyncFileManager::copy_realm_file(const std::string& old_path, const std::string& new_path) const
289
{
22✔
290
    REALM_ASSERT(old_path.length() > 0);
22✔
291
    try {
22✔
292
        const bool overwrite_existing = false;
22✔
293
        return File::copy(old_path, new_path, overwrite_existing);
22✔
294
    }
22✔
UNCOV
295
    catch (FileAccessError const&) {
×
296
        return false;
×
297
    }
×
UNCOV
298
    return true;
×
UNCOV
299
}
×
300

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

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

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

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

87✔
343
    std::string preferred_name_with_suffix = preferred_name_without_suffix + c_realm_file_suffix;
174✔
344
    if (try_file_exists(preferred_name_with_suffix)) {
174✔
345
        return preferred_name_with_suffix;
4✔
346
    }
4✔
347

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

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

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

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

77✔
384
    return util::none;
147✔
385
}
154✔
386

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

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

72✔
432
    return preferred_name_with_suffix;
144✔
433
}
144✔
434

435
std::string SyncFileManager::metadata_path() const
436
{
713✔
437
    auto dir_path = file_path_by_appending_component(get_utility_directory(), c_metadata_directory,
713✔
438
                                                     util::FilePathType::Directory);
713✔
439
    util::try_make_dir(dir_path);
713✔
440
    return util::file_path_by_appending_component(dir_path, c_metadata_realm);
713✔
441
}
713✔
442

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

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

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

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

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

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

29✔
507
    return path;
58✔
508
}
58✔
509

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

516
} // 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

© 2026 Coveralls, Inc