| 1 | // Copyright (C) 2021 The Qt Company Ltd. | 
| 2 | // Copyright (C) 2014 Ivan Komissarov <ABBAPOH@gmail.com> | 
| 3 | // Copyright (C) 2016 Intel Corporation. | 
| 4 | // Copyright (C) 2023 Ahmad Samir <a.samirh78@gmail.com> | 
| 5 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only | 
| 6 |  | 
| 7 | #include "qstorageinfo_linux_p.h" | 
| 8 |  | 
| 9 | #include "qdirlisting.h" | 
| 10 | #include <private/qcore_unix_p.h> | 
| 11 | #include <private/qtools_p.h> | 
| 12 |  | 
| 13 | #include <q20memory.h> | 
| 14 |  | 
| 15 | #include <sys/ioctl.h> | 
| 16 | #include <sys/stat.h> | 
| 17 | #include <sys/statfs.h> | 
| 18 |  | 
| 19 | // so we don't have to #include <linux/fs.h>, which is known to cause conflicts | 
| 20 | #ifndef FSLABEL_MAX | 
| 21 | #  define FSLABEL_MAX           256 | 
| 22 | #endif | 
| 23 | #ifndef FS_IOC_GETFSLABEL | 
| 24 | #  define FS_IOC_GETFSLABEL     _IOR(0x94, 49, char[FSLABEL_MAX]) | 
| 25 | #endif | 
| 26 |  | 
| 27 | // or <linux/statfs.h> | 
| 28 | #ifndef ST_RDONLY | 
| 29 | #  define ST_RDONLY             0x0001  /* mount read-only */ | 
| 30 | #endif | 
| 31 |  | 
| 32 | #if defined(Q_OS_ANDROID) | 
| 33 | // statx() is disabled on Android because quite a few systems | 
| 34 | // come with sandboxes that kill applications that make system calls outside a | 
| 35 | // whitelist and several Android vendors can't be bothered to update the list. | 
| 36 | #  undef STATX_BASIC_STATS | 
| 37 | #endif | 
| 38 |  | 
| 39 | QT_BEGIN_NAMESPACE | 
| 40 |  | 
| 41 | using namespace Qt::StringLiterals; | 
| 42 |  | 
| 43 | namespace { | 
| 44 | struct AutoFileDescriptor | 
| 45 | { | 
| 46 |     int fd = -1; | 
| 47 |     AutoFileDescriptor(const QString &path, int mode = QT_OPEN_RDONLY) | 
| 48 |         : fd(qt_safe_open(pathname: QFile::encodeName(fileName: path), flags: mode)) | 
| 49 |     {} | 
| 50 |     ~AutoFileDescriptor() { if (fd >= 0) qt_safe_close(fd); } | 
| 51 |     operator int() const noexcept { return fd; } | 
| 52 | }; | 
| 53 | } | 
| 54 |  | 
| 55 | // udev encodes the labels with ID_LABEL_FS_ENC which is done with | 
| 56 | // blkid_encode_string(). Within this function some 1-byte utf-8 | 
| 57 | // characters not considered safe (e.g. '\' or ' ') are encoded as hex | 
| 58 | static QString decodeFsEncString(QString &&str) | 
| 59 | { | 
| 60 |     using namespace QtMiscUtils; | 
| 61 |     qsizetype start = str.indexOf(c: u'\\'); | 
| 62 |     if (start < 0) | 
| 63 |         return std::move(str); | 
| 64 |  | 
| 65 |     // decode in-place | 
| 66 |     QString decoded = std::move(str); | 
| 67 |     auto ptr = reinterpret_cast<char16_t *>(decoded.begin()); | 
| 68 |     qsizetype in = start; | 
| 69 |     qsizetype out = start; | 
| 70 |     qsizetype size = decoded.size(); | 
| 71 |  | 
| 72 |     while (in < size) { | 
| 73 |         Q_ASSERT(ptr[in] == u'\\'); | 
| 74 |         if (size - in >= 4 && ptr[in + 1] == u'x') {    // we need four characters: \xAB | 
| 75 |             int c = fromHex(c: ptr[in + 2]) << 4; | 
| 76 |             c |= fromHex(c: ptr[in + 3]); | 
| 77 |             if (Q_UNLIKELY(c < 0)) | 
| 78 |                 c = QChar::ReplacementCharacter;        // bad hex sequence | 
| 79 |             ptr[out++] = c; | 
| 80 |             in += 4; | 
| 81 |         } | 
| 82 |  | 
| 83 |         for ( ; in < size; ++in) { | 
| 84 |             char16_t c = ptr[in]; | 
| 85 |             if (c == u'\\') | 
| 86 |                 break; | 
| 87 |             ptr[out++] = c; | 
| 88 |         } | 
| 89 |     } | 
| 90 |     decoded.resize(size: out); | 
| 91 |     return decoded; | 
| 92 | } | 
| 93 |  | 
| 94 | static inline dev_t deviceIdForPath(const QString &device) | 
| 95 | { | 
| 96 |     QT_STATBUF st; | 
| 97 |     if (QT_STAT(file: QFile::encodeName(fileName: device), buf: &st) < 0) | 
| 98 |         return 0; | 
| 99 |     return st.st_dev; | 
| 100 | } | 
| 101 |  | 
| 102 | static inline quint64 mountIdForPath(int fd) | 
| 103 | { | 
| 104 |     if (fd < 0) | 
| 105 |         return 0; | 
| 106 | #if defined(STATX_BASIC_STATS) && defined(STATX_MNT_ID) | 
| 107 |     // STATX_MNT_ID was added in kernel v5.8 | 
| 108 |     struct statx st; | 
| 109 |     int r = statx(dirfd: fd, path: "" , AT_EMPTY_PATH | AT_NO_AUTOMOUNT, STATX_MNT_ID, buf: &st); | 
| 110 |     if (r == 0 && (st.stx_mask & STATX_MNT_ID)) | 
| 111 |         return st.stx_mnt_id; | 
| 112 | #endif | 
| 113 |     return 0; | 
| 114 | } | 
| 115 |  | 
| 116 | static inline quint64 retrieveDeviceId(const QByteArray &device, quint64 deviceId = 0) | 
| 117 | { | 
| 118 |     // major = 0 implies an anonymous block device, so we need to stat() the | 
| 119 |     // actual device to get its dev_t. This is required for btrfs (and possibly | 
| 120 |     // others), which always uses them for all the subvolumes (including the | 
| 121 |     // root): | 
| 122 |     // https://codebrowser.dev/linux/linux/fs/btrfs/disk-io.c.html#btrfs_init_fs_root | 
| 123 |     // https://codebrowser.dev/linux/linux/fs/super.c.html#get_anon_bdev | 
| 124 |     // For everything else, we trust the parameter. | 
| 125 |     if (major(deviceId) != 0) | 
| 126 |         return deviceId; | 
| 127 |  | 
| 128 |     // don't even try to stat() a relative path or "/" | 
| 129 |     if (device.size() < 2 || !device.startsWith(c: '/')) | 
| 130 |         return 0; | 
| 131 |  | 
| 132 |     QT_STATBUF st; | 
| 133 |     if (QT_STAT(file: device, buf: &st) < 0) | 
| 134 |         return 0; | 
| 135 |     if (!S_ISBLK(st.st_mode)) | 
| 136 |         return 0; | 
| 137 |     return st.st_rdev; | 
| 138 | } | 
| 139 |  | 
| 140 | static QDirListing devicesByLabel() | 
| 141 | { | 
| 142 |     static const char pathDiskByLabel[] = "/dev/disk/by-label" ; | 
| 143 |     static constexpr auto LabelFileFilter = QDirListing::IteratorFlag::IncludeHidden; | 
| 144 |     return QDirListing(QLatin1StringView(pathDiskByLabel), LabelFileFilter); | 
| 145 | } | 
| 146 |  | 
| 147 | static inline auto retrieveLabels() | 
| 148 | { | 
| 149 |     struct Entry { | 
| 150 |         QString label; | 
| 151 |         quint64 deviceId; | 
| 152 |     }; | 
| 153 |     QList<Entry> result; | 
| 154 |  | 
| 155 |     for (const auto &dirEntry : devicesByLabel()) { | 
| 156 |         quint64 deviceId = retrieveDeviceId(device: QFile::encodeName(fileName: dirEntry.filePath())); | 
| 157 |         if (!deviceId) | 
| 158 |             continue; | 
| 159 |         result.emplaceBack(args: Entry{ .label: decodeFsEncString(str: dirEntry.fileName()), .deviceId: deviceId }); | 
| 160 |     } | 
| 161 |     return result; | 
| 162 | } | 
| 163 |  | 
| 164 | static std::optional<QString> retrieveLabelViaIoctl(int fd) | 
| 165 | { | 
| 166 |     // FS_IOC_GETFSLABEL was introduced in v4.18; previously it was btrfs-specific. | 
| 167 |     if (fd < 0) | 
| 168 |         return std::nullopt; | 
| 169 |  | 
| 170 |     // Note: it doesn't append the null terminator (despite what the man page | 
| 171 |     // says) and the return code on success (0) does not indicate the length. | 
| 172 |     char label[FSLABEL_MAX] = {}; | 
| 173 |     int r = ioctl(fd: fd, FS_IOC_GETFSLABEL, &label); | 
| 174 |     if (r < 0) | 
| 175 |         return std::nullopt; | 
| 176 |     return QString::fromUtf8(utf8: label); | 
| 177 | } | 
| 178 |  | 
| 179 | static inline QString retrieveLabel(const QStorageInfoPrivate &d, int fd, quint64 deviceId) | 
| 180 | { | 
| 181 |     if (auto label = retrieveLabelViaIoctl(fd)) | 
| 182 |         return *label; | 
| 183 |  | 
| 184 |     deviceId = retrieveDeviceId(device: d.device, deviceId); | 
| 185 |     if (!deviceId) | 
| 186 |         return QString(); | 
| 187 |  | 
| 188 |     for (const auto &dirEntry : devicesByLabel()) { | 
| 189 |         if (retrieveDeviceId(device: QFile::encodeName(fileName: dirEntry.filePath())) == deviceId) | 
| 190 |             return decodeFsEncString(str: dirEntry.fileName()); | 
| 191 |     } | 
| 192 |     return QString(); | 
| 193 | } | 
| 194 |  | 
| 195 | void QStorageInfoPrivate::retrieveVolumeInfo() | 
| 196 | { | 
| 197 |     struct statfs64 statfs_buf; | 
| 198 |     int result; | 
| 199 |     QT_EINTR_LOOP(result, statfs64(QFile::encodeName(rootPath).constData(), &statfs_buf)); | 
| 200 |     valid = ready = (result == 0); | 
| 201 |     if (valid) { | 
| 202 |         bytesTotal = statfs_buf.f_blocks * statfs_buf.f_frsize; | 
| 203 |         bytesFree = statfs_buf.f_bfree * statfs_buf.f_frsize; | 
| 204 |         bytesAvailable = statfs_buf.f_bavail * statfs_buf.f_frsize; | 
| 205 |         blockSize = int(statfs_buf.f_bsize); | 
| 206 |         readOnly = (statfs_buf.f_flags & ST_RDONLY) != 0; | 
| 207 |     } | 
| 208 | } | 
| 209 |  | 
| 210 | static std::vector<MountInfo> parseMountInfo(FilterMountInfo filter = FilterMountInfo::All) | 
| 211 | { | 
| 212 |     QFile file(u"/proc/self/mountinfo"_s ); | 
| 213 |     if (!file.open(flags: QIODevice::ReadOnly | QIODevice::Text)) | 
| 214 |         return {}; | 
| 215 |  | 
| 216 |     QByteArray mountinfo = file.readAll(); | 
| 217 |     file.close(); | 
| 218 |  | 
| 219 |     return doParseMountInfo(mountinfo, filter); | 
| 220 | } | 
| 221 |  | 
| 222 | void QStorageInfoPrivate::doStat() | 
| 223 | { | 
| 224 |     retrieveVolumeInfo(); | 
| 225 |     if (!ready) | 
| 226 |         return; | 
| 227 |  | 
| 228 |     rootPath = QFileInfo(rootPath).canonicalFilePath(); | 
| 229 |     if (rootPath.isEmpty()) | 
| 230 |         return; | 
| 231 |  | 
| 232 |     std::vector<MountInfo> infos = parseMountInfo(); | 
| 233 |     if (infos.empty()) { | 
| 234 |         rootPath = u'/'; | 
| 235 |         return; | 
| 236 |     } | 
| 237 |  | 
| 238 |     MountInfo *best = nullptr; | 
| 239 |     AutoFileDescriptor fd(rootPath); | 
| 240 |     if (quint64 mntid = mountIdForPath(fd)) { | 
| 241 |         // We have the mount ID for this path, so find the matching line. | 
| 242 |         auto it = std::find_if(first: infos.begin(), last: infos.end(), | 
| 243 |                                pred: [mntid](const MountInfo &info) { return info.mntid == mntid; }); | 
| 244 |         if (it != infos.end()) | 
| 245 |             best = q20::to_address(ptr: it); | 
| 246 |     } else { | 
| 247 |         // We have failed to get the mount ID for this path, usually because | 
| 248 |         // the path cannot be open()ed by this user (e.g., /root), so we fall | 
| 249 |         // back to a string search. | 
| 250 |         // We iterate over the /proc/self/mountinfo list backwards because then any | 
| 251 |         // matching isParentOf must be the actual mount point because it's the most | 
| 252 |         // recent mount on that path. Linux does allow mounting over non-empty | 
| 253 |         // directories, such as in: | 
| 254 |         //   # mount | tail -2 | 
| 255 |         //   tmpfs on /tmp/foo/bar type tmpfs (rw,relatime,inode64) | 
| 256 |         //   tmpfs on /tmp/foo type tmpfs (rw,relatime,inode64) | 
| 257 |         // | 
| 258 |         // We try to match the device ID in case there's a mount --move. | 
| 259 |         // We can't *rely* on it because some filesystems like btrfs will assign | 
| 260 |         // device IDs to subvolumes that aren't listed in /proc/self/mountinfo. | 
| 261 |  | 
| 262 |         const QString oldRootPath = std::exchange(obj&: rootPath, new_val: QString()); | 
| 263 |         const dev_t rootPathDevId = deviceIdForPath(device: oldRootPath); | 
| 264 |         for (auto it = infos.rbegin(); it != infos.rend(); ++it) { | 
| 265 |             if (!isParentOf(parent: it->mountPoint, dirName: oldRootPath)) | 
| 266 |                 continue; | 
| 267 |             if (rootPathDevId == it->stDev) { | 
| 268 |                 // device ID matches; this is definitely the best option | 
| 269 |                 best = q20::to_address(ptr: it); | 
| 270 |                 break; | 
| 271 |             } | 
| 272 |             if (!best) { | 
| 273 |                 // if we can't find a device ID match, this parent path is probably | 
| 274 |                 // the correct one | 
| 275 |                 best = q20::to_address(ptr: it); | 
| 276 |             } | 
| 277 |         } | 
| 278 |     } | 
| 279 |     if (best) { | 
| 280 |         auto stDev = best->stDev; | 
| 281 |         setFromMountInfo(std::move(*best)); | 
| 282 |         name = retrieveLabel(d: *this, fd, deviceId: stDev); | 
| 283 |     } | 
| 284 | } | 
| 285 |  | 
| 286 | QList<QStorageInfo> QStorageInfoPrivate::mountedVolumes() | 
| 287 | { | 
| 288 |     std::vector<MountInfo> infos = parseMountInfo(filter: FilterMountInfo::Filtered); | 
| 289 |     if (infos.empty()) | 
| 290 |         return QList{root()}; | 
| 291 |  | 
| 292 |     std::optional<decltype(retrieveLabels())> labelMap; | 
| 293 |     auto labelForDevice = [&labelMap](const QStorageInfoPrivate &d, int fd, quint64 devid) { | 
| 294 |         if (d.fileSystemType == "tmpfs" ) | 
| 295 |             return QString(); | 
| 296 |  | 
| 297 |         if (auto label = retrieveLabelViaIoctl(fd)) | 
| 298 |             return *label; | 
| 299 |  | 
| 300 |         devid = retrieveDeviceId(device: d.device, deviceId: devid); | 
| 301 |         if (!devid) | 
| 302 |             return QString(); | 
| 303 |  | 
| 304 |         if (!labelMap) | 
| 305 |             labelMap = retrieveLabels(); | 
| 306 |         for (auto &[deviceLabel, deviceId] : std::as_const(t&: *labelMap)) { | 
| 307 |             if (devid == deviceId) | 
| 308 |                 return deviceLabel; | 
| 309 |         } | 
| 310 |         return QString(); | 
| 311 |     }; | 
| 312 |  | 
| 313 |     QList<QStorageInfo> volumes; | 
| 314 |     volumes.reserve(asize: infos.size()); | 
| 315 |     for (auto it = infos.begin(); it != infos.end(); ++it) { | 
| 316 |         MountInfo &info = *it; | 
| 317 |         AutoFileDescriptor fd(info.mountPoint); | 
| 318 |  | 
| 319 |         // find out if the path as we see it matches this line from mountinfo | 
| 320 |         quint64 mntid = mountIdForPath(fd); | 
| 321 |         if (mntid == 0) { | 
| 322 |             // statx failed, so scan the later lines to see if any is a parent | 
| 323 |             // to this | 
| 324 |             auto isParent = [&info](const MountInfo &maybeParent) { | 
| 325 |                 return isParentOf(parent: maybeParent.mountPoint, dirName: info.mountPoint); | 
| 326 |             }; | 
| 327 |             if (std::find_if(first: it + 1, last: infos.end(), pred: isParent) != infos.end()) | 
| 328 |                 continue; | 
| 329 |         } else if (mntid != info.mntid) { | 
| 330 |             continue; | 
| 331 |         } | 
| 332 |  | 
| 333 |         const auto infoStDev = info.stDev; | 
| 334 |         QStorageInfoPrivate d(std::move(info)); | 
| 335 |         d.retrieveVolumeInfo(); | 
| 336 |         if (d.bytesTotal <= 0 && d.rootPath != u'/') | 
| 337 |             continue; | 
| 338 |         d.name = labelForDevice(d, fd, infoStDev); | 
| 339 |         volumes.emplace_back(args: QStorageInfo(*new QStorageInfoPrivate(std::move(d)))); | 
| 340 |     } | 
| 341 |     return volumes; | 
| 342 | } | 
| 343 |  | 
| 344 | QT_END_NAMESPACE | 
| 345 |  |