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