File: native.cpp

package info (click to toggle)
freefilesync 13.7-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 9,044 kB
  • sloc: cpp: 66,712; ansic: 447; makefile: 216
file content (755 lines) | stat: -rw-r--r-- 34,195 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under    *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0          *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************

#include "native.h"
#include <zen/file_access.h>
#include <zen/symlink_target.h>
#include <zen/file_io.h>
#include <zen/stl_tools.h>
#include <zen/resolve_path.h>
#include <zen/recycler.h>
#include <zen/thread.h>
#include <zen/guid.h>
#include <zen/crc.h>
#include "abstract_impl.h"
#include "../base/icon_loader.h"

    #include <sys/vfs.h> //statfs

    #include <sys/stat.h>
    #include <dirent.h>
    #include <fcntl.h> //fallocate, fcntl

using namespace zen;
using namespace fff;
using AFS = AbstractFileSystem;


namespace
{

void initComForThread() //throw FileError
{
}

//====================================================================================================
//====================================================================================================

//persistent + unique (relative to volume) or 0!
inline
AFS::FingerPrint getFileFingerprint(FileIndex fileIndex)
{
    static_assert(sizeof(fileIndex) == sizeof(AFS::FingerPrint));
    return fileIndex; //== 0 if not supported
    /*  File details
        ------------
            st_mtim      (Linux)
            st_mtimespec (macOS): nanosecond-precision for improved uniqueness?
                          => essentially unknown after file copy (e.g. to FAT) without extra directory traversal :(

            macOS st_birthtimespec: "if not supported by file system, holds the ctime instead"
                                    ctime: inode modification time => changed on* rename*! => FU...

        Volume details
        --------------
            st_dev: "st_dev value is not necessarily consistent across reboots or system crashes" https://freefilesync.org/forum/viewtopic.php?t=8054
                    only locally unique and depends on device mount point! => FU...

            f_fsid: "Some operating systems use the device number..." => fuck!
                    "Several OSes restrict giving out the f_fsid field to the superuser only"

            f_bsize  macOS: "fundamental file system block size"
                     Linux: "optimal transfer block size" -> no! for all intents and purposes this *is* the "fundamental file system block size": https://stackoverflow.com/a/54835515
            f_blocks => meh...

            f_type Linux: documented values, nice! https://linux.die.net/man/2/statfs
                    macOS: - not stable between macOS releases: https://developer.apple.com/forums/thread/87745
                            - Apple docs say: "generally not a useful value"
                            - f_fstypename can be used as alternative

            DADiskGetBSDName(): macOS only           */
}


struct NativeFileInfo
{
    FileTimeNative modTime;
    uint64_t fileSize;
    AFS::FingerPrint filePrint;
};
NativeFileInfo getNativeFileInfo(FileBase& file) //throw FileError
{
    const struct stat& fileInfo = file.getStatBuffered(); //throw FileError
    return
    {
        fileInfo.st_mtim,
        makeUnsigned(fileInfo.st_size),
        getFileFingerprint(fileInfo.st_ino)
    };
}


struct FsItem
{
    Zstring itemName;
};
std::vector<FsItem> getDirContentFlat(const Zstring& dirPath) //throw FileError
{
    //no need to check for endless recursion:
    //1. Linux has a fixed limit on the number of symbolic links in a path
    //2. fails with "too many open files" or "path too long" before reaching stack overflow

    DIR* folder = ::opendir(dirPath.c_str()); //directory must NOT end with path separator, except "/"
    if (!folder)
        THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot open directory %x."), L"%x", fmtPath(dirPath)), "opendir");
    ZEN_ON_SCOPE_EXIT(::closedir(folder)); //never close nullptr handles! -> crash

    std::vector<FsItem> output;
    for (;;)
    {
        /* Linux: https://man7.org/linux/man-pages/man3/readdir_r.3.html
                "It is recommended that applications use readdir(3) instead of readdir_r"
                "... in modern implementations (including the glibc implementation), concurrent calls to readdir(3) that specify different directory streams are thread-safe"

           macOS: - libc: readdir thread-safe already in code from 2000: https://opensource.apple.com/source/Libc/Libc-166/gen.subproj/readdir.c.auto.html
                  - and in the latest version from 2017:                 https://opensource.apple.com/source/Libc/Libc-1244.30.3/gen/FreeBSD/readdir.c.auto.html                   */
        errno = 0;
        const dirent* dirEntry = ::readdir(folder);
        if (!dirEntry)
        {
            if (errno == 0) //errno left unchanged => no more items
                return output;

            THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(dirPath)), "readdir");
            //don't retry but restart dir traversal on error! https://devblogs.microsoft.com/oldnewthing/20140612-00/?p=753
        }

        const char* itemNameRaw = dirEntry->d_name;

        //skip "." and ".."
        if (itemNameRaw[0] == '.' &&
            (itemNameRaw[1] == 0 || (itemNameRaw[1] == '.' && itemNameRaw[2] == 0)))
            continue;

        if (itemNameRaw[0] == 0) //show error instead of endless recursion!!!
            throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(dirPath)), formatSystemError("readdir", L"", L"Folder contains an item without name."));

        output.push_back({itemNameRaw});

        /* Unicode normalization is file-system-dependent:

               OS                 Accepts   Gives back
               ----------         -------   ----------
               macOS (HFS+)         all        NFD
               Linux                all      <input>
               Windows (NTFS, FAT)  all      <input>

            some file systems return precomposed others decomposed UTF8: https://developer.apple.com/library/archive/qa/qa1173/_index.html
                  - OS X edit controls and text fields may return precomposed UTF as directly received by keyboard or decomposed UTF that was copy & pasted!
                  - Posix APIs require decomposed form: https://freefilesync.org/forum/viewtopic.php?t=2480

            => General recommendation: always preserve input UNCHANGED (both unicode normalization and case sensitivity)
            => normalize only when needed during string comparison

            Create sample files on Linux: touch  decomposed-$'\x6f\xcc\x81'.txt
                                          touch precomposed-$'\xc3\xb3'.txt

            - list file name hex chars in terminal:  ls | od -c -t x1

            - SMB sharing case-sensitive or NFD file names is fundamentally broken on macOS:
                => the macOS SMB manager internally buffers file names as case-insensitive and NFC (= just like NTFS on Windows)
                => test: create SMB share from Linux => *boom* on macOS: "Error Code 2: No such file or directory [lstat]"
                    or WORSE: folders "test" and "Test" *both* incorrectly return the content of one of the two
                => Update 2020-04-24: converting to NFC doesn't help: both NFD/NFC forms fail(ENOENT) lstat in FFS, AS WELL AS IN FINDER => macOS bug!         */
    }
}


struct FsItemDetails
{
    ItemType type;
    time_t   modTime; //number of seconds since Jan. 1st 1970 GMT
    uint64_t fileSize; //unit: bytes!
    AFS::FingerPrint filePrint;
};
FsItemDetails getItemDetails(const Zstring& itemPath) //throw FileError
{
    struct stat itemInfo = {};
    if (::lstat(itemPath.c_str(), &itemInfo) != 0) //lstat() does not resolve symlinks
        THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), "lstat");

    return {S_ISLNK(itemInfo.st_mode) ? ItemType::symlink : //on Linux there is no distinction between file and directory symlinks!
            /**/ (S_ISDIR(itemInfo.st_mode) ? ItemType::folder : ItemType::file), //a file or named pipe, etc. S_ISREG, S_ISCHR, S_ISBLK, S_ISFIFO, S_ISSOCK
            //=> dont't check using S_ISREG(): see comment in file_traverser.cpp
            itemInfo.st_mtime,
            makeUnsigned(itemInfo.st_size),
            getFileFingerprint(itemInfo.st_ino)};
}


FsItemDetails getSymlinkTargetDetails(const Zstring& linkPath) //throw FileError
{
    try
    {
        struct stat itemInfo = {};
        if (::stat(linkPath.c_str(), &itemInfo) != 0)
            THROW_LAST_SYS_ERROR("stat");

        const ItemType targetType = S_ISDIR(itemInfo.st_mode) ? ItemType::folder : ItemType::file;

        const AFS::FingerPrint filePrint = targetType == ItemType::folder ? 0 : getFileFingerprint(itemInfo.st_ino);
        return {targetType,
                itemInfo.st_mtime,
                makeUnsigned(itemInfo.st_size),
                filePrint};
    }
    catch (const SysError& e)
    {
        throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), e.toString());
    }
}


class SingleFolderTraverser
{
public:
    SingleFolderTraverser(const std::vector<std::pair<Zstring, std::shared_ptr<AFS::TraverserCallback>>>& workload /*throw X*/)
    {
        for (const auto& [folderPath, cb] : workload)
            workload_.push_back({folderPath, cb});

        while (!workload_.empty())
        {
            WorkItem wi = std::move(workload_.    back()); //yes, no strong exception guarantee (std::bad_alloc)
            /**/                    workload_.pop_back();  //

            tryReportingDirError([&] //throw X
            {
                traverseWithException(wi.dirPath, *wi.cb); //throw FileError, X
            }, *wi.cb);
        }
    }

private:
    SingleFolderTraverser           (const SingleFolderTraverser&) = delete;
    SingleFolderTraverser& operator=(const SingleFolderTraverser&) = delete;

    void traverseWithException(const Zstring& dirPath, AFS::TraverserCallback& cb) //throw FileError, X
    {
        for (const auto& [itemName] : getDirContentFlat(dirPath)) //throw FileError
        {
            const Zstring itemPath = appendPath(dirPath, itemName);

            FsItemDetails itemDetails = {};
            if (!tryReportingItemError([&] //throw X
        {
            itemDetails = getItemDetails(itemPath); //throw FileError
            }, cb, itemName))
            continue; //ignore error: skip file

            switch (itemDetails.type)
            {
                case ItemType::file:
                    cb.onFile({itemName, itemDetails.fileSize, itemDetails.modTime, itemDetails.filePrint, false /*isFollowedSymlink*/}); //throw X
                    break;

                case ItemType::folder:
                    if (std::shared_ptr<AFS::TraverserCallback> cbSub = cb.onFolder({itemName, false /*isFollowedSymlink*/})) //throw X
                        workload_.push_back({itemPath, std::move(cbSub)});
                    break;

                case ItemType::symlink:
                    switch (cb.onSymlink({itemName, itemDetails.modTime})) //throw X
                    {
                        case AFS::TraverserCallback::HandleLink::follow:
                        {
                            FsItemDetails targetDetails = {};
                            if (!tryReportingItemError([&] //throw X
                        {
                            targetDetails = getSymlinkTargetDetails(itemPath); //throw FileError
                            }, cb, itemName))
                            continue;

                            if (targetDetails.type == ItemType::folder)
                            {
                                if (std::shared_ptr<AFS::TraverserCallback> cbSub = cb.onFolder({itemName, true /*isFollowedSymlink*/})) //throw X
                                    workload_.push_back({itemPath, std::move(cbSub)}); //symlink may link to different volume!
                            }
                            else //a file or named pipe, etc.
                                cb.onFile({itemName, targetDetails.fileSize, targetDetails.modTime, targetDetails.filePrint, true /*isFollowedSymlink*/}); //throw X
                        }
                        break;

                        case AFS::TraverserCallback::HandleLink::skip:
                            break;
                    }
                    break;
            }
        }
    }

    struct WorkItem
    {
        Zstring dirPath;
        std::shared_ptr<AFS::TraverserCallback> cb;
    };
    std::vector<WorkItem> workload_;
};


void traverseFolderRecursiveNative(const std::vector<std::pair<Zstring, std::shared_ptr<AFS::TraverserCallback>>>& workload /*throw X*/, size_t) //throw X
{
    SingleFolderTraverser dummy(workload); //throw X
}
//====================================================================================================
//====================================================================================================

class RecycleSessionNative : public AFS::RecycleSession
{
public:
    explicit RecycleSessionNative(const Zstring& baseFolderPath) : baseFolderPath_(baseFolderPath) {}
    //constructor will be running on main thread => keep trivial and defer work to getRecyclerTempPath()!

    void moveToRecycleBin(const AbstractPath& itemPath, const Zstring& logicalRelPath) override; //throw FileError, RecycleBinUnavailable
    void tryCleanup(const std::function<void(const std::wstring& displayPath)>& notifyDeletionStatus /*throw X*/) override; //throw FileError, X

private:
    const Zstring baseFolderPath_;
};

//===========================================================================================================================

struct InputStreamNative : public AFS::InputStream
{
    explicit InputStreamNative(const Zstring& filePath) : fileIn_(filePath) {} //throw FileError, ErrorFileLocked

    size_t getBlockSize() override { return fileIn_.getBlockSize(); } //throw FileError; non-zero block size is AFS contract!

    //may return short; only 0 means EOF! CONTRACT: bytesToRead > 0!
    size_t tryRead(void* buffer, size_t bytesToRead, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, ErrorFileLocked, X
    {
        const size_t bytesRead = fileIn_.tryRead(buffer, bytesToRead); //throw FileError, ErrorFileLocked
        if (notifyUnbufferedIO) notifyUnbufferedIO(bytesRead); //throw X
        return bytesRead;
    }

    std::optional<AFS::StreamAttributes> tryGetAttributesFast() override //throw FileError
    {
        const NativeFileInfo& fileInfo = getNativeFileInfo(fileIn_); //throw FileError

        return AFS::StreamAttributes({nativeFileTimeToTimeT(fileInfo.modTime),
                                      fileInfo.fileSize,
                                      fileInfo.filePrint});
    }

private:
    FileInputPlain fileIn_;
};

//===========================================================================================================================

struct OutputStreamNative : public AFS::OutputStreamImpl
{
    OutputStreamNative(const Zstring& filePath,
                       std::optional<uint64_t> streamSize,
                       std::optional<time_t> modTime) :
        fileOut_(filePath), //throw FileError, ErrorTargetExisting
        modTime_(modTime)
    {
        if (streamSize) //preallocate disk space + reduce fragmentation
            fileOut_.reserveSpace(*streamSize); //throw FileError
    }

    size_t getBlockSize() override { return fileOut_.getBlockSize(); } //throw FileError

    size_t tryWrite(const void* buffer, size_t bytesToWrite, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X; may return short! CONTRACT: bytesToWrite > 0
    {
        const size_t bytesWritten = fileOut_.tryWrite(buffer, bytesToWrite); //throw FileError
        if (notifyUnbufferedIO) notifyUnbufferedIO(bytesWritten); //throw X
        return bytesWritten;
    }

    AFS::FinalizeResult finalize(const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X
    {
        AFS::FinalizeResult result;

        result.filePrint = getNativeFileInfo(fileOut_).filePrint; //throw FileError

        fileOut_.close(); //throw FileError
        /* is setting modtime after closing the file handle a pessimization?
           no, needed for functional correctness, see file_access.cpp::copyNewFile() for macOS/Linux
           even required on Windows: https://freefilesync.org/forum/viewtopic.php?t=10781 */
        try
        {
            if (modTime_)
                setFileTime(fileOut_.getFilePath(), *modTime_, ProcSymlink::follow); //throw FileError
        }
        catch (const FileError& e) { result.errorModTime = e; /*might slice derived class?*/ }

        return result;
    }

private:
    FileOutputPlain fileOut_;
    const std::optional<time_t> modTime_;
};

//===========================================================================================================================

class NativeFileSystem : public AbstractFileSystem
{
public:
    explicit NativeFileSystem(const Zstring& rootPath) : rootPath_(rootPath) {}

    Zstring getNativePath(const AfsPath& itemPath) const { return isNullFileSystem() ? Zstring{} : appendPath(rootPath_, itemPath.value); }

private:
    Zstring getInitPathPhrase(const AfsPath& itemPath) const override { return makePathPhrase(getNativePath(itemPath)); }

    std::vector<Zstring> getPathPhraseAliases(const AfsPath& itemPath) const override
    {
        if (isNullFileSystem())
            return {};

        return ::getPathPhraseAliases(getNativePath(itemPath));
    }

    std::wstring getDisplayPath(const AfsPath& itemPath) const override { return utfTo<std::wstring>(getNativePath(itemPath)); }

    bool isNullFileSystem() const override { return rootPath_.empty(); }

    std::weak_ordering compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override
    {
        return compareNativePath(rootPath_, static_cast<const NativeFileSystem&>(afsRhs).rootPath_);
    }

    //----------------------------------------------------------------------------------------------------------------
    static ItemType zenToAfsItemType(zen::ItemType type)
    {
        switch (type)
        {
            case zen::ItemType::file:
                return AFS::ItemType::file;
            case zen::ItemType::folder:
                return AFS::ItemType::folder;
            case zen::ItemType::symlink:
                return AFS::ItemType::symlink;
        }
        assert(false);
        return static_cast<AFS::ItemType>(type);
    }

    ItemType getItemType(const AfsPath& itemPath) const override //throw FileError
    {
        initComForThread(); //throw FileError
        return zenToAfsItemType(zen::getItemType(getNativePath(itemPath))); //throw FileError
    }

    std::optional<ItemType> getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError
    {
        initComForThread(); //throw FileError
        if (const std::optional<zen::ItemType> type = zen::getItemTypeIfExists(getNativePath(itemPath))) //throw FileError
            return zenToAfsItemType(*type);
        return std::nullopt;
    }

    //----------------------------------------------------------------------------------------------------------------
    //already existing: fail
    void createFolderPlain(const AfsPath& folderPath) const override //throw FileError
    {
        initComForThread(); //throw FileError
        createDirectory(getNativePath(folderPath)); //throw FileError, ErrorTargetExisting
    }

    void removeFilePlain(const AfsPath& filePath) const override //throw FileError
    {
        initComForThread(); //throw FileError
        zen::removeFilePlain(getNativePath(filePath)); //throw FileError
    }

    void removeSymlinkPlain(const AfsPath& linkPath) const override //throw FileError
    {
        initComForThread(); //throw FileError
        zen::removeSymlinkPlain(getNativePath(linkPath)); //throw FileError
    }

    void removeFolderPlain(const AfsPath& folderPath) const override //throw FileError
    {
        initComForThread(); //throw FileError
        zen::removeDirectoryPlain(getNativePath(folderPath)); //throw FileError
    }

    void removeFolderIfExistsRecursion(const AfsPath& folderPath, //throw FileError
                                       const std::function<void(const std::wstring& displayPath)>& onBeforeFileDeletion   /*throw X*/,
                                       const std::function<void(const std::wstring& displayPath)>& onBeforeSymlinkDeletion/*throw X*/,
                                       const std::function<void(const std::wstring& displayPath)>& onBeforeFolderDeletion /*throw X*/) const override
    {
        //default implementation: folder traversal
        AFS::removeFolderIfExistsRecursion(folderPath, onBeforeFileDeletion, onBeforeSymlinkDeletion, onBeforeFolderDeletion); //throw FileError, X
    }

    //----------------------------------------------------------------------------------------------------------------
    AbstractPath getSymlinkResolvedPath(const AfsPath& linkPath) const override //throw FileError
    {
        initComForThread(); //throw FileError
        const Zstring nativePath = getNativePath(linkPath);

        const Zstring resolvedPath = zen::getSymlinkResolvedPath(nativePath); //throw FileError
        const std::optional<zen::PathComponents> comp = parsePathComponents(resolvedPath);
        if (!comp)
            throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(nativePath)),
                            replaceCpy<std::wstring>(L"Invalid path %x.", L"%x", fmtPath(resolvedPath)));

        return AbstractPath(makeSharedRef<NativeFileSystem>(comp->rootPath), AfsPath(comp->relPath));
    }

    bool equalSymlinkContentForSameAfsType(const AfsPath& linkPathL, const AbstractPath& linkPathR) const override //throw FileError
    {
        initComForThread(); //throw FileError

        const NativeFileSystem& nativeFsR = static_cast<const NativeFileSystem&>(linkPathR.afsDevice.ref());

        const SymlinkRawContent linkContentL = getSymlinkRawContent(getNativePath(linkPathL)); //throw FileError
        const SymlinkRawContent linkContentR = getSymlinkRawContent(nativeFsR.getNativePath(linkPathR.afsPath)); //throw FileError

        if (linkContentL.targetPath != linkContentR.targetPath)
            return false;

        return true;
    }
    //----------------------------------------------------------------------------------------------------------------

    //return value always bound:
    std::unique_ptr<InputStream> getInputStream(const AfsPath& filePath) const override //throw FileError, ErrorFileLocked
    {
        initComForThread(); //throw FileError
        return std::make_unique<InputStreamNative>(getNativePath(filePath)); //throw FileError, ErrorFileLocked
    }

    //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename)
    //=> actual behavior: fail with clear error message
    std::unique_ptr<OutputStreamImpl> getOutputStream(const AfsPath& filePath, //throw FileError
                                                      std::optional<uint64_t> streamSize,
                                                      std::optional<time_t> modTime) const override
    {
        initComForThread(); //throw FileError
        return std::make_unique<OutputStreamNative>(getNativePath(filePath), streamSize, modTime); //throw FileError, ErrorTargetExisting
    }

    //----------------------------------------------------------------------------------------------------------------
    void traverseFolderRecursive(const TraverserWorkload& workload /*throw X*/, size_t parallelOps) const override
    {
        //initComForThread() -> done on traverser worker threads

        std::vector<std::pair<Zstring, std::shared_ptr<TraverserCallback>>> initialWorkItems;
        for (const auto& [folderPath, cb] : workload)
            initialWorkItems.emplace_back(getNativePath(folderPath), cb);

        traverseFolderRecursiveNative(initialWorkItems, parallelOps); //throw X
    }
    //----------------------------------------------------------------------------------------------------------------

    //symlink handling: follow
    //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename)
    //=> actual behavior: fail with clear error message
    FileCopyResult copyFileForSameAfsType(const AfsPath& sourcePath, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X
                                          const AbstractPath& targetPath, bool copyFilePermissions, const IoCallback& notifyUnbufferedIO /*throw X*/) const override
    {
        const Zstring nativePathTarget = static_cast<const NativeFileSystem&>(targetPath.afsDevice.ref()).getNativePath(targetPath.afsPath);

        initComForThread(); //throw FileError

        const zen::FileCopyResult nativeResult = copyNewFile(getNativePath(sourcePath), nativePathTarget, notifyUnbufferedIO); //throw FileError, ErrorTargetExisting, ErrorFileLocked, X

        //at this point we know we created a new file, so it's fine to delete it for cleanup!
        ZEN_ON_SCOPE_FAIL(try { zen::removeFilePlain(nativePathTarget); }
        catch (const FileError& e) { logExtraError(e.toString()); });

        if (copyFilePermissions)
            copyItemPermissions(getNativePath(sourcePath), nativePathTarget, ProcSymlink::follow); //throw FileError

        FileCopyResult result;
        result.fileSize = nativeResult.fileSize;
        //caveat: modTime will be incorrect for file systems with imprecise file times, e.g. see FAT_FILE_TIME_PRECISION_SEC
        result.modTime = nativeFileTimeToTimeT(nativeResult.sourceModTime);
        result.sourceFilePrint = getFileFingerprint(nativeResult.sourceFileIdx);
        result.targetFilePrint = getFileFingerprint(nativeResult.targetFileIdx);
        result.errorModTime = nativeResult.errorModTime;
        return result;
    }

    //symlink handling: follow
    //already existing: fail
    void copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError
    {
        initComForThread(); //throw FileError

        const Zstring& sourcePathNative = getNativePath(sourcePath);
        const Zstring& targetPathNative = static_cast<const NativeFileSystem&>(targetPath.afsDevice.ref()).getNativePath(targetPath.afsPath);

        zen::createDirectory(targetPathNative); //throw FileError, ErrorTargetExisting

        try
        {
            copyDirectoryAttributes(sourcePathNative, targetPathNative); //throw FileError
        }
        catch (FileError&) {} //[!] too unimportant + too frequent for external devices, e.g. "ERROR_INVALID_PARAMETER [SetFileInformationByHandle(FileBasicInfo)]" on Samba share

        if (copyFilePermissions)
            copyItemPermissions(sourcePathNative, targetPathNative, ProcSymlink::follow); //throw FileError
    }

    //already existing: fail
    void copySymlinkForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError
    {
        const Zstring targetPathNative = static_cast<const NativeFileSystem&>(targetPath.afsDevice.ref()).getNativePath(targetPath.afsPath);

        initComForThread(); //throw FileError
        zen::copySymlink(getNativePath(sourcePath), targetPathNative); //throw FileError

        ZEN_ON_SCOPE_FAIL(try { zen::removeSymlinkPlain(targetPathNative); }
        catch (const FileError& e) { logExtraError(e.toString()); });

        if (copyFilePermissions)
            copyItemPermissions(getNativePath(sourcePath), targetPathNative, ProcSymlink::asLink); //throw FileError
    }

    //already existing: undefined behavior! (e.g. fail/overwrite)
    //=> actual behavior: fail with clear error message
    void moveAndRenameItemForSameAfsType(const AfsPath& pathFrom, const AbstractPath& pathTo) const override //throw FileError, ErrorMoveUnsupported
    {
        //perf test: detecting different volumes by path is ~30 times faster than having ::MoveFileEx() fail with ERROR_NOT_SAME_DEVICE (6µs vs 190µs)
        //=> maybe we can even save some actual I/O in some cases?
        if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != std::weak_ordering::equivalent)
            throw ErrorMoveUnsupported(generateMoveErrorMsg(pathFrom, pathTo), _("Operation not supported between different devices."));

        initComForThread(); //throw FileError
        const Zstring nativePathTarget = static_cast<const NativeFileSystem&>(pathTo.afsDevice.ref()).getNativePath(pathTo.afsPath);

        zen::moveAndRenameItem(getNativePath(pathFrom), nativePathTarget, false /*replaceExisting*/); //throw FileError, ErrorTargetExisting, ErrorMoveUnsupported
        //may fail with ERROR_ALREADY_EXISTS despite previously existing file already deleted
        //=> reason: corrupted disk, fixable via Windows error checking! https://freefilesync.org/forum/viewtopic.php?t=9776
    }

    bool supportsPermissions(const AfsPath& folderPath) const override //throw FileError
    {
        initComForThread(); //throw FileError
        return zen::supportsPermissions(getNativePath(folderPath));
    }

    //----------------------------------------------------------------------------------------------------------------
    FileIconHolder getFileIcon(const AfsPath& filePath, int pixelSize) const override //throw FileError; (optional return value)
    {
        initComForThread(); //throw FileError
        try
        {
            return fff::getFileIcon(getNativePath(filePath), pixelSize); //throw SysError
        }
        catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getDisplayPath(filePath))), e.toString()); }
    }

    ImageHolder getThumbnailImage(const AfsPath& filePath, int pixelSize) const override //throw FileError; (optional return value)
    {
        initComForThread(); //throw FileError
        try
        {
            return fff::getThumbnailImage(getNativePath(filePath), pixelSize); //throw SysError
        }
        catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getDisplayPath(filePath))), e.toString()); }
    }

    void authenticateAccess(const RequestPasswordFun& requestPassword /*throw X*/) const override //throw FileError, (X)
    {
    }

    bool hasNativeTransactionalCopy() const override { return false; }
    //----------------------------------------------------------------------------------------------------------------

    int64_t getFreeDiskSpace(const AfsPath& folderPath) const override //throw FileError, returns < 0 if not available
    {
        initComForThread(); //throw FileError
        return zen::getFreeDiskSpace(getNativePath(folderPath)); //throw FileError
    }

    std::unique_ptr<RecycleSession> createRecyclerSession(const AfsPath& folderPath) const override //throw FileError, (RecycleBinUnavailable)
    {
        initComForThread(); //throw FileError
        return std::make_unique<RecycleSessionNative>(getNativePath(folderPath));
    }

    //fails if item is not existing
    void moveToRecycleBin(const AfsPath& itemPath) const override //throw FileError, RecycleBinUnavailable
    {
        initComForThread(); //throw FileError
        zen::moveToRecycleBin(getNativePath(itemPath)); //throw FileError, RecycleBinUnavailable
    }

    const Zstring rootPath_;
};

//===========================================================================================================================



//- fails if item is not existing
//- multi-threaded access: internally synchronized!
void RecycleSessionNative::moveToRecycleBin(const AbstractPath& itemPath, const Zstring& logicalRelPath) //throw FileError, RecycleBinUnavailable
{
    const Zstring& itemPathNative = getNativeItemPath(itemPath);
    if (itemPathNative.empty())
        throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");

    zen::moveToRecycleBin(itemPathNative); //throw FileError, RecycleBinUnavailable
}


void RecycleSessionNative::tryCleanup(const std::function<void(const std::wstring& displayPath)>& notifyDeletionStatus /*throw X*/) //throw FileError, X
{
}
}


//coordinate changes with getResolvedFilePath()!
bool fff::acceptsItemPathPhraseNative(const Zstring& itemPathPhrase) //noexcept
{
    Zstring path = expandMacros(itemPathPhrase); //expand before trimming!
    trim(path);

    if (path.empty()) //eat up empty paths before other AFS implementations get a chance!
        return true;


    if (startsWith(path, Zstr('['))) //drive letter by volume name syntax
        return true;

    //don't accept relative paths!!! indistinguishable from MTP paths as shown in Explorer's address bar!
    return static_cast<bool>(parsePathComponents(path));
}


AbstractPath fff::createItemPathNative(const Zstring& itemPathPhrase) //noexcept
{
    const Zstring& itemPath = getResolvedFilePath(itemPathPhrase);
    return createItemPathNativeNoFormatting(itemPath);
}


AbstractPath fff::createItemPathNativeNoFormatting(const Zstring& nativePath) //noexcept
{
    if (const std::optional<PathComponents> pc = parsePathComponents(nativePath))
        return AbstractPath(makeSharedRef<NativeFileSystem>(pc->rootPath), AfsPath(pc->relPath));

    assert(nativePath.empty()); //broken path syntax
    return AbstractPath(makeSharedRef<NativeFileSystem>(nativePath), AfsPath());
}


Zstring fff::getNativeItemPath(const AbstractPath& itemPath)
{
    if (const auto nativeDevice = dynamic_cast<const NativeFileSystem*>(&itemPath.afsDevice.ref()))
        return nativeDevice->getNativePath(itemPath.afsPath);
    return {};
}