summaryrefslogtreecommitdiffstats
path: root/src/corelib/io/qstorageinfo_linux.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/corelib/io/qstorageinfo_linux.cpp')
-rw-r--r--src/corelib/io/qstorageinfo_linux.cpp288
1 files changed, 288 insertions, 0 deletions
diff --git a/src/corelib/io/qstorageinfo_linux.cpp b/src/corelib/io/qstorageinfo_linux.cpp
new file mode 100644
index 0000000000..a9d46e7395
--- /dev/null
+++ b/src/corelib/io/qstorageinfo_linux.cpp
@@ -0,0 +1,288 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// Copyright (C) 2014 Ivan Komissarov <ABBAPOH@gmail.com>
+// Copyright (C) 2016 Intel Corporation.
+// Copyright (C) 2023 Ahmad Samir <a.samirh78@gmail.com>
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qstorageinfo_linux_p.h"
+
+#include "qdirlisting.h"
+#include <private/qcore_unix_p.h>
+#include <private/qtools_p.h>
+
+#include <q20memory.h>
+
+#include <sys/ioctl.h>
+#include <sys/statfs.h>
+
+// so we don't have to #include <linux/fs.h>, which is known to cause conflicts
+#ifndef FSLABEL_MAX
+# define FSLABEL_MAX 256
+#endif
+#ifndef FS_IOC_GETFSLABEL
+# define FS_IOC_GETFSLABEL _IOR(0x94, 49, char[FSLABEL_MAX])
+#endif
+
+// or <linux/statfs.h>
+#ifndef ST_RDONLY
+# define ST_RDONLY 0x0001 /* mount read-only */
+#endif
+
+QT_BEGIN_NAMESPACE
+
+using namespace Qt::StringLiterals;
+
+// udev encodes the labels with ID_LABEL_FS_ENC which is done with
+// blkid_encode_string(). Within this function some 1-byte utf-8
+// characters not considered safe (e.g. '\' or ' ') are encoded as hex
+static QString decodeFsEncString(QString &&str)
+{
+ using namespace QtMiscUtils;
+ qsizetype start = str.indexOf(u'\\');
+ if (start < 0)
+ return std::move(str);
+
+ // decode in-place
+ QString decoded = std::move(str);
+ auto ptr = reinterpret_cast<char16_t *>(decoded.begin());
+ qsizetype in = start;
+ qsizetype out = start;
+ qsizetype size = decoded.size();
+
+ while (in < size) {
+ Q_ASSERT(ptr[in] == u'\\');
+ if (size - in >= 4 && ptr[in + 1] == u'x') { // we need four characters: \xAB
+ int c = fromHex(ptr[in + 2]) << 4;
+ c |= fromHex(ptr[in + 3]);
+ if (Q_UNLIKELY(c < 0))
+ c = QChar::ReplacementCharacter; // bad hex sequence
+ ptr[out++] = c;
+ in += 4;
+ }
+
+ for ( ; in < size; ++in) {
+ char16_t c = ptr[in];
+ if (c == u'\\')
+ break;
+ ptr[out++] = c;
+ }
+ }
+ decoded.resize(out);
+ return decoded;
+}
+
+static inline dev_t deviceIdForPath(const QString &device)
+{
+ QT_STATBUF st;
+ if (QT_STAT(QFile::encodeName(device), &st) < 0)
+ return 0;
+ return st.st_dev;
+}
+
+static inline quint64 retrieveDeviceId(const QByteArray &device, quint64 deviceId = 0)
+{
+ // major = 0 implies an anonymous block device, so we need to stat() the
+ // actual device to get its dev_t. This is required for btrfs (and possibly
+ // others), which always uses them for all the subvolumes (including the
+ // root):
+ // https://codebrowser.dev/linux/linux/fs/btrfs/disk-io.c.html#btrfs_init_fs_root
+ // https://codebrowser.dev/linux/linux/fs/super.c.html#get_anon_bdev
+ // For everything else, we trust the parameter.
+ if (major(deviceId) != 0)
+ return deviceId;
+
+ // don't even try to stat() a relative path or "/"
+ if (device.size() < 2 || !device.startsWith('/'))
+ return 0;
+
+ QT_STATBUF st;
+ if (QT_STAT(device, &st) < 0)
+ return 0;
+ if (!S_ISBLK(st.st_mode))
+ return 0;
+ return st.st_rdev;
+}
+
+static QDirListing devicesByLabel()
+{
+ static const char pathDiskByLabel[] = "/dev/disk/by-label";
+ static constexpr auto LabelFileFilter =
+ QDir::AllEntries | QDir::System | QDir::Hidden | QDir::NoDotAndDotDot;
+
+ return QDirListing(QLatin1StringView(pathDiskByLabel), LabelFileFilter);
+}
+
+static inline auto retrieveLabels()
+{
+ struct Entry {
+ QString label;
+ quint64 deviceId;
+ };
+ QList<Entry> result;
+
+ for (const auto &dirEntry : devicesByLabel()) {
+ quint64 deviceId = retrieveDeviceId(QFile::encodeName(dirEntry.filePath()));
+ if (!deviceId)
+ continue;
+ result.emplaceBack(Entry{ decodeFsEncString(dirEntry.fileName()), deviceId });
+ }
+ return result;
+}
+
+static std::optional<QString> retrieveLabelViaIoctl(const QString &path)
+{
+ // FS_IOC_GETFSLABEL was introduced in v4.18; previously it was btrfs-specific.
+ int fd = qt_safe_open(QFile::encodeName(path).constData(), QT_OPEN_RDONLY);
+ if (fd < 0)
+ return std::nullopt;
+
+ // Note: it doesn't append the null terminator (despite what the man page
+ // says) and the return code on success (0) does not indicate the length.
+ char label[FSLABEL_MAX] = {};
+ int r = ioctl(fd, FS_IOC_GETFSLABEL, &label);
+ close(fd);
+ if (r < 0)
+ return std::nullopt;
+ return QString::fromUtf8(label);
+}
+
+static inline QString retrieveLabel(const QStorageInfoPrivate &d, quint64 deviceId)
+{
+ if (auto label = retrieveLabelViaIoctl(d.rootPath))
+ return *label;
+
+ deviceId = retrieveDeviceId(d.device, deviceId);
+ if (!deviceId)
+ return QString();
+
+ for (const auto &dirEntry : devicesByLabel()) {
+ if (retrieveDeviceId(QFile::encodeName(dirEntry.filePath())) == deviceId)
+ return decodeFsEncString(dirEntry.fileName());
+ }
+ return QString();
+}
+
+void QStorageInfoPrivate::retrieveVolumeInfo()
+{
+ struct statfs64 statfs_buf;
+ int result;
+ QT_EINTR_LOOP(result, statfs64(QFile::encodeName(rootPath).constData(), &statfs_buf));
+ if (result == 0) {
+ valid = true;
+ ready = true;
+
+ bytesTotal = statfs_buf.f_blocks * statfs_buf.f_frsize;
+ bytesFree = statfs_buf.f_bfree * statfs_buf.f_frsize;
+ bytesAvailable = statfs_buf.f_bavail * statfs_buf.f_frsize;
+ blockSize = int(statfs_buf.f_bsize);
+ readOnly = (statfs_buf.f_flags & ST_RDONLY) != 0;
+ }
+}
+
+static std::vector<MountInfo> parseMountInfo(FilterMountInfo filter = FilterMountInfo::All)
+{
+ QFile file(u"/proc/self/mountinfo"_s);
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
+ return {};
+
+ QByteArray mountinfo = file.readAll();
+ file.close();
+
+ return doParseMountInfo(mountinfo, filter);
+}
+
+void QStorageInfoPrivate::doStat()
+{
+ retrieveVolumeInfo();
+ if (!ready)
+ return;
+
+ rootPath = QFileInfo(rootPath).canonicalFilePath();
+ if (rootPath.isEmpty())
+ return;
+
+ std::vector<MountInfo> infos = parseMountInfo();
+ if (infos.empty()) {
+ rootPath = u'/';
+ return;
+ }
+
+ // We iterate over the /proc/self/mountinfo list backwards because then any
+ // matching isParentOf must be the actual mount point because it's the most
+ // recent mount on that path. Linux does allow mounting over non-empty
+ // directories, such as in:
+ // # mount | tail -2
+ // tmpfs on /tmp/foo/bar type tmpfs (rw,relatime,inode64)
+ // tmpfs on /tmp/foo type tmpfs (rw,relatime,inode64)
+ //
+ // We try to match the device ID in case there's a mount --move.
+ // We can't *rely* on it because some filesystems like btrfs will assign
+ // device IDs to subvolumes that aren't listed in /proc/self/mountinfo.
+
+ const QString oldRootPath = std::exchange(rootPath, QString());
+ const dev_t rootPathDevId = deviceIdForPath(oldRootPath);
+ MountInfo *best = nullptr;
+ for (auto it = infos.rbegin(); it != infos.rend(); ++it) {
+ if (!isParentOf(it->mountPoint, oldRootPath))
+ continue;
+ if (rootPathDevId == it->stDev) {
+ // device ID matches; this is definitely the best option
+ best = q20::to_address(it);
+ break;
+ }
+ if (!best) {
+ // if we can't find a device ID match, this parent path is probably
+ // the correct one
+ best = q20::to_address(it);
+ }
+ }
+ if (best) {
+ auto stDev = best->stDev;
+ setFromMountInfo(std::move(*best));
+ name = retrieveLabel(*this, stDev);
+ }
+}
+
+QList<QStorageInfo> QStorageInfoPrivate::mountedVolumes()
+{
+ std::vector<MountInfo> infos = parseMountInfo(FilterMountInfo::Filtered);
+ if (infos.empty())
+ return QList{root()};
+
+ std::optional<decltype(retrieveLabels())> labelMap;
+ auto labelForDevice = [&labelMap](const QStorageInfoPrivate &d, quint64 devid) {
+ if (d.fileSystemType == "tmpfs")
+ return QString();
+
+ if (auto label = retrieveLabelViaIoctl(d.rootPath))
+ return *label;
+
+ devid = retrieveDeviceId(d.device, devid);
+ if (!devid)
+ return QString();
+
+ if (!labelMap)
+ labelMap = retrieveLabels();
+ for (auto &[deviceLabel, deviceId] : std::as_const(*labelMap)) {
+ if (devid == deviceId)
+ return deviceLabel;
+ }
+ return QString();
+ };
+
+ QList<QStorageInfo> volumes;
+ for (MountInfo &info : infos) {
+ const auto infoStDev = info.stDev;
+ QStorageInfoPrivate d(std::move(info));
+ d.retrieveVolumeInfo();
+ if (d.bytesTotal <= 0 && d.rootPath != u'/')
+ continue;
+ if (infoStDev != deviceIdForPath(d.rootPath))
+ continue; // probably something mounted over this mountpoint
+ d.name = labelForDevice(d, infoStDev);
+ volumes.emplace_back(QStorageInfo(*new QStorageInfoPrivate(std::move(d))));
+ }
+ return volumes;
+}
+
+QT_END_NAMESPACE