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