diff --git a/src/corelib/io/qstorageinfo_linux_p.h b/src/corelib/io/qstorageinfo_linux_p.h new file mode 100644 index 0000000000..a1e4e7e94a --- /dev/null +++ b/src/corelib/io/qstorageinfo_linux_p.h @@ -0,0 +1,253 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// Copyright (C) 2014 Ivan Komissarov +// Copyright (C) 2016 Intel Corporation. +// Copyright (C) 2023 Ahmad Samir +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSTORAGEINFO_LINUX_P_H +#define QSTORAGEINFO_LINUX_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. +// This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qstorageinfo_p.h" + +#include +#include + +#include // makedev() + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +static const char MountInfoPath[] = "/proc/self/mountinfo"; + +static std::optional deviceNumber(QByteArrayView devno) +{ + // major:minor + auto it = devno.cbegin(); + auto r = qstrntoll(it, devno.size(), 10); + if (!r.ok()) + return std::nullopt; + int rdevmajor = int(r.result); + it += r.used; + + if (*it != ':') + return std::nullopt; + + r = qstrntoll(++it, devno.size() - r.used + 1, 10); + if (!r.ok()) + return std::nullopt; + + return makedev(rdevmajor, r.result); +} + +// Helper function to parse paths that the kernel inserts escape sequences +// for. +static QByteArray parseMangledPath(QByteArrayView path) +{ + // The kernel escapes with octal the following characters: + // space ' ', tab '\t', backslash '\\', and newline '\n' + // See: + // https://codebrowser.dev/linux/linux/fs/proc_namespace.c.html#show_mountinfo + // https://codebrowser.dev/linux/linux/fs/seq_file.c.html#mangle_path + + QByteArray ret(path.size(), '\0'); + char *dst = ret.data(); + const char *src = path.data(); + const char *srcEnd = path.data() + path.size(); + while (src != srcEnd) { + switch (*src) { + case ' ': // Shouldn't happen + return {}; + + case '\\': { + // It always uses exactly three octal characters. + ++src; + char c = (*src++ - '0') << 6; + c |= (*src++ - '0') << 3; + c |= (*src++ - '0'); + *dst++ = c; + break; + } + + default: + *dst++ = *src++; + break; + } + } + // If "path" contains any of the characters this method is demangling, + // "ret" would be oversized with extra '\0' characters at the end. + ret.resize(dst - ret.data()); + return ret; +} + +// Indexes into the "fields" std::array in parseMountInfo() +// static constexpr short MountId = 0; +// static constexpr short ParentId = 1; +static constexpr short DevNo = 2; +static constexpr short FsRoot = 3; +static constexpr short MountPoint = 4; +static constexpr short MountOptions = 5; +// static constexpr short OptionalFields = 6; +// static constexpr short Separator = 7; +static constexpr short FsType = 8; +static constexpr short MountSource = 9; +static constexpr short SuperOptions = 10; +static constexpr short FieldCount = 11; + +// Splits a line from /proc/self/mountinfo into fields; fields are separated +// by a single space. +static void tokenizeLine(std::array &fields, QByteArrayView line) +{ + size_t fieldIndex = 0; + qsizetype from = 0; + const char *begin = line.data(); + const qsizetype len = line.size(); + qsizetype spaceIndex = -1; + while ((spaceIndex = line.indexOf(' ', from)) != -1 && fieldIndex < FieldCount) { + fields[fieldIndex] = QByteArrayView{begin + from, begin + spaceIndex}; + from = spaceIndex; + + // Skip "OptionalFields" and Separator fields + if (fieldIndex == MountOptions) { + static constexpr char separatorField[] = " - "; + const qsizetype sepIndex = line.indexOf(separatorField, from); + if (sepIndex == -1) { + qCWarning(lcStorageInfo, + "Malformed line (missing '-' separator field) while parsing '%s':\n%s", + MountInfoPath, line.constData()); + fields.fill({}); + return; + } + + from = sepIndex + strlen(separatorField); + // Continue parsing at FsType field + fieldIndex = FsType; + continue; + } + + if (from + 1 < len) + ++from; // Skip the space at spaceIndex + + ++fieldIndex; + } + + // Currently we don't use the last field, so just check the index + if (fieldIndex != SuperOptions) { + qCInfo(lcStorageInfo, + "Expected %d fields while parsing line from '%s', but found %zu instead:\n%.*s", + FieldCount, MountInfoPath, fieldIndex, int(line.size()), line.data()); + fields.fill({}); + } +} + +namespace { +struct MountInfo { + QString mountPoint; + QByteArray fsType; + QByteArray device; + QByteArray fsRoot; + dev_t stDev = 0; +}; +} + +// parseMountInfo() is called from: +// - QStorageInfoPrivate::initRootPath(), where a list of all mounted volumes is needed +// - QStorageInfoPrivate::mountedVolumes(), where some filesystem types are ignored +// (see shouldIncludefs()) +enum class FilterMountInfo { + All, + Filtered, +}; + +[[maybe_unused]] static std::vector +doParseMountInfo(const QByteArray &mountinfo, FilterMountInfo filter = FilterMountInfo::All) +{ + // https://www.kernel.org/doc/Documentation/filesystems/proc.txt: + // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue + // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) + + auto it = mountinfo.cbegin(); + const auto end = mountinfo.cend(); + auto nextLine = [&it, &end]() -> QByteArrayView { + auto nIt = std::find(it, end, '\n'); + if (nIt != end) { + QByteArrayView ba(it, nIt); + it = ++nIt; // Advance + return ba; + } + return {}; + }; + + std::vector infos; + std::array fields; + QByteArrayView line; + + auto checkField = [&line](QByteArrayView field) { + if (field.isEmpty()) { + qDebug("Failed to parse line from %s:\n%.*s", MountInfoPath, int(line.size()), + line.data()); + return false; + } + return true; + }; + + // mountinfo has a stable format, no empty lines + while (!(line = nextLine()).isEmpty()) { + fields.fill({}); + tokenizeLine(fields, line); + + MountInfo info; + QByteArray mountP = parseMangledPath(fields[MountPoint]); + if (!checkField(mountP)) + continue; + info.mountPoint = QFile::decodeName(mountP); + + if (!checkField(fields[FsType])) + continue; + info.fsType = fields[FsType].toByteArray(); + + if (filter == FilterMountInfo::Filtered && !shouldIncludeFs(info.mountPoint, info.fsType)) + continue; + + std::optional devno = deviceNumber(fields[DevNo]); + if (!devno) { + checkField({}); + continue; + } + info.stDev = *devno; + + QByteArrayView fsRootView = fields[FsRoot]; + if (!checkField(fsRootView)) + continue; + + // If the filesystem root is "/" -- it's not a *sub*-volume/bind-mount, + // in that case we leave info.fsRoot empty + if (fsRootView != "/") { + info.fsRoot = parseMangledPath(fsRootView); + if (!checkField(info.fsRoot)) + continue; + } + + info.device = parseMangledPath(fields[MountSource]); + if (!checkField(info.device)) + continue; + + infos.push_back(std::move(info)); + } + return infos; +} + +QT_END_NAMESPACE + +#endif // QSTORAGEINFO_LINUX_P_H diff --git a/src/corelib/io/qstorageinfo_p.h b/src/corelib/io/qstorageinfo_p.h index af323b4ddc..77a7aa6c7d 100644 --- a/src/corelib/io/qstorageinfo_p.h +++ b/src/corelib/io/qstorageinfo_p.h @@ -15,11 +15,14 @@ // We mean it. // +#include #include #include "qstorageinfo.h" QT_BEGIN_NAMESPACE +inline Q_LOGGING_CATEGORY(lcStorageInfo, "qt.core.qstorageinfo", QtWarningMsg) + class QStorageInfoPrivate : public QSharedData { public: @@ -65,6 +68,58 @@ public: bool valid; }; +// Common helper functions +template +static bool isParentOf(const String &parent, const QString &dirName) +{ + return dirName.startsWith(parent) && + (dirName.size() == parent.size() || dirName.at(parent.size()) == u'/' || + parent.size() == 1); +} + +static inline bool shouldIncludeFs(const QString &mountDir, const QByteArray &fsType) +{ +#if defined(Q_OS_ANDROID) + // "rootfs" is the filesystem type of "/" on Android + static constexpr char RootFsStr[] = ""; +#else + // "rootfs" is a type of ramfs on Linux, used in the initrd on some distros + static constexpr char RootFsStr[] = "rootfs"; +#endif + + using namespace Qt::StringLiterals; + /* + * This function implements a heuristic algorithm to determine whether a + * given mount should be reported to the user. Our objective is to list + * only entries that the end-user would find useful. + * + * We therefore ignore: + * - mounted in /dev, /proc, /sys: special mounts + * (this will catch /sys/fs/cgroup, /proc/sys/fs/binfmt_misc, /dev/pts, + * some of which are tmpfs on Linux) + * - mounted in /var/run or /var/lock: most likely pseudofs + * (on earlier systemd versions, /var/run was a bind-mount of /run, so + * everything would be unnecessarily duplicated) + * - filesystem type is "rootfs": artifact of the root-pivot on some Linux + * initrd + * - if the filesystem total size is zero, it's a pseudo-fs (not checked here). + */ + + if (isParentOf("/dev"_L1, mountDir) + || isParentOf("/proc"_L1, mountDir) + || isParentOf("/sys"_L1, mountDir) + || isParentOf("/var/run"_L1, mountDir) + || isParentOf("/var/lock"_L1, mountDir)) { + return false; + } + + if (!fsType.isEmpty() && fsType == RootFsStr) + return false; + + // size checking in QStorageInfo::mountedVolumes() + return true; +} + QT_END_NAMESPACE #endif // QSTORAGEINFO_P_H diff --git a/src/corelib/io/qstorageinfo_unix.cpp b/src/corelib/io/qstorageinfo_unix.cpp index c58b7ce87d..cdf5315dd1 100644 --- a/src/corelib/io/qstorageinfo_unix.cpp +++ b/src/corelib/io/qstorageinfo_unix.cpp @@ -15,6 +15,10 @@ #include #include +#if defined(Q_OS_LINUX) +# include "qstorageinfo_linux_p.h" +#endif + #if defined(Q_OS_BSD4) # include # include @@ -22,7 +26,9 @@ # include # include # include -#elif defined(Q_OS_LINUX) || defined(Q_OS_HURD) +#elif defined(Q_OS_LINUX) +# include +#elif defined(Q_OS_HURD) # include # include # include @@ -112,37 +118,10 @@ private: QByteArray m_fileSystemType; QByteArray m_device; QByteArray m_options; -#elif defined(Q_OS_LINUX) || defined(Q_OS_HURD) - struct mountinfoent : public mntent { - // Details from proc(5) section from /proc//mountinfo: - //(1) mount ID: a unique ID for the mount (may be reused after umount(2)). - int mount_id; - //(2) parent ID: the ID of the parent mount (or of self for the top of the mount tree). -// int parent_id; - //(3) major:minor: the value of st_dev for files on this filesystem (see stat(2)). - dev_t rdev; - //(4) root: the pathname of the directory in the filesystem which forms the root of this mount. - char *subvolume; - //(5) mount point: the pathname of the mount point relative to the process's root directory. -// char *mnt_dir; // in mntent - //(6) mount options: per-mount options. -// char *mnt_opts; // in mntent - //(7) optional fields: zero or more fields of the form "tag[:value]"; see below. -// int flags; - //(8) separator: the end of the optional fields is marked by a single hyphen. - - //(9) filesystem type: the filesystem type in the form "type[.subtype]". -// char *mnt_type; // in mntent - //(10) mount source: filesystem-specific information or "none". -// char *mnt_fsname; // in mntent - //(11) super options: per-superblock options. - char *superopts; - }; - +#elif defined(Q_OS_HURD) FILE *fp; QByteArray buffer; mountinfoent mnt; - bool usingMountinfo; #elif defined(Q_OS_HAIKU) BVolumeRoster m_volumeRoster; @@ -152,51 +131,6 @@ private: #endif }; -template -static bool isParentOf(const String &parent, const QString &dirName) -{ - return dirName.startsWith(parent) && - (dirName.size() == parent.size() || dirName.at(parent.size()) == u'/' || - parent.size() == 1); -} - -static bool shouldIncludeFs(const QStorageIterator &it) -{ - /* - * This function implements a heuristic algorithm to determine whether a - * given mount should be reported to the user. Our objective is to list - * only entries that the end-user would find useful. - * - * We therefore ignore: - * - mounted in /dev, /proc, /sys: special mounts - * (this will catch /sys/fs/cgroup, /proc/sys/fs/binfmt_misc, /dev/pts, - * some of which are tmpfs on Linux) - * - mounted in /var/run or /var/lock: most likely pseudofs - * (on earlier systemd versions, /var/run was a bind-mount of /run, so - * everything would be unnecessarily duplicated) - * - filesystem type is "rootfs": artifact of the root-pivot on some Linux - * initrd - * - if the filesystem total size is zero, it's a pseudo-fs (not checked here). - */ - - QString mountDir = it.rootPath(); - if (isParentOf("/dev"_L1, mountDir) - || isParentOf("/proc"_L1, mountDir) - || isParentOf("/sys"_L1, mountDir) - || isParentOf("/var/run"_L1, mountDir) - || isParentOf("/var/lock"_L1, mountDir)) { - return false; - } - -#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) - if (it.fileSystemType() == "rootfs") - return false; -#endif - - // size checking in mountedVolumes() - return true; -} - #if defined(Q_OS_BSD4) #ifndef MNT_NOWAIT @@ -350,7 +284,8 @@ inline QByteArray QStorageIterator::subvolume() const { return QByteArray(); } -#elif defined(Q_OS_LINUX) || defined(Q_OS_HURD) + +#elif defined(Q_OS_HURD) static const int bufferSize = 1024; // 2 paths (mount point+device) and metainfo; // should be enough @@ -358,28 +293,13 @@ static const int bufferSize = 1024; // 2 paths (mount point+device) and metainfo inline QStorageIterator::QStorageIterator() : buffer(QByteArray(bufferSize, 0)) { - fp = nullptr; - -#ifdef Q_OS_LINUX - // first, try to open /proc/self/mountinfo, which has more details - fp = ::fopen("/proc/self/mountinfo", "re"); -#endif - if (fp) { - usingMountinfo = true; - } else { - usingMountinfo = false; - fp = ::setmntent(_PATH_MOUNTED, "r"); - } + fp = ::setmntent(_PATH_MOUNTED, "r"); } inline QStorageIterator::~QStorageIterator() { - if (fp) { - if (usingMountinfo) - ::fclose(fp); - else - ::endmntent(fp); - } + if (fp) + ::endmntent(fp); } inline bool QStorageIterator::isValid() const @@ -389,143 +309,7 @@ inline bool QStorageIterator::isValid() const inline bool QStorageIterator::next() { - mnt.subvolume = nullptr; - mnt.superopts = nullptr; - if (!usingMountinfo) - return ::getmntent_r(fp, &mnt, buffer.data(), buffer.size()) != nullptr; - - // Helper function to parse paths that the kernel inserts escape sequences - // for. The unescaped string is left at \a src and is properly - // NUL-terminated. Returns a pointer to the delimiter that terminated the - // path, or nullptr if it failed. - auto parseMangledPath = [](char *src) { - // The kernel escapes with octal the following characters: - // space ' ', tab '\t', backslask '\\', and newline '\n' - char *dst = src; - while (*src) { - switch (*src) { - case ' ': - // Unescaped space: end of the field. - *dst = '\0'; - return src; - - default: - *dst++ = *src++; - break; - - case '\\': - // It always uses exactly three octal characters. - ++src; - char c = (*src++ - '0') << 6; - c |= (*src++ - '0') << 3; - c |= (*src++ - '0'); - *dst++ = c; - break; - } - } - - // Found a NUL before the end of the field. - src = nullptr; - return src; - }; - - char *ptr = buffer.data(); - if (fgets(ptr, buffer.size(), fp) == nullptr) - return false; - - size_t len = strlen(ptr); - if (len == 0) - return false; - while (Q_UNLIKELY(ptr[len - 1] != '\n' && !feof(fp))) { - // buffer wasn't large enough. Enlarge and try again. - // (we're readidng from the kernel, so OOM is unlikely) - buffer.resize((buffer.size() + 4096) & ~4095); - ptr = buffer.data(); - if (fgets(ptr + len, buffer.size() - len, fp) == nullptr) - return false; - - len += strlen(ptr + len); - Q_ASSERT(len < size_t(buffer.size())); - } - ptr[len - 1] = '\0'; - const char *const stop = ptr + len - 1; - - // parse the line - mnt.mnt_freq = 0; - mnt.mnt_passno = 0; - - auto r = qstrntoll(ptr, stop - ptr, 10); - if (!r.ok()) - return false; - mnt.mount_id = r.result; - - ptr += r.used; - r = qstrntoll(ptr, stop - ptr, 10); - if (!r.ok()) - return false; - // parent_id = r.result; // member currently not in use - - ptr += r.used; - r = qstrntoll(ptr, stop - ptr, 10); - if (!r.ok()) - return false; - ptr += r.used; - if (*ptr != ':') - return false; - int rdevmajor = r.result; - ++ptr; // Skip over the ':' - r = qstrntoll(ptr, stop - ptr, 10); - if (!r.ok()) - return false; - mnt.rdev = makedev(rdevmajor, r.result); - - ptr += r.used; - if (*ptr != ' ') - return false; - - mnt.subvolume = ++ptr; - ptr = parseMangledPath(ptr); - if (!ptr) - return false; - - // unset a subvolume of "/" -- it's not a *sub* volume - if (mnt.subvolume + 1 == ptr) - *mnt.subvolume = '\0'; - - mnt.mnt_dir = ++ptr; - ptr = parseMangledPath(ptr); - if (!ptr) - return false; - - mnt.mnt_opts = ++ptr; - ptr = strchr(ptr, ' '); - if (!ptr) - return false; - - // we don't parse the flags, so just find the separator - if (char *const dashed = strstr(ptr, " - ")) { - *ptr = '\0'; - ptr = dashed + strlen(" - ") - 1; - } else { - return false; - } - - mnt.mnt_type = ++ptr; - ptr = strchr(ptr, ' '); - if (!ptr) - return false; - *ptr = '\0'; - - mnt.mnt_fsname = ++ptr; - ptr = parseMangledPath(ptr); - if (!ptr) - return false; - - mnt.superopts = ++ptr; - ptr += strcspn(ptr, " \n"); - *ptr = '\0'; - - return true; + return ::getmntent_r(fp, &mnt, buffer.data(), buffer.size()) != nullptr; } inline QString QStorageIterator::rootPath() const @@ -540,49 +324,17 @@ inline QByteArray QStorageIterator::fileSystemType() const inline QByteArray QStorageIterator::device() const { -#ifdef Q_OS_LINUX - // check that the device exists - if (mnt.mnt_fsname[0] == '/' && access(mnt.mnt_fsname, F_OK) != 0) { - // It doesn't, so let's try to resolve the dev_t from /dev/block. - // Note how strlen("4294967295") == digits10 + 1, so we need to add 1 - // for each number, plus the ':'. - char buf[sizeof("/dev/block/") + 2 * std::numeric_limits::digits10 + 3]; - QByteArray dev(PATH_MAX, Qt::Uninitialized); - char *devdata = dev.data(); - - snprintf(buf, sizeof(buf), "/dev/block/%u:%u", major(mnt.rdev), minor(mnt.rdev)); - if (realpath(buf, devdata)) { - dev.truncate(strlen(devdata)); - return dev; - } - } -#endif return QByteArray(mnt.mnt_fsname); } inline QByteArray QStorageIterator::options() const { - // Merge the two options, starting with the superblock options and letting - // the per-mount options override. - const char *superopts = mnt.superopts; - - // Both mnt_opts and superopts start with "ro" or "rw", so we can skip the - // superblock's field (see show_mountinfo() in fs/proc_namespace.c). - if (superopts && superopts[0] == 'r') { - if (superopts[2] == '\0') // no other superopts besides "ro" / "rw"? - superopts = nullptr; - else if (superopts[2] == ',') - superopts += 3; - } - - if (superopts) - return QByteArray(superopts) + ',' + mnt.mnt_opts; return QByteArray(mnt.mnt_opts); } inline QByteArray QStorageIterator::subvolume() const { - return QByteArray(mnt.subvolume); + return QByteArray(); } #elif defined(Q_OS_HAIKU) inline QStorageIterator::QStorageIterator() @@ -650,6 +402,7 @@ inline QByteArray QStorageIterator::subvolume() const { return QByteArray(); } + #else inline QStorageIterator::QStorageIterator() @@ -696,37 +449,6 @@ inline QByteArray QStorageIterator::subvolume() const } #endif -void QStorageInfoPrivate::initRootPath() -{ - rootPath = QFileInfo(rootPath).canonicalFilePath(); - - if (rootPath.isEmpty()) - return; - - QStorageIterator it; - if (!it.isValid()) { - rootPath = QStringLiteral("/"); - return; - } - - int maxLength = 0; - const QString oldRootPath = rootPath; - rootPath.clear(); - - while (it.next()) { - const QString mountDir = it.rootPath(); - const QByteArray fsName = it.fileSystemType(); - // we try to find most suitable entry - if (isParentOf(mountDir, oldRootPath) && maxLength < mountDir.size()) { - maxLength = mountDir.size(); - rootPath = mountDir; - device = it.device(); - fileSystemType = fsName; - subvolume = it.subvolume(); - } - } -} - #ifdef Q_OS_LINUX // udev encodes the labels with ID_LABEL_FS_ENC which is done with // blkid_encode_string(). Within this function some 1-byte utf-8 @@ -831,6 +553,101 @@ void QStorageInfoPrivate::retrieveVolumeInfo() } } +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + +static std::vector 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::initRootPath() +{ + rootPath = QFileInfo(rootPath).canonicalFilePath(); + if (rootPath.isEmpty()) + return; + + std::vector infos = parseMountInfo(); + if (infos.empty()) { + rootPath = u'/'; + return; + } + + qsizetype maxLength = 0; + const QString oldRootPath = rootPath; + rootPath.clear(); + + for (auto &info : infos) { + // we try to find most suitable entry + qsizetype mpSize = info.mountPoint.size(); + if (isParentOf(info.mountPoint, oldRootPath) && maxLength < mpSize) { + maxLength = mpSize; + rootPath = info.mountPoint; + device = info.device; + fileSystemType = info.fsType; + subvolume = info.fsRoot; + } + } +} + +QList QStorageInfoPrivate::mountedVolumes() +{ + std::vector infos = parseMountInfo(FilterMountInfo::Filtered); + if (infos.empty()) + return QList{root()}; + + QList volumes; + for (MountInfo &info : infos) { + QStorageInfo storage(info.mountPoint); + storage.d->device = info.device; + storage.d->fileSystemType = info.fsType; + storage.d->subvolume = info.fsRoot; + if (storage.bytesTotal() == 0 && storage != root()) + continue; + volumes.push_back(storage); + } + return volumes; +} + +#else // defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + +void QStorageInfoPrivate::initRootPath() +{ + rootPath = QFileInfo(rootPath).canonicalFilePath(); + + if (rootPath.isEmpty()) + return; + + QStorageIterator it; + if (!it.isValid()) { + rootPath = QStringLiteral("/"); + return; + } + + int maxLength = 0; + const QString oldRootPath = rootPath; + rootPath.clear(); + + while (it.next()) { + const QString mountDir = it.rootPath(); + const QByteArray fsName = it.fileSystemType(); + // we try to find most suitable entry + if (isParentOf(mountDir, oldRootPath) && maxLength < mountDir.size()) { + maxLength = mountDir.size(); + rootPath = mountDir; + device = it.device(); + fileSystemType = fsName; + subvolume = it.subvolume(); + } + } +} + QList QStorageInfoPrivate::mountedVolumes() { QStorageIterator it; @@ -840,7 +657,7 @@ QList QStorageInfoPrivate::mountedVolumes() QList volumes; while (it.next()) { - if (!shouldIncludeFs(it)) + if (!shouldIncludeFs(it.rootPath(), it.fileSystemType())) continue; const QString mountDir = it.rootPath(); @@ -855,6 +672,7 @@ QList QStorageInfoPrivate::mountedVolumes() return volumes; } +#endif // defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) QStorageInfo QStorageInfoPrivate::root() { diff --git a/src/corelib/text/qlocale_tools_p.h b/src/corelib/text/qlocale_tools_p.h index 3e1d441f93..df58c88963 100644 --- a/src/corelib/text/qlocale_tools_p.h +++ b/src/corelib/text/qlocale_tools_p.h @@ -90,7 +90,8 @@ template return qstrntod(s00, len, se, ok); } -[[nodiscard]] QSimpleParsedNumber qstrntoll(const char *nptr, qsizetype size, int base); +[[nodiscard]] Q_AUTOTEST_EXPORT +QSimpleParsedNumber qstrntoll(const char *nptr, qsizetype size, int base); [[nodiscard]] QSimpleParsedNumber qstrntoull(const char *nptr, qsizetype size, int base); QT_END_NAMESPACE diff --git a/tests/auto/corelib/io/qstorageinfo/tst_qstorageinfo.cpp b/tests/auto/corelib/io/qstorageinfo/tst_qstorageinfo.cpp index 94acef6bd4..9e280b8c46 100644 --- a/tests/auto/corelib/io/qstorageinfo/tst_qstorageinfo.cpp +++ b/tests/auto/corelib/io/qstorageinfo/tst_qstorageinfo.cpp @@ -9,6 +9,10 @@ #include "../../../../manual/qstorageinfo/printvolumes.cpp" +#ifdef Q_OS_LINUX +# include "../../../../../src/corelib/io/qstorageinfo_linux_p.h" +#endif + class tst_QStorageInfo : public QObject { Q_OBJECT @@ -22,6 +26,13 @@ private slots: void storageList(); void tempFile(); void caching(); + +#if defined(Q_OS_LINUX) && defined(QT_BUILD_INTERNAL) + void testParseMountInfo_data(); + void testParseMountInfo(); + void testParseMountInfo_filtered_data(); + void testParseMountInfo_filtered(); +#endif }; void tst_QStorageInfo::defaultValues() @@ -211,6 +222,155 @@ void tst_QStorageInfo::caching() QCOMPARE_NE(free, storage2.bytesFree()); } +#if defined(Q_OS_LINUX) && defined(QT_BUILD_INTERNAL) +void tst_QStorageInfo::testParseMountInfo_data() +{ + QTest::addColumn("line"); + QTest::addColumn("expected"); + + QTest::newRow("tmpfs") + << "17 25 0:18 / /dev rw,nosuid,relatime shared:2 - tmpfs tmpfs rw,seclabel,mode=755\n"_ba + << MountInfo{"/dev", "tmpfs", "tmpfs", "", makedev(0, 18)}; + QTest::newRow("proc") + << "23 66 0:21 / /proc rw,nosuid,nodev,noexec,relatime shared:12 - proc proc rw\n"_ba + << MountInfo{"/proc", "proc", "proc", "", makedev(0, 21)}; + + // E.g. on Android + QTest::newRow("rootfs") + << "618 618 0:1 / / ro,relatime master:1 - rootfs rootfs ro,seclabel\n"_ba + << MountInfo{"/", "rootfs", "rootfs", "", makedev(0, 1)}; + + QTest::newRow("ext4") + << "47 66 8:3 / /home rw,relatime shared:50 - ext4 /dev/sda3 rw,stripe=32736\n"_ba + << MountInfo{"/home", "ext4", "/dev/sda3", "", makedev(8, 3)}; + + QTest::newRow("empty-optional-field") + << "23 25 0:22 / /apex rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,seclabel,mode=755\n"_ba + << MountInfo{"/apex", "tmpfs", "tmpfs", "", makedev(0, 22)}; + + QTest::newRow("one-optional-field") + << "47 66 8:3 / /home rw,relatime shared:50 - ext4 /dev/sda3 rw,stripe=32736\n"_ba + << MountInfo{"/home", "ext4", "/dev/sda3", "", makedev(8, 3)}; + + QTest::newRow("multiple-optional-fields") + << "47 66 8:3 / /home rw,relatime shared:142 master:111 - ext4 /dev/sda3 rw,stripe=32736\n"_ba + << MountInfo{"/home", "ext4", "/dev/sda3", "", makedev(8, 3)}; + + QTest::newRow("mountdir-with-utf8") + << "129 66 8:51 / /mnt/lab\xC3\xA9l rw,relatime shared:234 - ext4 /dev/sdd3 rw\n"_ba + << MountInfo{"/mnt/labél", "ext4", "/dev/sdd3", "", makedev(8, 51)}; + + QTest::newRow("mountdir-with-space") + << "129 66 8:51 / /mnt/labe\\040l rw,relatime shared:234 - ext4 /dev/sdd3 rw\n"_ba + << MountInfo{"/mnt/labe l", "ext4", "/dev/sdd3", "", makedev(8, 51)}; + + QTest::newRow("mountdir-with-tab") + << "129 66 8:51 / /mnt/labe\\011l rw,relatime shared:234 - ext4 /dev/sdd3 rw\n"_ba + << MountInfo{"/mnt/labe\tl", "ext4", "/dev/sdd3", "", makedev(8, 51)}; + + QTest::newRow("mountdir-with-backslash") + << "129 66 8:51 / /mnt/labe\\134l rw,relatime shared:234 - ext4 /dev/sdd3 rw\n"_ba + << MountInfo{"/mnt/labe\\l", "ext4", "/dev/sdd3", "", makedev(8, 51)}; + + QTest::newRow("mountdir-with-newline") + << "129 66 8:51 / /mnt/labe\\012l rw,relatime shared:234 - ext4 /dev/sdd3 rw\n"_ba + << MountInfo{"/mnt/labe\nl", "ext4", "/dev/sdd3", "", makedev(8, 51)}; + + QTest::newRow("btrfs-subvol") + << "775 503 0:49 /foo/bar / rw,relatime shared:142 master:111 - btrfs " + "/dev/mapper/vg0-stuff rw,ssd,discard,space_cache,subvolid=272,subvol=/foo/bar\n"_ba + << MountInfo{"/", "btrfs", "/dev/mapper/vg0-stuff", "/foo/bar", makedev(0, 49)}; + + QTest::newRow("bind-mount") + << "59 47 8:17 /rpmbuild /home/user/rpmbuild rw,relatime shared:48 - ext4 /dev/sdb1 rw\n"_ba + << MountInfo{"/home/user/rpmbuild", "ext4", "/dev/sdb1", "/rpmbuild", makedev(8, 17)}; + + QTest::newRow("space-dash-space") + << "47 66 8:3 / /home\\040-\\040dir rw,relatime shared:50 - ext4 /dev/sda3 rw,stripe=32736\n"_ba + << MountInfo{"/home - dir", "ext4", "/dev/sda3", "", makedev(8, 3)}; + + QTest::newRow("btrfs-mount-bind-file") + << "1799 1778 0:49 " + "/var_lib_docker/containers/81fde0fec3dd3d99765c3f7fd9cf1ab121b6ffcfd05d5d7ff434db933fe9d795/resolv.conf " + "/etc/resolv.conf rw,relatime - btrfs /dev/mapper/vg0-stuff " + "rw,ssd,discard,space_cache,subvolid=1773,subvol=/var_lib_docker\n"_ba + << MountInfo{"/etc/resolv.conf", "btrfs", "/dev/mapper/vg0-stuff", + "/var_lib_docker/containers/81fde0fec3dd3d99765c3f7fd9cf1ab121b6ffcfd05d5d7ff434db933fe9d795/resolv.conf", + makedev(0, 49)}; + + QTest::newRow("very-long-line-QTBUG-77059") + << "727 26 0:52 / " + "/var/lib/docker/overlay2/f3fbad5eedef71145f00729f0826ea8c44defcfec8c92c58aee0aa2c5ea3fa3a/merged " + "rw,relatime shared:399 - overlay overlay " + "rw,lowerdir=/var/lib/docker/overlay2/l/PUP2PIY4EQLAOEDQOZ56BHVE53:" + "/var/lib/docker/overlay2/l/6IIID3C6J3SUXZEA3GJXKQSTLD:" + "/var/lib/docker/overlay2/l/PA6N6URNR7XDBBGGOSFWSFQ2CG:" + "/var/lib/docker/overlay2/l/5EOMBTZNCPOCE4LM3I4JCTNSTT:" + "/var/lib/docker/overlay2/l/DAMINQ46P3LKX2GDDDIWQKDIWC:" + "/var/lib/docker/overlay2/l/DHR3N57AEH4OG5QER5XJW2LXIN:" + "/var/lib/docker/overlay2/l/NW26KA7QPRS2KSVQI77QJWLMHW," + "upperdir=/var/lib/docker/overlay2/f3fbad5eedef71145f00729f0826ea8c44defcfec8c92c58aee0aa2c5ea3fa3a/diff," + "workdir=/var/lib/docker/overlay2/f3fbad5eedef71145f00729f0826ea8c44defcfec8c92c58aee0aa2c5ea3fa3a/work," + "index=off,xino=off\n"_ba + << MountInfo{"/var/lib/docker/overlay2/f3fbad5eedef71145f00729f0826ea8c44defcfec8c92c58aee0aa2c5ea3fa3a/merged", + "overlay", "overlay", "", makedev(0, 52)}; + + QTest::newRow("sshfs-src-device-not-start-with-slash") + << "128 92 0:64 / /mnt-point rw,nosuid,nodev,relatime shared:234 - " + "fuse.sshfs admin@192.168.1.2:/storage/emulated/0 rw,user_id=1000,group_id=1000\n"_ba + << MountInfo{"/mnt-point", "fuse.sshfs", + "admin@192.168.1.2:/storage/emulated/0", "", makedev(0, 64)}; +} + +void tst_QStorageInfo::testParseMountInfo() +{ + QFETCH(QByteArray, line); + QFETCH(MountInfo, expected); + + const std::vector result = doParseMountInfo(line); + QVERIFY(!result.empty()); + const MountInfo &a = result.front(); + QCOMPARE(a.mountPoint, expected.mountPoint); + QCOMPARE(a.fsType, expected.fsType); + QCOMPARE(a.device, expected.device); + QCOMPARE(a.fsRoot, expected.fsRoot); + QCOMPARE(a.stDev, expected.stDev); +} + +void tst_QStorageInfo::testParseMountInfo_filtered_data() +{ + QTest::addColumn("line"); + + QTest::newRow("proc") + << "23 66 0:21 / /proc rw,nosuid,nodev,noexec,relatime shared:12 - proc proc rw\n"_ba; + + QTest::newRow("sys") + << "24 66 0:22 / /sys rw,nosuid,nodev,noexec,relatime shared:2 - sysfs sysfs rw\n"_ba; + QTest::newRow("sys-kernel") + << "26 24 0:6 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime " + "shared:3 - securityfs securityfs rw\n"_ba; + + QTest::newRow("dev") + << "25 66 0:5 / /dev rw,nosuid shared:8 - devtmpfs devtmpfs " + "rw,size=4096k,nr_inodes=8213017,mode=755,inode64\n"_ba; + QTest::newRow("dev-shm") + << "27 25 0:23 / /dev/shm rw,nosuid,nodev shared:9 - tmpfs tmpfs rw,inode64\n"_ba; + + QTest::newRow("var-run") + << "46 28 0:25 / /var/run rw,nosuid,nodev,noexec,relatime shared:1 - " + "tmpfs tmpfs rw,size=32768k,mode=755,inode64\n"_ba; + QTest::newRow("var-lock") + << "46 28 0:25 / /var/lock rw,nosuid,nodev,noexec,relatime shared:1 - " + "tmpfs tmpfs rw,size=32768k,mode=755,inode64\n"_ba; +} +void tst_QStorageInfo::testParseMountInfo_filtered() +{ + QFETCH(QByteArray, line); + QVERIFY(doParseMountInfo(line, FilterMountInfo::Filtered).empty()); +} + +#endif // Q_OS_LINUX + QTEST_MAIN(tst_QStorageInfo) #include "tst_qstorageinfo.moc"