1 /*
2  * Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17  */
18 
19 #include "filesystembase.h"
20 #include "utility.h"
21 #include "common/asserts.h"
22 
23 #include <QDateTime>
24 #include <QDir>
25 #include <QUrl>
26 #include <QFile>
27 #include <QCoreApplication>
28 
29 #include <sys/stat.h>
30 #include <sys/types.h>
31 
32 #ifdef Q_OS_WIN
33 #include <windows.h>
34 #include <windef.h>
35 #include <winbase.h>
36 #include <fcntl.h>
37 #include <io.h>
38 #endif
39 
40 namespace OCC {
41 
42 Q_LOGGING_CATEGORY(lcFileSystem, "nextcloud.sync.filesystem", QtInfoMsg)
43 
longWinPath(const QString & inpath)44 QString FileSystem::longWinPath(const QString &inpath)
45 {
46 #ifdef Q_OS_WIN
47     return pathtoUNC(inpath);
48 #else
49     return inpath;
50 #endif
51 }
52 
setFileHidden(const QString & filename,bool hidden)53 void FileSystem::setFileHidden(const QString &filename, bool hidden)
54 {
55 #ifdef _WIN32
56     QString fName = longWinPath(filename);
57     DWORD dwAttrs;
58 
59     dwAttrs = GetFileAttributesW((wchar_t *)fName.utf16());
60 
61     if (dwAttrs != INVALID_FILE_ATTRIBUTES) {
62         if (hidden && !(dwAttrs & FILE_ATTRIBUTE_HIDDEN)) {
63             SetFileAttributesW((wchar_t *)fName.utf16(), dwAttrs | FILE_ATTRIBUTE_HIDDEN);
64         } else if (!hidden && (dwAttrs & FILE_ATTRIBUTE_HIDDEN)) {
65             SetFileAttributesW((wchar_t *)fName.utf16(), dwAttrs & ~FILE_ATTRIBUTE_HIDDEN);
66         }
67     }
68 #else
69     Q_UNUSED(filename);
70     Q_UNUSED(hidden);
71 #endif
72 }
73 
getDefaultWritePermissions()74 static QFile::Permissions getDefaultWritePermissions()
75 {
76     QFile::Permissions result = QFile::WriteUser;
77 #ifndef Q_OS_WIN
78     mode_t mask = umask(0);
79     umask(mask);
80     if (!(mask & S_IWGRP)) {
81         result |= QFile::WriteGroup;
82     }
83     if (!(mask & S_IWOTH)) {
84         result |= QFile::WriteOther;
85     }
86 #endif
87     return result;
88 }
89 
setFileReadOnly(const QString & filename,bool readonly)90 void FileSystem::setFileReadOnly(const QString &filename, bool readonly)
91 {
92     QFile file(filename);
93     QFile::Permissions permissions = file.permissions();
94 
95     QFile::Permissions allWritePermissions =
96         QFile::WriteUser | QFile::WriteGroup | QFile::WriteOther | QFile::WriteOwner;
97     static QFile::Permissions defaultWritePermissions = getDefaultWritePermissions();
98 
99     permissions &= ~allWritePermissions;
100     if (!readonly) {
101         permissions |= defaultWritePermissions;
102     }
103     file.setPermissions(permissions);
104 }
105 
setFolderMinimumPermissions(const QString & filename)106 void FileSystem::setFolderMinimumPermissions(const QString &filename)
107 {
108 #ifdef Q_OS_MAC
109     QFile::Permissions perm = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner;
110     QFile file(filename);
111     file.setPermissions(perm);
112 #else
113     Q_UNUSED(filename);
114 #endif
115 }
116 
117 
setFileReadOnlyWeak(const QString & filename,bool readonly)118 void FileSystem::setFileReadOnlyWeak(const QString &filename, bool readonly)
119 {
120     QFile file(filename);
121     QFile::Permissions permissions = file.permissions();
122 
123     if (!readonly && (permissions & QFile::WriteOwner)) {
124         return; // already writable enough
125     }
126 
127     setFileReadOnly(filename, readonly);
128 }
129 
rename(const QString & originFileName,const QString & destinationFileName,QString * errorString)130 bool FileSystem::rename(const QString &originFileName,
131     const QString &destinationFileName,
132     QString *errorString)
133 {
134     bool success = false;
135     QString error;
136 #ifdef Q_OS_WIN
137     QString orig = longWinPath(originFileName);
138     QString dest = longWinPath(destinationFileName);
139 
140     if (isLnkFile(originFileName) || isLnkFile(destinationFileName)) {
141         success = MoveFileEx((wchar_t *)orig.utf16(),
142             (wchar_t *)dest.utf16(),
143             MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH);
144         if (!success) {
145             error = Utility::formatWinError(GetLastError());
146         }
147     } else
148 #endif
149     {
150         QFile orig(originFileName);
151         success = orig.rename(destinationFileName);
152         if (!success) {
153             error = orig.errorString();
154         }
155     }
156 
157     if (!success) {
158         qCWarning(lcFileSystem) << "Error renaming file" << originFileName
159                                 << "to" << destinationFileName
160                                 << "failed: " << error;
161         if (errorString) {
162             *errorString = error;
163         }
164     }
165     return success;
166 }
167 
uncheckedRenameReplace(const QString & originFileName,const QString & destinationFileName,QString * errorString)168 bool FileSystem::uncheckedRenameReplace(const QString &originFileName,
169     const QString &destinationFileName,
170     QString *errorString)
171 {
172 #ifndef Q_OS_WIN
173     bool success = false;
174     QFile orig(originFileName);
175     // We want a rename that also overwites.  QFile::rename does not overwite.
176     // Qt 5.1 has QSaveFile::renameOverwrite we could use.
177     // ### FIXME
178     success = true;
179     bool destExists = fileExists(destinationFileName);
180     if (destExists && !QFile::remove(destinationFileName)) {
181         *errorString = orig.errorString();
182         qCWarning(lcFileSystem) << "Target file could not be removed.";
183         success = false;
184     }
185     if (success) {
186         success = orig.rename(destinationFileName);
187     }
188     if (!success) {
189         *errorString = orig.errorString();
190         qCWarning(lcFileSystem) << "Renaming temp file to final failed: " << *errorString;
191         return false;
192     }
193 
194 #else //Q_OS_WIN
195     // You can not overwrite a read-only file on windows.
196     if (!QFileInfo(destinationFileName).isWritable()) {
197         setFileReadOnly(destinationFileName, false);
198     }
199 
200     BOOL ok;
201     QString orig = longWinPath(originFileName);
202     QString dest = longWinPath(destinationFileName);
203 
204     ok = MoveFileEx((wchar_t *)orig.utf16(),
205         (wchar_t *)dest.utf16(),
206         MOVEFILE_REPLACE_EXISTING + MOVEFILE_COPY_ALLOWED + MOVEFILE_WRITE_THROUGH);
207     if (!ok) {
208         *errorString = Utility::formatWinError(GetLastError());
209         qCWarning(lcFileSystem) << "Renaming temp file to final failed: " << *errorString;
210         return false;
211     }
212 #endif
213     return true;
214 }
215 
openAndSeekFileSharedRead(QFile * file,QString * errorOrNull,qint64 seek)216 bool FileSystem::openAndSeekFileSharedRead(QFile *file, QString *errorOrNull, qint64 seek)
217 {
218     QString errorDummy;
219     // avoid many if (errorOrNull) later.
220     QString &error = errorOrNull ? *errorOrNull : errorDummy;
221     error.clear();
222 
223 #ifdef Q_OS_WIN
224     //
225     // The following code is adapted from Qt's QFSFileEnginePrivate::nativeOpen()
226     // by including the FILE_SHARE_DELETE share mode.
227     //
228 
229     // Enable full sharing.
230     DWORD shareMode = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
231 
232     int accessRights = GENERIC_READ;
233     DWORD creationDisp = OPEN_EXISTING;
234 
235     // Create the file handle.
236     SECURITY_ATTRIBUTES securityAtts = { sizeof(SECURITY_ATTRIBUTES), nullptr, FALSE };
237     QString fName = longWinPath(file->fileName());
238 
239     HANDLE fileHandle = CreateFileW(
240         (const wchar_t *)fName.utf16(),
241         accessRights,
242         shareMode,
243         &securityAtts,
244         creationDisp,
245         FILE_ATTRIBUTE_NORMAL,
246         nullptr);
247 
248     // Bail out on error.
249     if (fileHandle == INVALID_HANDLE_VALUE) {
250         error = qt_error_string();
251         return false;
252     }
253 
254     // Convert the HANDLE to an fd and pass it to QFile's foreign-open
255     // function. The fd owns the handle, so when QFile later closes
256     // the fd the handle will be closed too.
257     int fd = _open_osfhandle((intptr_t)fileHandle, _O_RDONLY);
258     if (fd == -1) {
259         error = QStringLiteral("could not make fd from handle");
260         CloseHandle(fileHandle);
261         return false;
262     }
263     if (!file->open(fd, QIODevice::ReadOnly, QFile::AutoCloseHandle)) {
264         error = file->errorString();
265         _close(fd); // implicitly closes fileHandle
266         return false;
267     }
268 
269     // Seek to the right spot
270     LARGE_INTEGER *li = reinterpret_cast<LARGE_INTEGER *>(&seek);
271     DWORD newFilePointer = SetFilePointer(fileHandle, li->LowPart, &li->HighPart, FILE_BEGIN);
272     if (newFilePointer == 0xFFFFFFFF && GetLastError() != NO_ERROR) {
273         error = qt_error_string();
274         return false;
275     }
276 
277     return true;
278 #else
279     if (!file->open(QFile::ReadOnly)) {
280         error = file->errorString();
281         return false;
282     }
283     if (!file->seek(seek)) {
284         error = file->errorString();
285         return false;
286     }
287     return true;
288 #endif
289 }
290 
291 #ifdef Q_OS_WIN
fileExistsWin(const QString & filename)292 static bool fileExistsWin(const QString &filename)
293 {
294     WIN32_FIND_DATA FindFileData;
295     HANDLE hFind;
296     QString fName = FileSystem::longWinPath(filename);
297 
298     hFind = FindFirstFileW((wchar_t *)fName.utf16(), &FindFileData);
299     if (hFind == INVALID_HANDLE_VALUE) {
300         return false;
301     }
302     FindClose(hFind);
303     return true;
304 }
305 #endif
306 
fileExists(const QString & filename,const QFileInfo & fileInfo)307 bool FileSystem::fileExists(const QString &filename, const QFileInfo &fileInfo)
308 {
309 #ifdef Q_OS_WIN
310     if (isLnkFile(filename)) {
311         // Use a native check.
312         return fileExistsWin(filename);
313     }
314 #endif
315     bool re = fileInfo.exists();
316     // if the filename is different from the filename in fileInfo, the fileInfo is
317     // not valid. There needs to be one initialised here. Otherwise the incoming
318     // fileInfo is re-used.
319     if (fileInfo.filePath() != filename) {
320         QFileInfo myFI(filename);
321         re = myFI.exists();
322     }
323     return re;
324 }
325 
326 #ifdef Q_OS_WIN
fileSystemForPath(const QString & path)327 QString FileSystem::fileSystemForPath(const QString &path)
328 {
329     // See also QStorageInfo (Qt >=5.4) and GetVolumeInformationByHandleW (>= Vista)
330     QString drive = path.left(2);
331     if (!drive.endsWith(QLatin1Char(':')))
332         return QString();
333     drive.append(QLatin1Char('\\'));
334 
335     const size_t fileSystemBufferSize = 4096;
336     TCHAR fileSystemBuffer[fileSystemBufferSize];
337 
338     if (!GetVolumeInformationW(
339             reinterpret_cast<LPCWSTR>(drive.utf16()),
340             nullptr, 0,
341             nullptr, nullptr, nullptr,
342             fileSystemBuffer, fileSystemBufferSize)) {
343         return QString();
344     }
345     return QString::fromUtf16(reinterpret_cast<const ushort *>(fileSystemBuffer));
346 }
347 #endif
348 
remove(const QString & fileName,QString * errorString)349 bool FileSystem::remove(const QString &fileName, QString *errorString)
350 {
351 #ifdef Q_OS_WIN
352     // You cannot delete a read-only file on windows, but we want to
353     // allow that.
354     if (!QFileInfo(fileName).isWritable()) {
355         setFileReadOnly(fileName, false);
356     }
357 #endif
358     QFile f(fileName);
359     if (!f.remove()) {
360         if (errorString) {
361             *errorString = f.errorString();
362         }
363         return false;
364     }
365     return true;
366 }
367 
moveToTrash(const QString & fileName,QString * errorString)368 bool FileSystem::moveToTrash(const QString &fileName, QString *errorString)
369 {
370     // TODO: Qt 5.15 bool QFile::moveToTrash()
371 #if defined Q_OS_UNIX && !defined Q_OS_MAC
372     QString trashPath, trashFilePath, trashInfoPath;
373     QString xdgDataHome = QFile::decodeName(qgetenv("XDG_DATA_HOME"));
374     if (xdgDataHome.isEmpty()) {
375         trashPath = QDir::homePath() + QStringLiteral("/.local/share/Trash/"); // trash path that should exist
376     } else {
377         trashPath = xdgDataHome + QStringLiteral("/Trash/");
378     }
379 
380     trashFilePath = trashPath + QStringLiteral("files/"); // trash file path contain delete files
381     trashInfoPath = trashPath + QStringLiteral("info/"); // trash info path contain delete files information
382 
383     if (!(QDir().mkpath(trashFilePath) && QDir().mkpath(trashInfoPath))) {
384         *errorString = QCoreApplication::translate("FileSystem", "Could not make directories in trash");
385         return false; //mkpath will return true if path exists
386     }
387 
388     QFileInfo f(fileName);
389 
390     QDir file;
391     int suffix_number = 1;
392     if (file.exists(trashFilePath + f.fileName())) { //file in trash already exists, move to "filename.1"
393         QString path = trashFilePath + f.fileName() + QLatin1Char('.');
394         while (file.exists(path + QString::number(suffix_number))) { //or to "filename.2" if "filename.1" exists, etc
395             suffix_number++;
396         }
397         if (!file.rename(f.absoluteFilePath(), path + QString::number(suffix_number))) { // rename(file old path, file trash path)
398             *errorString = QCoreApplication::translate("FileSystem", R"(Could not move "%1" to "%2")")
399                                .arg(f.absoluteFilePath(), path + QString::number(suffix_number));
400             return false;
401         }
402     } else {
403         if (!file.rename(f.absoluteFilePath(), trashFilePath + f.fileName())) { // rename(file old path, file trash path)
404             *errorString = QCoreApplication::translate("FileSystem", R"(Could not move "%1" to "%2")")
405                                .arg(f.absoluteFilePath(), trashFilePath + f.fileName());
406             return false;
407         }
408     }
409 
410     // create file format for trash info file----- START
411     QFile infoFile;
412     if (file.exists(trashInfoPath + f.fileName() + QStringLiteral(".trashinfo"))) { //TrashInfo file already exists, create "filename.1.trashinfo"
413         QString filename = trashInfoPath + f.fileName() + QLatin1Char('.') + QString::number(suffix_number) + QStringLiteral(".trashinfo");
414         infoFile.setFileName(filename); //filename+.trashinfo //  create file information file in /.local/share/Trash/info/ folder
415     } else {
416         QString filename = trashInfoPath + f.fileName() + QStringLiteral(".trashinfo");
417         infoFile.setFileName(filename); //filename+.trashinfo //  create file information file in /.local/share/Trash/info/ folder
418     }
419 
420     infoFile.open(QIODevice::ReadWrite);
421 
422     QTextStream stream(&infoFile); // for write data on open file
423 
424     stream << "[Trash Info]\n"
425            << "Path="
426            << QUrl::toPercentEncoding(f.absoluteFilePath(), "~_-./")
427            << "\n"
428            << "DeletionDate="
429            << QDateTime::currentDateTime().toString(Qt::ISODate)
430            << '\n';
431     infoFile.close();
432 
433     // create info file format of trash file----- END
434 
435     return true;
436 #else
437     Q_UNUSED(fileName)
438     *errorString = QCoreApplication::translate("FileSystem", "Moving to the trash is not implemented on this platform");
439     return false;
440 #endif
441 }
442 
isFileLocked(const QString & fileName)443 bool FileSystem::isFileLocked(const QString &fileName)
444 {
445 #ifdef Q_OS_WIN
446     // Check if file exists
447     const QString fName = longWinPath(fileName);
448     DWORD attr = GetFileAttributesW(reinterpret_cast<const wchar_t *>(fName.utf16()));
449     if (attr != INVALID_FILE_ATTRIBUTES) {
450         // Try to open the file with as much access as possible..
451         HANDLE win_h = CreateFileW(
452             reinterpret_cast<const wchar_t *>(fName.utf16()),
453             GENERIC_READ | GENERIC_WRITE,
454             FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
455             nullptr, OPEN_EXISTING,
456             FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
457             nullptr);
458 
459         if (win_h == INVALID_HANDLE_VALUE) {
460             /* could not be opened, so locked? */
461             /* 32 == ERROR_SHARING_VIOLATION */
462             return true;
463         } else {
464             CloseHandle(win_h);
465         }
466     }
467 #else
468     Q_UNUSED(fileName);
469 #endif
470     return false;
471 }
472 
isLnkFile(const QString & filename)473 bool FileSystem::isLnkFile(const QString &filename)
474 {
475     return filename.endsWith(QLatin1String(".lnk"));
476 }
477 
isExcludeFile(const QString & filename)478 bool FileSystem::isExcludeFile(const QString &filename)
479 {
480     return filename.compare(QStringLiteral(".sync-exclude.lst"), Qt::CaseInsensitive) == 0
481         || filename.compare(QStringLiteral("exclude.lst"), Qt::CaseInsensitive) == 0
482         || filename.endsWith(QStringLiteral("/.sync-exclude.lst"), Qt::CaseInsensitive)
483         || filename.endsWith(QStringLiteral("/exclude.lst"), Qt::CaseInsensitive);
484 }
485 
isJunction(const QString & filename)486 bool FileSystem::isJunction(const QString &filename)
487 {
488 #ifdef Q_OS_WIN
489     WIN32_FIND_DATA findData;
490     HANDLE hFind = FindFirstFileEx(reinterpret_cast<const wchar_t *>(longWinPath(filename).utf16()), FindExInfoBasic, &findData, FindExSearchNameMatch, nullptr, 0);
491     if (hFind != INVALID_HANDLE_VALUE) {
492         FindClose(hFind);
493         return false;
494     }
495     return findData.dwFileAttributes != INVALID_FILE_ATTRIBUTES
496         && findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT
497         && findData.dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT;
498 #else
499     Q_UNUSED(filename);
500     return false;
501 #endif
502 }
503 
504 #ifdef Q_OS_WIN
pathtoUNC(const QString & _str)505 QString FileSystem::pathtoUNC(const QString &_str)
506 {
507     Q_ASSERT(QFileInfo(_str).isAbsolute());
508     if (_str.isEmpty()) {
509         return _str;
510     }
511     const QString str = QDir::toNativeSeparators(_str);
512     const QLatin1Char sep('\\');
513 
514     // we already have a unc path
515     if (str.startsWith(sep + sep)) {
516         return str;
517     }
518     // prepend \\?\ and to support long names
519 
520     if (str.at(0) == sep) {
521         // should not happen as we require the path to be absolute
522         return QStringLiteral(R"(\\?)") + str;
523     }
524     return QStringLiteral(R"(\\?\)") + str;
525 }
526 #endif
527 
528 } // namespace OCC
529