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
39QT_BEGIN_NAMESPACE
40
41using namespace Qt::StringLiterals;
42
43namespace {
44struct 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
58static 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
94static 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
102static 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
116static 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
140static 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
147static 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
164static 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
179static 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
195void 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
210static 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
222void 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
286QList<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
344QT_END_NAMESPACE
345

source code of qtbase/src/corelib/io/qstorageinfo_linux.cpp