QStorageInfo/Linux: use readAll() with /proc/self/mountinfo
This minimizes any multi-threading / file-locking issues as the file is closed once the contents are read. This change assumes /proc/self/mountinfo is available on Linux systems, and doesn't fallback to setmntent(). It's been around since at least Linux Kernel 2.4.0. This requires exporting qstrntoll() for the unittests (using QT_AUTOTEST_EXPORT and wrapping the those unittests in "#ifdef QT_BUILD_INTERNAL"), otherwise linking fails. Fixes: QTBUG-77059 Change-Id: I0363258a9979ea6dadfe5e36c02534ffbd3386c5 Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Axel Spoerl <axel.spoerl@qt.io>
This commit is contained in:
parent
74bd7a7019
commit
543ae6e6a4
253
src/corelib/io/qstorageinfo_linux_p.h
Normal file
253
src/corelib/io/qstorageinfo_linux_p.h
Normal file
@ -0,0 +1,253 @@
|
||||
// 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
|
||||
|
||||
#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 <QtCore/qsystemdetection.h>
|
||||
#include <QtCore/private/qlocale_tools_p.h>
|
||||
|
||||
#include <sys/sysmacros.h> // makedev()
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
static const char MountInfoPath[] = "/proc/self/mountinfo";
|
||||
|
||||
static std::optional<dev_t> 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<QByteArrayView, FieldCount> &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<MountInfo>
|
||||
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<MountInfo> infos;
|
||||
std::array<QByteArrayView, FieldCount> 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<dev_t> 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
|
@ -15,11 +15,14 @@
|
||||
// We mean it.
|
||||
//
|
||||
|
||||
#include <QtCore/qloggingcategory.h>
|
||||
#include <QtCore/private/qglobal_p.h>
|
||||
#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 <typename String>
|
||||
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
|
||||
|
@ -15,6 +15,10 @@
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#if defined(Q_OS_LINUX)
|
||||
# include "qstorageinfo_linux_p.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_BSD4)
|
||||
# include <sys/mount.h>
|
||||
# include <sys/statvfs.h>
|
||||
@ -22,7 +26,9 @@
|
||||
# include <sys/mount.h>
|
||||
# include <sys/vfs.h>
|
||||
# include <mntent.h>
|
||||
#elif defined(Q_OS_LINUX) || defined(Q_OS_HURD)
|
||||
#elif defined(Q_OS_LINUX)
|
||||
# include <sys/statvfs.h>
|
||||
#elif defined(Q_OS_HURD)
|
||||
# include <mntent.h>
|
||||
# include <sys/statvfs.h>
|
||||
# include <sys/sysmacros.h>
|
||||
@ -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/<pid>/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 <typename String>
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
inline QStorageIterator::~QStorageIterator()
|
||||
{
|
||||
if (fp) {
|
||||
if (usingMountinfo)
|
||||
::fclose(fp);
|
||||
else
|
||||
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;
|
||||
}
|
||||
|
||||
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<unsigned>::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<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::initRootPath()
|
||||
{
|
||||
rootPath = QFileInfo(rootPath).canonicalFilePath();
|
||||
if (rootPath.isEmpty())
|
||||
return;
|
||||
|
||||
std::vector<MountInfo> 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<QStorageInfo> QStorageInfoPrivate::mountedVolumes()
|
||||
{
|
||||
std::vector<MountInfo> infos = parseMountInfo(FilterMountInfo::Filtered);
|
||||
if (infos.empty())
|
||||
return QList{root()};
|
||||
|
||||
QList<QStorageInfo> 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<QStorageInfo> QStorageInfoPrivate::mountedVolumes()
|
||||
{
|
||||
QStorageIterator it;
|
||||
@ -840,7 +657,7 @@ QList<QStorageInfo> QStorageInfoPrivate::mountedVolumes()
|
||||
QList<QStorageInfo> volumes;
|
||||
|
||||
while (it.next()) {
|
||||
if (!shouldIncludeFs(it))
|
||||
if (!shouldIncludeFs(it.rootPath(), it.fileSystemType()))
|
||||
continue;
|
||||
|
||||
const QString mountDir = it.rootPath();
|
||||
@ -855,6 +672,7 @@ QList<QStorageInfo> QStorageInfoPrivate::mountedVolumes()
|
||||
|
||||
return volumes;
|
||||
}
|
||||
#endif // defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
|
||||
QStorageInfo QStorageInfoPrivate::root()
|
||||
{
|
||||
|
@ -90,7 +90,8 @@ template <typename UcsInt>
|
||||
return qstrntod(s00, len, se, ok);
|
||||
}
|
||||
|
||||
[[nodiscard]] QSimpleParsedNumber<qlonglong> qstrntoll(const char *nptr, qsizetype size, int base);
|
||||
[[nodiscard]] Q_AUTOTEST_EXPORT
|
||||
QSimpleParsedNumber<qlonglong> qstrntoll(const char *nptr, qsizetype size, int base);
|
||||
[[nodiscard]] QSimpleParsedNumber<qulonglong> qstrntoull(const char *nptr, qsizetype size, int base);
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
@ -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<QByteArray>("line");
|
||||
QTest::addColumn<MountInfo>("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<MountInfo> 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<QByteArray>("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"
|
||||
|
Loading…
Reference in New Issue
Block a user