1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org> |
4 | SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org> |
5 | |
6 | SPDX-License-Identifier: LGPL-2.0-only |
7 | */ |
8 | |
9 | #include "kmountpoint.h" |
10 | |
11 | #include <stdlib.h> |
12 | |
13 | #include "../utils_p.h" |
14 | #include <config-kmountpoint.h> |
15 | #include <kioglobal_p.h> // Defines QT_LSTAT on windows to kio_windows_lstat |
16 | |
17 | #include <QDebug> |
18 | #include <QDir> |
19 | #include <QFile> |
20 | #include <QFileInfo> |
21 | #include <QTextStream> |
22 | |
23 | #include <qplatformdefs.h> |
24 | |
25 | #ifdef Q_OS_WIN |
26 | #include <qt_windows.h> |
27 | static const Qt::CaseSensitivity cs = Qt::CaseInsensitive; |
28 | #else |
29 | static const Qt::CaseSensitivity cs = Qt::CaseSensitive; |
30 | #endif |
31 | |
32 | // This is the *BSD branch |
33 | #if HAVE_SYS_MOUNT_H |
34 | #if HAVE_SYS_PARAM_H |
35 | #include <sys/param.h> |
36 | #endif |
37 | // FreeBSD has a table of names of mount-options in mount.h, which is only |
38 | // defined (as MNTOPT_NAMES) if _WANT_MNTOPTNAMES is defined. |
39 | #define _WANT_MNTOPTNAMES |
40 | #include <sys/mount.h> |
41 | #undef _WANT_MNTOPTNAMES |
42 | #endif |
43 | |
44 | #if HAVE_FSTAB_H |
45 | #include <fstab.h> |
46 | #endif |
47 | |
48 | // Linux |
49 | #if HAVE_LIB_MOUNT |
50 | #include <libmount/libmount.h> |
51 | #endif |
52 | |
53 | static bool isNetfs(const QString &mountType) |
54 | { |
55 | // List copied from util-linux/libmount/src/utils.c |
56 | static const std::vector<QLatin1String> netfsList{ |
57 | QLatin1String("cifs" ), |
58 | QLatin1String("smb3" ), |
59 | QLatin1String("smbfs" ), |
60 | QLatin1String("nfs" ), |
61 | QLatin1String("nfs3" ), |
62 | QLatin1String("nfs4" ), |
63 | QLatin1String("afs" ), |
64 | QLatin1String("ncpfs" ), |
65 | QLatin1String("fuse.curlftpfs" ), |
66 | QLatin1String("fuse.sshfs" ), |
67 | QLatin1String("9p" ), |
68 | }; |
69 | |
70 | return std::any_of(first: netfsList.cbegin(), last: netfsList.cend(), pred: [mountType](const QLatin1String netfs) { |
71 | return mountType == netfs; |
72 | }); |
73 | } |
74 | |
75 | class KMountPointPrivate |
76 | { |
77 | public: |
78 | void resolveGvfsMountPoints(KMountPoint::List &result); |
79 | void finalizePossibleMountPoint(KMountPoint::DetailsNeededFlags infoNeeded); |
80 | void finalizeCurrentMountPoint(KMountPoint::DetailsNeededFlags infoNeeded); |
81 | |
82 | QString m_mountedFrom; |
83 | QString m_device; // Only available when the NeedRealDeviceName flag was set. |
84 | QString m_mountPoint; |
85 | QString m_mountType; |
86 | QStringList m_mountOptions; |
87 | dev_t m_deviceId = 0; |
88 | bool m_isNetFs = false; |
89 | }; |
90 | |
91 | KMountPoint::KMountPoint() |
92 | : d(new KMountPointPrivate) |
93 | { |
94 | } |
95 | |
96 | KMountPoint::~KMountPoint() = default; |
97 | |
98 | #if HAVE_GETMNTINFO |
99 | |
100 | #ifdef MNTOPT_NAMES |
101 | static struct mntoptnames bsdOptionNames[] = {MNTOPT_NAMES}; |
102 | |
103 | /** @brief Get mount options from @p flags and puts human-readable version in @p list |
104 | * |
105 | * Appends all positive options found in @p flags to the @p list |
106 | * This is roughly paraphrased from FreeBSD's mount.c, prmount(). |
107 | */ |
108 | static void translateMountOptions(QStringList &list, uint64_t flags) |
109 | { |
110 | const struct mntoptnames *optionInfo = bsdOptionNames; |
111 | |
112 | // Not all 64 bits are useful option names |
113 | flags = flags & MNT_VISFLAGMASK; |
114 | // Chew up options as long as we're in the table and there |
115 | // are any flags left. |
116 | for (; flags != 0 && optionInfo->o_opt != 0; ++optionInfo) { |
117 | if (flags & optionInfo->o_opt) { |
118 | list.append(QString::fromLatin1(optionInfo->o_name)); |
119 | flags &= ~optionInfo->o_opt; |
120 | } |
121 | } |
122 | } |
123 | #else |
124 | /** @brief Get mount options from @p flags and puts human-readable version in @p list |
125 | * |
126 | * This default version just puts the hex representation of @p flags |
127 | * in the list, because there is no human-readable version. |
128 | */ |
129 | static void translateMountOptions(QStringList &list, uint64_t flags) |
130 | { |
131 | list.append(QStringLiteral("0x%1" ).arg(QString::number(flags, 16))); |
132 | } |
133 | #endif |
134 | |
135 | #endif // HAVE_GETMNTINFO |
136 | |
137 | void KMountPointPrivate::finalizePossibleMountPoint(KMountPoint::DetailsNeededFlags infoNeeded) |
138 | { |
139 | QString potentialDevice; |
140 | if (const auto tag = QLatin1String("UUID=" ); m_mountedFrom.startsWith(s: tag)) { |
141 | potentialDevice = QFile::symLinkTarget(fileName: QLatin1String("/dev/disk/by-uuid/" ) + QStringView(m_mountedFrom).mid(pos: tag.size())); |
142 | } else if (const auto tag = QLatin1String("LABEL=" ); m_mountedFrom.startsWith(s: tag)) { |
143 | potentialDevice = QFile::symLinkTarget(fileName: QLatin1String("/dev/disk/by-label/" ) + QStringView(m_mountedFrom).mid(pos: tag.size())); |
144 | } |
145 | |
146 | if (QFile::exists(fileName: potentialDevice)) { |
147 | m_mountedFrom = potentialDevice; |
148 | } |
149 | |
150 | if (infoNeeded & KMountPoint::NeedRealDeviceName) { |
151 | if (m_mountedFrom.startsWith(c: QLatin1Char('/'))) { |
152 | m_device = QFileInfo(m_mountedFrom).canonicalFilePath(); |
153 | } |
154 | } |
155 | |
156 | // Chop trailing slash |
157 | Utils::removeTrailingSlash(path&: m_mountedFrom); |
158 | } |
159 | |
160 | void KMountPointPrivate::finalizeCurrentMountPoint(KMountPoint::DetailsNeededFlags infoNeeded) |
161 | { |
162 | if (infoNeeded & KMountPoint::NeedRealDeviceName) { |
163 | if (m_mountedFrom.startsWith(c: QLatin1Char('/'))) { |
164 | m_device = QFileInfo(m_mountedFrom).canonicalFilePath(); |
165 | } |
166 | } |
167 | } |
168 | |
169 | KMountPoint::List KMountPoint::possibleMountPoints(DetailsNeededFlags infoNeeded) |
170 | { |
171 | KMountPoint::List result; |
172 | |
173 | #ifdef Q_OS_WIN |
174 | result = KMountPoint::currentMountPoints(infoNeeded); |
175 | |
176 | #elif HAVE_LIB_MOUNT |
177 | if (struct libmnt_table *table = mnt_new_table()) { |
178 | // By default parses "/etc/fstab" |
179 | if (mnt_table_parse_fstab(tb: table, filename: nullptr) == 0) { |
180 | struct libmnt_iter *itr = mnt_new_iter(direction: MNT_ITER_FORWARD); |
181 | struct libmnt_fs *fs; |
182 | |
183 | while (mnt_table_next_fs(tb: table, itr, fs: &fs) == 0) { |
184 | const char *fsType = mnt_fs_get_fstype(fs); |
185 | if (qstrcmp(str1: fsType, str2: "swap" ) == 0) { |
186 | continue; |
187 | } |
188 | |
189 | Ptr mp(new KMountPoint); |
190 | mp->d->m_mountType = QFile::decodeName(localFileName: fsType); |
191 | const char *target = mnt_fs_get_target(fs); |
192 | mp->d->m_mountPoint = QFile::decodeName(localFileName: target); |
193 | |
194 | if (QT_STATBUF buff; QT_LSTAT(file: target, buf: &buff) == 0) { |
195 | mp->d->m_deviceId = buff.st_dev; |
196 | } |
197 | |
198 | // First field in /etc/fstab, e.g. /dev/sdXY, LABEL=, UUID=, /some/bind/mount/dir |
199 | // or some network mount |
200 | if (const char *source = mnt_fs_get_source(fs)) { |
201 | mp->d->m_mountedFrom = QFile::decodeName(localFileName: source); |
202 | } |
203 | |
204 | if (infoNeeded & NeedMountOptions) { |
205 | mp->d->m_mountOptions = QFile::decodeName(localFileName: mnt_fs_get_options(fs)).split(sep: QLatin1Char(',')); |
206 | } |
207 | |
208 | mp->d->finalizePossibleMountPoint(infoNeeded); |
209 | result.append(t: mp); |
210 | } |
211 | mnt_free_iter(itr); |
212 | } |
213 | |
214 | mnt_free_table(tb: table); |
215 | } |
216 | #elif HAVE_FSTAB_H |
217 | |
218 | QFile f{QLatin1String(FSTAB)}; |
219 | if (!f.open(QIODevice::ReadOnly)) { |
220 | return result; |
221 | } |
222 | |
223 | QTextStream t(&f); |
224 | QString s; |
225 | |
226 | while (!t.atEnd()) { |
227 | s = t.readLine().simplified(); |
228 | if (s.isEmpty() || (s[0] == QLatin1Char('#'))) { |
229 | continue; |
230 | } |
231 | |
232 | // not empty or commented out by '#' |
233 | const QStringList item = s.split(QLatin1Char(' ')); |
234 | |
235 | if (item.count() < 4) { |
236 | continue; |
237 | } |
238 | |
239 | Ptr mp(new KMountPoint); |
240 | |
241 | int i = 0; |
242 | mp->d->m_mountedFrom = item[i++]; |
243 | mp->d->m_mountPoint = item[i++]; |
244 | mp->d->m_mountType = item[i++]; |
245 | if (mp->d->m_mountType == QLatin1String("swap" )) { |
246 | continue; |
247 | } |
248 | QString options = item[i++]; |
249 | |
250 | if (infoNeeded & NeedMountOptions) { |
251 | mp->d->m_mountOptions = options.split(QLatin1Char(',')); |
252 | } |
253 | |
254 | mp->d->finalizePossibleMountPoint(infoNeeded); |
255 | |
256 | result.append(mp); |
257 | } // while |
258 | |
259 | f.close(); |
260 | #endif |
261 | |
262 | return result; |
263 | } |
264 | |
265 | void KMountPointPrivate::resolveGvfsMountPoints(KMountPoint::List &result) |
266 | { |
267 | if (m_mountedFrom == QLatin1String("gvfsd-fuse" )) { |
268 | const QDir gvfsDir(m_mountPoint); |
269 | const QStringList mountDirs = gvfsDir.entryList(filters: QDir::Dirs | QDir::NoDotAndDotDot); |
270 | for (const QString &mountDir : mountDirs) { |
271 | const QString type = mountDir.section(asep: QLatin1Char(':'), astart: 0, aend: 0); |
272 | if (type.isEmpty()) { |
273 | continue; |
274 | } |
275 | |
276 | KMountPoint::Ptr gvfsmp(new KMountPoint); |
277 | gvfsmp->d->m_mountedFrom = m_mountedFrom; |
278 | gvfsmp->d->m_mountPoint = m_mountPoint + QLatin1Char('/') + mountDir; |
279 | gvfsmp->d->m_mountType = type; |
280 | result.append(t: gvfsmp); |
281 | } |
282 | } |
283 | } |
284 | |
285 | KMountPoint::List KMountPoint::currentMountPoints(DetailsNeededFlags infoNeeded) |
286 | { |
287 | KMountPoint::List result; |
288 | |
289 | #if HAVE_GETMNTINFO |
290 | |
291 | #if GETMNTINFO_USES_STATVFS |
292 | struct statvfs *mounted; |
293 | #else |
294 | struct statfs *mounted; |
295 | #endif |
296 | |
297 | int num_fs = getmntinfo(&mounted, MNT_NOWAIT); |
298 | |
299 | result.reserve(num_fs); |
300 | |
301 | for (int i = 0; i < num_fs; i++) { |
302 | Ptr mp(new KMountPoint); |
303 | mp->d->m_mountedFrom = QFile::decodeName(mounted[i].f_mntfromname); |
304 | mp->d->m_mountPoint = QFile::decodeName(mounted[i].f_mntonname); |
305 | mp->d->m_mountType = QFile::decodeName(mounted[i].f_fstypename); |
306 | |
307 | if (QT_STATBUF buff; QT_LSTAT(mounted[i].f_mntonname, &buff) == 0) { |
308 | mp->d->m_deviceId = buff.st_dev; |
309 | } |
310 | |
311 | if (infoNeeded & NeedMountOptions) { |
312 | struct fstab *ft = getfsfile(mounted[i].f_mntonname); |
313 | if (ft != nullptr) { |
314 | QString options = QFile::decodeName(ft->fs_mntops); |
315 | mp->d->m_mountOptions = options.split(QLatin1Char(',')); |
316 | } else { |
317 | translateMountOptions(mp->d->m_mountOptions, mounted[i].f_flags); |
318 | } |
319 | } |
320 | |
321 | mp->d->finalizeCurrentMountPoint(infoNeeded); |
322 | // TODO: Strip trailing '/' ? |
323 | result.append(mp); |
324 | } |
325 | |
326 | #elif defined(Q_OS_WIN) |
327 | // nothing fancy with infoNeeded but it gets the job done |
328 | DWORD bits = GetLogicalDrives(); |
329 | if (!bits) { |
330 | return result; |
331 | } |
332 | |
333 | for (int i = 0; i < 26; i++) { |
334 | if (bits & (1 << i)) { |
335 | Ptr mp(new KMountPoint); |
336 | mp->d->m_mountPoint = QString(QLatin1Char('A' + i) + QLatin1String(":/" )); |
337 | result.append(mp); |
338 | } |
339 | } |
340 | |
341 | #elif HAVE_LIB_MOUNT |
342 | if (struct libmnt_table *table = mnt_new_table()) { |
343 | // if "/etc/mtab" is a regular file, |
344 | // "/etc/mtab" is used by default instead of "/proc/self/mountinfo" file. |
345 | // This leads to NTFS mountpoints being hidden. |
346 | if (mnt_table_parse_mtab(tb: table, filename: "/proc/self/mountinfo" ) == 0) { |
347 | struct libmnt_iter *itr = mnt_new_iter(direction: MNT_ITER_FORWARD); |
348 | struct libmnt_fs *fs; |
349 | |
350 | while (mnt_table_next_fs(tb: table, itr, fs: &fs) == 0) { |
351 | Ptr mp(new KMountPoint); |
352 | mp->d->m_mountedFrom = QFile::decodeName(localFileName: mnt_fs_get_source(fs)); |
353 | mp->d->m_mountPoint = QFile::decodeName(localFileName: mnt_fs_get_target(fs)); |
354 | mp->d->m_mountType = QFile::decodeName(localFileName: mnt_fs_get_fstype(fs)); |
355 | mp->d->m_isNetFs = mnt_fs_is_netfs(fs) == 1; |
356 | mp->d->m_deviceId = mnt_fs_get_devno(fs); |
357 | |
358 | if (infoNeeded & NeedMountOptions) { |
359 | mp->d->m_mountOptions = QFile::decodeName(localFileName: mnt_fs_get_options(fs)).split(sep: QLatin1Char(',')); |
360 | } |
361 | |
362 | if (infoNeeded & NeedRealDeviceName) { |
363 | if (mp->d->m_mountedFrom.startsWith(c: QLatin1Char('/'))) { |
364 | mp->d->m_device = mp->d->m_mountedFrom; |
365 | } |
366 | } |
367 | |
368 | mp->d->resolveGvfsMountPoints(result); |
369 | |
370 | mp->d->finalizeCurrentMountPoint(infoNeeded); |
371 | result.push_back(t: mp); |
372 | } |
373 | |
374 | mnt_free_iter(itr); |
375 | } |
376 | |
377 | mnt_free_table(tb: table); |
378 | } |
379 | #endif |
380 | |
381 | return result; |
382 | } |
383 | |
384 | QString KMountPoint::mountedFrom() const |
385 | { |
386 | return d->m_mountedFrom; |
387 | } |
388 | |
389 | dev_t KMountPoint::deviceId() const |
390 | { |
391 | return d->m_deviceId; |
392 | } |
393 | |
394 | bool KMountPoint::isOnNetwork() const |
395 | { |
396 | return d->m_isNetFs || isNetfs(mountType: d->m_mountType); |
397 | } |
398 | |
399 | QString KMountPoint::realDeviceName() const |
400 | { |
401 | return d->m_device; |
402 | } |
403 | |
404 | QString KMountPoint::mountPoint() const |
405 | { |
406 | return d->m_mountPoint; |
407 | } |
408 | |
409 | QString KMountPoint::mountType() const |
410 | { |
411 | return d->m_mountType; |
412 | } |
413 | |
414 | QStringList KMountPoint::mountOptions() const |
415 | { |
416 | return d->m_mountOptions; |
417 | } |
418 | |
419 | KMountPoint::List::List() |
420 | : QList<Ptr>() |
421 | { |
422 | } |
423 | |
424 | KMountPoint::Ptr KMountPoint::List::findByPath(const QString &path) const |
425 | { |
426 | #ifdef Q_OS_WIN |
427 | const QString realPath = QDir::fromNativeSeparators(QDir(path).absolutePath()); |
428 | #else |
429 | /* If the path contains symlinks, get the real name */ |
430 | QFileInfo fileinfo(path); |
431 | // canonicalFilePath won't work unless file exists |
432 | const QString realPath = fileinfo.exists() ? fileinfo.canonicalFilePath() : fileinfo.absolutePath(); |
433 | #endif |
434 | |
435 | KMountPoint::Ptr result; |
436 | |
437 | if (QT_STATBUF buff; QT_LSTAT(file: QFile::encodeName(fileName: realPath).constData(), buf: &buff) == 0) { |
438 | auto it = std::find_if(first: this->cbegin(), last: this->cend(), pred: [&buff, &realPath](const KMountPoint::Ptr &mountPtr) { |
439 | // For a bind mount, the deviceId() is that of the base mount point, e.g. /mnt/foo, |
440 | // however the path we're looking for, e.g. /home/user/bar, doesn't start with the |
441 | // mount point of the base device, so we go on searching |
442 | return mountPtr->deviceId() == buff.st_dev && realPath.startsWith(s: mountPtr->mountPoint()); |
443 | }); |
444 | |
445 | if (it != this->cend()) { |
446 | result = *it; |
447 | } |
448 | } |
449 | |
450 | return result; |
451 | } |
452 | |
453 | KMountPoint::Ptr KMountPoint::List::findByDevice(const QString &device) const |
454 | { |
455 | const QString realDevice = QFileInfo(device).canonicalFilePath(); |
456 | if (realDevice.isEmpty()) { // d->m_device can be empty in the loop below, don't match empty with it |
457 | return Ptr(); |
458 | } |
459 | for (const KMountPoint::Ptr &mountPoint : *this) { |
460 | if (realDevice.compare(s: mountPoint->d->m_device, cs) == 0 || realDevice.compare(s: mountPoint->d->m_mountedFrom, cs) == 0) { |
461 | return mountPoint; |
462 | } |
463 | } |
464 | return Ptr(); |
465 | } |
466 | |
467 | bool KMountPoint::probablySlow() const |
468 | { |
469 | /* clang-format off */ |
470 | return isOnNetwork() |
471 | || d->m_mountType == QLatin1String("autofs" ) |
472 | || d->m_mountType == QLatin1String("subfs" ) |
473 | // Technically KIOFUSe mounts local workers as well, |
474 | // such as recents:/, but better safe than sorry... |
475 | || d->m_mountType == QLatin1String("fuse.kio-fuse" ); |
476 | /* clang-format on */ |
477 | } |
478 | |
479 | bool KMountPoint::testFileSystemFlag(FileSystemFlag flag) const |
480 | { |
481 | /* clang-format off */ |
482 | const bool isMsDos = d->m_mountType == QLatin1String("msdos" ) |
483 | || d->m_mountType == QLatin1String("fat" ) |
484 | || d->m_mountType == QLatin1String("vfat" ); |
485 | |
486 | const bool isNtfs = d->m_mountType.contains(s: QLatin1String("fuse.ntfs" )) |
487 | || d->m_mountType.contains(s: QLatin1String("fuseblk.ntfs" )) |
488 | // fuseblk could really be anything. But its most common use is for NTFS mounts, these days. |
489 | || d->m_mountType == QLatin1String("fuseblk" ); |
490 | |
491 | const bool isSmb = d->m_mountType == QLatin1String("cifs" ) |
492 | || d->m_mountType == QLatin1String("smb3" ) |
493 | || d->m_mountType == QLatin1String("smbfs" ) |
494 | // gvfs-fuse mounted SMB share |
495 | || d->m_mountType == QLatin1String("smb-share" ); |
496 | /* clang-format on */ |
497 | |
498 | switch (flag) { |
499 | case SupportsChmod: |
500 | case SupportsChown: |
501 | case SupportsUTime: |
502 | case SupportsSymlinks: |
503 | return !isMsDos && !isNtfs && !isSmb; // it's amazing the number of things Microsoft filesystems don't support :) |
504 | case CaseInsensitive: |
505 | return isMsDos; |
506 | } |
507 | return false; |
508 | } |
509 | |
510 | KIOCORE_EXPORT QDebug operator<<(QDebug debug, const KMountPoint::Ptr &mp) |
511 | { |
512 | QDebugStateSaver saver(debug); |
513 | if (!mp) { |
514 | debug << "QDebug operator<< called on a null KMountPoint::Ptr" ; |
515 | return debug; |
516 | } |
517 | |
518 | // clang-format off |
519 | debug.nospace() << "KMountPoint [" |
520 | << "Mounted from: " << mp->d->m_mountedFrom |
521 | << ", device name: " << mp->d->m_device |
522 | << ", mount point: " << mp->d->m_mountPoint |
523 | << ", mount type: " << mp->d->m_mountType |
524 | <<']'; |
525 | |
526 | // clang-format on |
527 | return debug; |
528 | } |
529 | |