123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 | // 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 <private/qcore_unix_p.h>#include <private/qlocale_tools_p.h>#include <private/qtools_p.h>#include <QtCore/qdirlisting.h>#include <QtCore/qsystemdetection.h>#include <q20memory.h>#include <sys/ioctl.h>#include <sys/stat.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#if defined(Q_OS_ANDROID)// statx() is disabled on Android because quite a few systems// come with sandboxes that kill applications that make system calls outside a// whitelist and several Android vendors can't be bothered to update the list.# undef STATX_BASIC_STATS#include <private/qjnihelpers_p.h>#endif QT_BEGIN_NAMESPACE using namespaceQt::StringLiterals;static const char MountInfoPath[] ="/proc/self/mountinfo";staticstd::optional<dev_t>deviceNumber(QByteArrayView devno){// major:minorauto it = devno.cbegin();auto r =qstrntoll(it, devno.size(),10);if(!r.ok())returnstd::nullopt;int rdevmajor =int(r.result); it += r.used;if(*it !=':')returnstd::nullopt; r =qstrntoll(++it, devno.size() - r.used +1,10);if(!r.ok())returnstd::nullopt;returnmakedev(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 happenreturn{};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()staticconstexprshort MountId =0;// static constexpr short ParentId = 1;staticconstexprshort DevNo =2;staticconstexprshort FsRoot =3;staticconstexprshort MountPoint =4;staticconstexprshort MountOptions =5;// static constexpr short OptionalFields = 6;// static constexpr short Separator = 7;staticconstexprshort FsType =8;staticconstexprshort MountSource =9;staticconstexprshort SuperOptions =10;staticconstexprshort FieldCount =11;// Splits a line from /proc/self/mountinfo into fields; fields are separated// by a single space.static voidtokenizeLine(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 fieldsif(fieldIndex == MountOptions) {staticconstexprchar 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 indexif(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({});}}std::vector<MountInfo>doParseMountInfo(const QByteArray &mountinfo, FilterMountInfo filter){// 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();constauto end = mountinfo.cend();auto nextLine = [&it, &end]() -> QByteArrayView {auto nIt =std::find(it, end,'\n');if(nIt != end) { QByteArrayView ba(it, nIt); it = ++nIt;// Advancereturn 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 lineswhile(!(line =nextLine()).isEmpty()) { fields.fill({});tokenizeLine(fields, line); MountInfo info;if(auto r =qstrntoll(fields[MountId].data(), fields[MountId].size(),10); r.ok()) { info.mntid = r.result;}else{checkField({});continue;} 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 && !QStorageInfoPrivate::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 emptyif(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;}namespace{struct AutoFileDescriptor {int fd = -1;AutoFileDescriptor(const QString &path,int mode = QT_OPEN_RDONLY):fd(qt_safe_open(QFile::encodeName(path), mode)){}~AutoFileDescriptor() {if(fd >=0)qt_safe_close(fd); }operatorint()const noexcept {return fd; }};}// 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 hexstatic QString decodeFsEncString(QString &&str){using namespace QtMiscUtils; qsizetype start = str.indexOf(u'\\');if(start <0)returnstd::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: \xABint 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;}staticinline dev_t deviceIdForPath(const QString &device){ QT_STATBUF st;if(QT_STAT(QFile::encodeName(device), &st) <0)return0;return st.st_dev;}staticinline quint64 mountIdForPath(int fd){if(fd <0)return0;#if defined(STATX_BASIC_STATS) && defined(STATX_MNT_ID)// STATX_MNT_ID was added in kernel v5.8struct statx st;int r =statx(fd,"", AT_EMPTY_PATH | AT_NO_AUTOMOUNT, STATX_MNT_ID, &st);if(r ==0&& (st.stx_mask & STATX_MNT_ID))return st.stx_mnt_id;#endifreturn0;}staticinline 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('/'))return0; QT_STATBUF st;if(QT_STAT(device, &st) <0)return0;if(!S_ISBLK(st.st_mode))return0;return st.st_rdev;}static QDirListing devicesByLabel(){static const char pathDiskByLabel[] ="/dev/disk/by-label";staticconstexpr auto LabelFileFilter =QDirListing::IteratorFlag::IncludeHidden;returnQDirListing(QLatin1StringView(pathDiskByLabel), LabelFileFilter);}staticinline autoretrieveLabels(){struct Entry { QString label; quint64 deviceId;}; QList<Entry> result;for(constauto&dirEntry :devicesByLabel()) { quint64 deviceId =retrieveDeviceId(QFile::encodeName(dirEntry.filePath()));if(!deviceId)continue; result.emplaceBack(Entry{decodeFsEncString(dirEntry.fileName()), deviceId });}return result;}staticstd::optional<QString>retrieveLabelViaIoctl(int fd){// FS_IOC_GETFSLABEL was introduced in v4.18; previously it was btrfs-specific.if(fd <0)returnstd::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);if(r <0)returnstd::nullopt;returnQString::fromUtf8(label);}staticinline QString retrieveLabel(const QStorageInfoPrivate &d,int fd, quint64 deviceId){if(auto label =retrieveLabelViaIoctl(fd))return*label; deviceId =retrieveDeviceId(d.device, deviceId);if(!deviceId)returnQString();for(constauto&dirEntry :devicesByLabel()) {if(retrieveDeviceId(QFile::encodeName(dirEntry.filePath())) == deviceId)returndecodeFsEncString(dirEntry.fileName());}returnQString();}voidQStorageInfoPrivate::retrieveVolumeInfo(){struct statfs64 statfs_buf;int result;QT_EINTR_LOOP(result,statfs64(QFile::encodeName(rootPath).constData(), &statfs_buf)); valid = ready = (result ==0);if(valid) { 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;}}staticstd::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();returndoParseMountInfo(mountinfo, filter);}voidQStorageInfoPrivate::doStat(){#ifdef Q_OS_ANDROIDif(QtAndroidPrivate::isUncompressedNativeLibs()) {// We need to pass the actual file path on the file system to statfs64 QString possibleApk =QtAndroidPrivate::resolveApkPath(rootPath);if(!possibleApk.isEmpty()) rootPath = possibleApk;}#endifretrieveVolumeInfo();if(!ready)return; rootPath =QFileInfo(rootPath).canonicalFilePath();if(rootPath.isEmpty())return;std::vector<MountInfo> infos =parseMountInfo();if(infos.empty()) { rootPath = u'/';return;} MountInfo *best =nullptr; AutoFileDescriptor fd(rootPath);if(quint64 mntid =mountIdForPath(fd)) {// We have the mount ID for this path, so find the matching line.auto it =std::find_if(infos.begin(), infos.end(),[mntid](const MountInfo &info) {return info.mntid == mntid; });if(it != infos.end()) best =q20::to_address(it);}else{// We have failed to get the mount ID for this path, usually because// the path cannot be open()ed by this user (e.g., /root), so we fall// back to a string search.// 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);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, fd, 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,int fd, quint64 devid) {if(d.fileSystemType =="tmpfs")returnQString();if(auto label =retrieveLabelViaIoctl(fd))return*label; devid =retrieveDeviceId(d.device, devid);if(!devid)returnQString();if(!labelMap) labelMap =retrieveLabels();for(auto&[deviceLabel, deviceId] :std::as_const(*labelMap)) {if(devid == deviceId)return deviceLabel;}returnQString();}; QList<QStorageInfo> volumes; volumes.reserve(infos.size());for(auto it = infos.begin(); it != infos.end(); ++it) { MountInfo &info = *it; AutoFileDescriptor fd(info.mountPoint);// find out if the path as we see it matches this line from mountinfo quint64 mntid =mountIdForPath(fd);if(mntid ==0) {// statx failed, so scan the later lines to see if any is a parent// to thisauto isParent = [&info](const MountInfo &maybeParent) {returnisParentOf(maybeParent.mountPoint, info.mountPoint);};if(std::find_if(it +1, infos.end(), isParent) != infos.end())continue;}else if(mntid != info.mntid) {continue;}constauto infoStDev = info.stDev; QStorageInfoPrivate d(std::move(info)); d.retrieveVolumeInfo();if(d.bytesTotal <=0&& d.rootPath != u'/')continue; d.name =labelForDevice(d, fd, infoStDev); volumes.emplace_back(QStorageInfo(*newQStorageInfoPrivate(std::move(d))));}return volumes;} QT_END_NAMESPACE
|