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>
27static const Qt::CaseSensitivity cs = Qt::CaseInsensitive;
28#else
29static 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
53static 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
75class KMountPointPrivate
76{
77public:
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
91KMountPoint::KMountPoint()
92 : d(new KMountPointPrivate)
93{
94}
95
96KMountPoint::~KMountPoint() = default;
97
98#if HAVE_GETMNTINFO
99
100#ifdef MNTOPT_NAMES
101static struct mntoptnames bsdOptionNames[] = {MNTOPT_NAMES};
102
103/* Get mount options from flags and puts human-readable version in list
104 *
105 * Appends all positive options found in flags to the list
106 * This is roughly paraphrased from FreeBSD's mount.c, prmount().
107 */
108static 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/* Get mount options from flags and puts human-readable version in list
125 *
126 * This default version just puts the hex representation of flags
127 * in the list, because there is no human-readable version.
128 */
129static 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
137void 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
160void 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
169KMountPoint::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
265void 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
285KMountPoint::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 (
347#if QT_VERSION_CHECK(LIBMOUNT_MAJOR_VERSION, LIBMOUNT_MINOR_VERSION, LIBMOUNT_PATCH_VERSION) >= QT_VERSION_CHECK(2, 39, 0)
348 mnt_table_parse_mtab(table, nullptr)
349#else // backwards compat, the documentation advises to use nullptr so lets do that whenever possible
350 mnt_table_parse_mtab(tb: table, filename: "/proc/self/mountinfo")
351#endif
352 == 0) {
353 struct libmnt_iter *itr = mnt_new_iter(direction: MNT_ITER_FORWARD);
354 struct libmnt_fs *fs;
355
356 while (mnt_table_next_fs(tb: table, itr, fs: &fs) == 0) {
357 Ptr mp(new KMountPoint);
358 mp->d->m_mountedFrom = QFile::decodeName(localFileName: mnt_fs_get_source(fs));
359 mp->d->m_mountPoint = QFile::decodeName(localFileName: mnt_fs_get_target(fs));
360 mp->d->m_mountType = QFile::decodeName(localFileName: mnt_fs_get_fstype(fs));
361 mp->d->m_isNetFs = mnt_fs_is_netfs(fs) == 1;
362
363 // handle bind mounts
364 if (mp->d->m_mountedFrom != mp->d->m_mountPoint) {
365 if (QT_STATBUF buff; QT_LSTAT(file: mnt_fs_get_target(fs), buf: &buff) == 0) {
366 mp->d->m_deviceId = buff.st_dev;
367 }
368 } else {
369 mp->d->m_deviceId = mnt_fs_get_devno(fs);
370 }
371
372 if (infoNeeded & NeedMountOptions) {
373 mp->d->m_mountOptions = QFile::decodeName(localFileName: mnt_fs_get_options(fs)).split(sep: QLatin1Char(','));
374 }
375
376 if (infoNeeded & NeedRealDeviceName) {
377 if (mp->d->m_mountedFrom.startsWith(c: QLatin1Char('/'))) {
378 mp->d->m_device = mp->d->m_mountedFrom;
379 }
380 }
381
382 mp->d->resolveGvfsMountPoints(result);
383
384 mp->d->finalizeCurrentMountPoint(infoNeeded);
385 result.push_back(t: mp);
386 }
387
388 mnt_free_iter(itr);
389 }
390
391 mnt_free_table(tb: table);
392 }
393#endif
394
395 return result;
396}
397
398QString KMountPoint::mountedFrom() const
399{
400 return d->m_mountedFrom;
401}
402
403dev_t KMountPoint::deviceId() const
404{
405 return d->m_deviceId;
406}
407
408bool KMountPoint::isOnNetwork() const
409{
410 return d->m_isNetFs || isNetfs(mountType: d->m_mountType);
411}
412
413QString KMountPoint::realDeviceName() const
414{
415 return d->m_device;
416}
417
418QString KMountPoint::mountPoint() const
419{
420 return d->m_mountPoint;
421}
422
423QString KMountPoint::mountType() const
424{
425 return d->m_mountType;
426}
427
428QStringList KMountPoint::mountOptions() const
429{
430 return d->m_mountOptions;
431}
432
433KMountPoint::List::List()
434 : QList<Ptr>()
435{
436}
437
438KMountPoint::Ptr KMountPoint::List::findByPath(const QString &path) const
439{
440#ifdef Q_OS_WIN
441 const QString realPath = QDir::fromNativeSeparators(QDir(path).absolutePath());
442#else
443 /* If the path contains symlinks, get the real name */
444 QFileInfo fileinfo(path);
445 // canonicalFilePath won't work unless file exists
446 const QString realPath = fileinfo.exists() ? fileinfo.canonicalFilePath() : fileinfo.absolutePath();
447#endif
448
449 KMountPoint::Ptr result;
450
451 if (QT_STATBUF buff; QT_LSTAT(file: QFile::encodeName(fileName: realPath).constData(), buf: &buff) == 0) {
452 auto it = std::find_if(first: this->cbegin(), last: this->cend(), pred: [&buff, &realPath](const KMountPoint::Ptr &mountPtr) {
453 // For a bind mount, the deviceId() is that of the base mount point, e.g. /mnt/foo,
454 // however the path we're looking for, e.g. /home/user/bar, doesn't start with the
455 // mount point of the base device, so we go on searching
456 return mountPtr->deviceId() == buff.st_dev && realPath.startsWith(s: mountPtr->mountPoint());
457 });
458
459 if (it != this->cend()) {
460 result = *it;
461 }
462 }
463
464 return result;
465}
466
467KMountPoint::Ptr KMountPoint::List::findByDevice(const QString &device) const
468{
469 const QString realDevice = QFileInfo(device).canonicalFilePath();
470 if (realDevice.isEmpty()) { // d->m_device can be empty in the loop below, don't match empty with it
471 return Ptr();
472 }
473 for (const KMountPoint::Ptr &mountPoint : *this) {
474 if (realDevice.compare(s: mountPoint->d->m_device, cs) == 0 || realDevice.compare(s: mountPoint->d->m_mountedFrom, cs) == 0) {
475 return mountPoint;
476 }
477 }
478 return Ptr();
479}
480
481bool KMountPoint::probablySlow() const
482{
483 /* clang-format off */
484 return isOnNetwork()
485 || d->m_mountType == QLatin1String("autofs")
486 || d->m_mountType == QLatin1String("subfs")
487 // Technically KIOFUSe mounts local workers as well,
488 // such as recents:/, but better safe than sorry...
489 || d->m_mountType == QLatin1String("fuse.kio-fuse");
490 /* clang-format on */
491}
492
493bool KMountPoint::testFileSystemFlag(FileSystemFlag flag) const
494{
495 /* clang-format off */
496 const bool isMsDos = d->m_mountType == QLatin1String("msdos")
497 || d->m_mountType == QLatin1String("fat")
498 || d->m_mountType == QLatin1String("vfat");
499
500 const bool isNtfs = d->m_mountType.contains(s: QLatin1String("fuse.ntfs"))
501 || d->m_mountType.contains(s: QLatin1String("fuseblk.ntfs"))
502 // fuseblk could really be anything. But its most common use is for NTFS mounts, these days.
503 || d->m_mountType == QLatin1String("fuseblk");
504
505 const bool isSmb = d->m_mountType == QLatin1String("cifs")
506 || d->m_mountType == QLatin1String("smb3")
507 || d->m_mountType == QLatin1String("smbfs")
508 // gvfs-fuse mounted SMB share
509 || d->m_mountType == QLatin1String("smb-share");
510 /* clang-format on */
511
512 switch (flag) {
513 case SupportsChmod:
514 case SupportsChown:
515 case SupportsUTime:
516 case SupportsSymlinks:
517 return !isMsDos && !isNtfs && !isSmb; // it's amazing the number of things Microsoft filesystems don't support :)
518 case CaseInsensitive:
519 return isMsDos;
520 }
521 return false;
522}
523
524KIOCORE_EXPORT QDebug operator<<(QDebug debug, const KMountPoint::Ptr &mp)
525{
526 QDebugStateSaver saver(debug);
527 if (!mp) {
528 debug << "QDebug operator<< called on a null KMountPoint::Ptr";
529 return debug;
530 }
531
532 // clang-format off
533 debug.nospace() << "KMountPoint ["
534 << "Mounted from: " << mp->d->m_mountedFrom
535 << ", device name: " << mp->d->m_device
536 << ", mount point: " << mp->d->m_mountPoint
537 << ", mount type: " << mp->d->m_mountType
538 <<']';
539
540 // clang-format on
541 return debug;
542}
543

source code of kio/src/core/kmountpoint.cpp