qt6-bb10/src/corelib/io/qstorageinfo_linux.cpp

273 lines
8.6 KiB
C++

// 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 "qdiriterator.h"
#include <private/qcore_unix_p.h>
#include <private/qtools_p.h>
#if defined(Q_OS_ANDROID)
# include <sys/mount.h>
# include <sys/vfs.h>
# define QT_STATFS ::statfs
# define QT_STATFSBUF struct statfs
# if !defined(ST_RDONLY)
# define ST_RDONLY 1 // hack for missing define on Android
# endif
#else
# include <sys/statvfs.h>
# if defined(QT_LARGEFILE_SUPPORT)
# define QT_STATFSBUF struct statvfs64
# define QT_STATFS ::statvfs64
# else
# define QT_STATFSBUF struct statvfs
# define QT_STATFS ::statvfs
# endif // QT_LARGEFILE_SUPPORT
#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 QDirIterator devicesByLabel()
{
static const char pathDiskByLabel[] = "/dev/disk/by-label";
static constexpr auto LabelFileFilter =
QDir::AllEntries | QDir::System | QDir::Hidden | QDir::NoDotAndDotDot;
return QDirIterator(QLatin1StringView(pathDiskByLabel), LabelFileFilter);
}
static inline auto retrieveLabels()
{
struct Entry {
QString label;
quint64 deviceId;
};
QList<Entry> result;
QDirIterator it = devicesByLabel();
while (it.hasNext()) {
QFileInfo fileInfo = it.nextFileInfo();
quint64 deviceId = retrieveDeviceId(QFile::encodeName(fileInfo.filePath()));
if (!deviceId)
continue;
result.emplaceBack(Entry{ decodeFsEncString(fileInfo.fileName()), deviceId });
}
return result;
}
static inline QString retrieveLabel(const QByteArray &device, quint64 deviceId)
{
deviceId = retrieveDeviceId(device, deviceId);
if (!deviceId)
return QString();
QDirIterator it = devicesByLabel();
while (it.hasNext()) {
QFileInfo fileInfo = it.nextFileInfo();
QString name = fileInfo.fileName();
if (retrieveDeviceId(QFile::encodeName(fileInfo.filePath())) == deviceId)
return decodeFsEncString(std::move(name));
}
return QString();
}
void QStorageInfoPrivate::doStat()
{
quint64 deviceId = initRootPath();
if (!deviceId)
return;
retrieveVolumeInfo();
name = retrieveLabel(device, deviceId);
}
void QStorageInfoPrivate::retrieveVolumeInfo()
{
QT_STATFSBUF statfs_buf;
int result;
EINTR_LOOP(result, QT_STATFS(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 = statfs_buf.f_bsize;
#if defined(Q_OS_ANDROID)
#if defined(_STATFS_F_FLAGS)
readOnly = (statfs_buf.f_flags & ST_RDONLY) != 0;
#endif
#else
readOnly = (statfs_buf.f_flag & ST_RDONLY) != 0;
#endif
}
}
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);
}
quint64 QStorageInfoPrivate::initRootPath()
{
rootPath = QFileInfo(rootPath).canonicalFilePath();
if (rootPath.isEmpty())
return 0;
std::vector<MountInfo> infos = parseMountInfo();
if (infos.empty()) {
rootPath = u'/';
// Need to return something non-zero here for this unlikely condition.
// Linux currently uses 20 bits for the minor portion[1] in a 32-bit
// integer; glibc, MUSL, and 64-bit Bionic use a 64-bit userspace
// dev_t, so this value will not match a real device from the kernel.
// 32-bit Bionic still has a 32-bit dev_t, but its makedev() macro[2]
// returns 64-bit content too.
// [1] https://codebrowser.dev/linux/linux/include/linux/kdev_t.h.html#_M/MINORBITS
// [2] https://android.googlesource.com/platform/bionic/+/ndk-r19/libc/include/sys/sysmacros.h#39
return makedev(0, -1);
}
// 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)
// But just in case there's a mount --move, we ensure the device ID does
// match.
const QString oldRootPath = std::exchange(rootPath, QString());
const dev_t rootPathDevId = deviceIdForPath(oldRootPath);
for (auto it = infos.rbegin(); it != infos.rend(); ++it) {
if (rootPathDevId != it->stDev || !isParentOf(it->mountPoint, oldRootPath))
continue;
rootPath = std::move(it->mountPoint);
device = std::move(it->device);
fileSystemType = std::move(it->fsType);
subvolume = std::move(it->fsRoot);
return it->stDev;
}
return 0;
}
QList<QStorageInfo> QStorageInfoPrivate::mountedVolumes()
{
std::vector<MountInfo> infos = parseMountInfo(FilterMountInfo::Filtered);
if (infos.empty())
return QList{root()};
auto labelForDevice = [labelMap = retrieveLabels()](const QByteArray &device, quint64 devid) {
devid = retrieveDeviceId(device, devid);
if (!devid)
return QString();
for (auto &[deviceLabel, deviceId] : labelMap) {
if (devid == deviceId)
return deviceLabel;
}
return QString();
};
QList<QStorageInfo> volumes;
for (MountInfo &info : infos) {
QStorageInfoPrivate d(std::move(info));
d.retrieveVolumeInfo();
if (d.bytesTotal <= 0 && d.rootPath != u'/')
continue;
if (info.stDev != deviceIdForPath(d.rootPath))
continue; // probably something mounted over this mountpoint
d.name = labelForDevice(d.device, info.stDev);
volumes.emplace_back(QStorageInfo(*new QStorageInfoPrivate(std::move(d))));
}
return volumes;
}
QT_END_NAMESPACE