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/** @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 */
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/** @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 */
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 (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
384QString KMountPoint::mountedFrom() const
385{
386 return d->m_mountedFrom;
387}
388
389dev_t KMountPoint::deviceId() const
390{
391 return d->m_deviceId;
392}
393
394bool KMountPoint::isOnNetwork() const
395{
396 return d->m_isNetFs || isNetfs(mountType: d->m_mountType);
397}
398
399QString KMountPoint::realDeviceName() const
400{
401 return d->m_device;
402}
403
404QString KMountPoint::mountPoint() const
405{
406 return d->m_mountPoint;
407}
408
409QString KMountPoint::mountType() const
410{
411 return d->m_mountType;
412}
413
414QStringList KMountPoint::mountOptions() const
415{
416 return d->m_mountOptions;
417}
418
419KMountPoint::List::List()
420 : QList<Ptr>()
421{
422}
423
424KMountPoint::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
453KMountPoint::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
467bool 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
479bool 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
510KIOCORE_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

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