1 /*
2     This file is part of the KDE libraries
3 
4     SPDX-FileCopyrightText: 2006 Jacob R Rideout <kde@jacobrideout.net>
5     SPDX-FileCopyrightText: 2015 Nick Shaforostoff <shafff@ukr.net>
6 
7     SPDX-License-Identifier: LGPL-2.0-or-later
8 */
9 
10 #include "kautosavefile.h"
11 
12 #include <climits> // for NAME_MAX
13 
14 #ifdef Q_OS_WIN
15 #include <stdlib.h> // for _MAX_FNAME
16 static const int maxNameLength = _MAX_FNAME;
17 #else
18 static const int maxNameLength = NAME_MAX;
19 #endif
20 
21 #include "kcoreaddons_debug.h"
22 #include "krandom.h"
23 #include <QCoreApplication>
24 #include <QDir>
25 #include <QLatin1Char>
26 #include <QLockFile>
27 #include <QStandardPaths>
28 
29 class KAutoSaveFilePrivate
30 {
31 public:
32     enum {
33         NamePadding = 8,
34     };
35 
36     QString tempFileName();
37     QUrl managedFile;
38     QLockFile *lock = nullptr;
39     bool managedFileNameChanged = false;
40 };
41 
findAllStales(const QString & appName)42 static QStringList findAllStales(const QString &appName)
43 {
44     const QStringList dirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
45     QStringList files;
46 
47     for (const QString &dir : dirs) {
48         QDir appDir(dir + QLatin1String("/stalefiles/") + appName);
49         qCDebug(KCOREADDONS_DEBUG) << "Looking in" << appDir.absolutePath();
50         const auto listFiles = appDir.entryList(QDir::Files);
51         for (const QString &file : listFiles) {
52             files << (appDir.absolutePath() + QLatin1Char('/') + file);
53         }
54     }
55     return files;
56 }
57 
tempFileName()58 QString KAutoSaveFilePrivate::tempFileName()
59 {
60     // Note: we drop any query string and user/pass info
61     const QString protocol(managedFile.scheme());
62     const QByteArray encodedDirectory = QUrl::toPercentEncoding(managedFile.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path());
63     const QString directory = QString::fromLatin1(encodedDirectory);
64     const QByteArray encodedFileName = QUrl::toPercentEncoding(managedFile.fileName());
65     QString fileName = QString::fromLatin1(encodedFileName);
66 
67     // Remove any part of the path to the right if it is longer than the maximum file name length;
68     // note that "file name" in this context means the file name component only (e.g. test.txt), and
69     // not the whole path (e.g. /home/simba/text.txt).
70     // Ensure that the max. file name length takes into account the other parts of the tempFileName
71     // Subtract 1 for the _ char, 3 for the padding separator, 5 is for the .lock,
72     // 7 for QLockFile's internal code (adding tmp .rmlock) = 16
73     const int pathLengthLimit = maxNameLength - NamePadding - fileName.size() - protocol.size() - 16;
74 
75     const QString junk = KRandom::randomString(NamePadding);
76     // This is done so that the separation between the filename and path can be determined
77     fileName += QStringView(junk).right(3) + protocol + QLatin1Char('_') + QStringView(directory).left(pathLengthLimit) + junk;
78 
79     return fileName;
80 }
81 
KAutoSaveFile(const QUrl & filename,QObject * parent)82 KAutoSaveFile::KAutoSaveFile(const QUrl &filename, QObject *parent)
83     : QFile(parent)
84     , d(new KAutoSaveFilePrivate)
85 {
86     setManagedFile(filename);
87 }
88 
KAutoSaveFile(QObject * parent)89 KAutoSaveFile::KAutoSaveFile(QObject *parent)
90     : QFile(parent)
91     , d(new KAutoSaveFilePrivate)
92 {
93 }
94 
~KAutoSaveFile()95 KAutoSaveFile::~KAutoSaveFile()
96 {
97     releaseLock();
98     delete d->lock;
99 }
100 
managedFile() const101 QUrl KAutoSaveFile::managedFile() const
102 {
103     return d->managedFile;
104 }
105 
setManagedFile(const QUrl & filename)106 void KAutoSaveFile::setManagedFile(const QUrl &filename)
107 {
108     releaseLock();
109 
110     d->managedFile = filename;
111     d->managedFileNameChanged = true;
112 }
113 
releaseLock()114 void KAutoSaveFile::releaseLock()
115 {
116     if (d->lock && d->lock->isLocked()) {
117         delete d->lock;
118         d->lock = nullptr;
119         if (!fileName().isEmpty()) {
120             remove();
121         }
122     }
123 }
124 
open(OpenMode openmode)125 bool KAutoSaveFile::open(OpenMode openmode)
126 {
127     if (d->managedFile.isEmpty()) {
128         return false;
129     }
130 
131     QString tempFile;
132     if (d->managedFileNameChanged) {
133         QString staleFilesDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/stalefiles/")
134             + QCoreApplication::instance()->applicationName();
135         if (!QDir().mkpath(staleFilesDir)) {
136             return false;
137         }
138         tempFile = staleFilesDir + QChar::fromLatin1('/') + d->tempFileName();
139     } else {
140         tempFile = fileName();
141     }
142 
143     d->managedFileNameChanged = false;
144 
145     setFileName(tempFile);
146 
147     if (QFile::open(openmode)) {
148         if (!d->lock) {
149             d->lock = new QLockFile(tempFile + QLatin1String(".lock"));
150             d->lock->setStaleLockTime(60 * 1000); // HARDCODE, 1 minute
151         }
152 
153         if (d->lock->isLocked() || d->lock->tryLock()) {
154             return true;
155         } else {
156             qCWarning(KCOREADDONS_DEBUG) << "Could not lock file:" << tempFile;
157             close();
158         }
159     }
160 
161     return false;
162 }
163 
extractManagedFilePath(const QString & staleFileName)164 static QUrl extractManagedFilePath(const QString &staleFileName)
165 {
166     const QStringView stale{staleFileName};
167     // Warning, if we had a long path, it was truncated by tempFileName()
168     // So in that case, extractManagedFilePath will return an incorrect truncated path for original source
169     const auto sep = stale.right(3);
170     const int sepPos = staleFileName.indexOf(sep);
171     const QByteArray managedFilename = stale.left(sepPos).toLatin1();
172 
173     const int pathPos = staleFileName.indexOf(QChar::fromLatin1('_'), sepPos);
174     QUrl managedFileName;
175     // name.setScheme(file.mid(sepPos + 3, pathPos - sep.size() - 3));
176     const QByteArray encodedPath = stale.mid(pathPos + 1, staleFileName.length() - pathPos - 1 - KAutoSaveFilePrivate::NamePadding).toLatin1();
177     managedFileName.setPath(QUrl::fromPercentEncoding(encodedPath) + QLatin1Char('/') + QFileInfo(QUrl::fromPercentEncoding(managedFilename)).fileName());
178     return managedFileName;
179 }
180 
staleMatchesManaged(const QString & staleFileName,const QUrl & managedFile)181 bool staleMatchesManaged(const QString &staleFileName, const QUrl &managedFile)
182 {
183     const QStringView stale{staleFileName};
184     const auto sep = stale.right(3);
185     int sepPos = staleFileName.indexOf(sep);
186     // Check filenames first
187     if (managedFile.fileName() != QUrl::fromPercentEncoding(stale.left(sepPos).toLatin1())) {
188         return false;
189     }
190     // Check paths
191     const int pathPos = staleFileName.indexOf(QChar::fromLatin1('_'), sepPos);
192     const QByteArray encodedPath = stale.mid(pathPos + 1, staleFileName.length() - pathPos - 1 - KAutoSaveFilePrivate::NamePadding).toLatin1();
193     return QUrl::toPercentEncoding(managedFile.path()).startsWith(encodedPath);
194 }
195 
staleFiles(const QUrl & filename,const QString & applicationName)196 QList<KAutoSaveFile *> KAutoSaveFile::staleFiles(const QUrl &filename, const QString &applicationName)
197 {
198     QString appName(applicationName);
199     if (appName.isEmpty()) {
200         appName = QCoreApplication::instance()->applicationName();
201     }
202 
203     // get stale files
204     const QStringList files = findAllStales(appName);
205 
206     QList<KAutoSaveFile *> list;
207 
208     // contruct a KAutoSaveFile for stale files corresponding given filename
209     for (const QString &file : files) {
210         if (file.endsWith(QLatin1String(".lock")) || (!filename.isEmpty() && !staleMatchesManaged(QFileInfo(file).fileName(), filename))) {
211             continue;
212         }
213 
214         // sets managedFile
215         KAutoSaveFile *asFile = new KAutoSaveFile(filename.isEmpty() ? extractManagedFilePath(file) : filename);
216         asFile->setFileName(file);
217         asFile->d->managedFileNameChanged = false; // do not regenerate tempfile name
218         list.append(asFile);
219     }
220 
221     return list;
222 }
223 
allStaleFiles(const QString & applicationName)224 QList<KAutoSaveFile *> KAutoSaveFile::allStaleFiles(const QString &applicationName)
225 {
226     return staleFiles(QUrl(), applicationName);
227 }
228 
229 #include "moc_kautosavefile.cpp"
230