1 /*
2     SPDX-FileCopyrightText: 2017-2020 Andrius Štikonas <andrius@stikonas.eu>
3     SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
4 
5     SPDX-License-Identifier: GPL-3.0-or-later
6 */
7 
8 #include "core/fstab.h"
9 #include "util/externalcommand.h"
10 #include "util/report.h"
11 
12 #include <algorithm>
13 #include <array>
14 
15 #if defined(Q_OS_LINUX)
16     #include <blkid/blkid.h>
17 #endif
18 
19 #include <QChar>
20 #include <QDebug>
21 #include <QFileInfo>
22 #include <QRegularExpression>
23 #include <QTemporaryFile>
24 #include <QTextStream>
25 
26 static void parseFsSpec(const QString& m_fsSpec, FstabEntry::Type& m_entryType, QString& m_deviceNode);
27 static QString findBlkIdDevice(const char *token, const QString& value);
28 static void writeEntry(QTextStream& s, const FstabEntry& entry, std::array<unsigned int, 4> columnWidth);
29 std::array<unsigned int, 4> fstabColumnWidth(const FstabEntryList& fstabEntries);
30 
31 struct FstabEntryPrivate
32 {
33     QString m_fsSpec;
34     QString m_deviceNode;
35     QString m_mountPoint;
36     QString m_type;
37     QStringList m_options;
38     int m_dumpFreq;
39     int m_passNumber;
40     QString m_comment;
41     FstabEntry::Type m_entryType;
42 };
43 
FstabEntry(const QString & fsSpec,const QString & mountPoint,const QString & type,const QString & options,int dumpFreq,int passNumber,const QString & comment)44 FstabEntry::FstabEntry(const QString& fsSpec, const QString& mountPoint, const QString& type, const QString& options, int dumpFreq, int passNumber, const QString& comment) :
45     d(std::make_unique<FstabEntryPrivate>())
46 {
47     d->m_fsSpec = fsSpec;
48     d->m_mountPoint = mountPoint;
49     d->m_type = type;
50     d->m_dumpFreq = dumpFreq;
51     d->m_passNumber = passNumber;
52     d->m_comment = comment;
53 
54     d->m_options = options.split(QLatin1Char(','));
55     d->m_options.removeAll(QStringLiteral("defaults"));
56     parseFsSpec(d->m_fsSpec, d->m_entryType, d->m_deviceNode);
57 }
58 
readFstabEntries(const QString & fstabPath)59 FstabEntryList readFstabEntries( const QString& fstabPath )
60 {
61     FstabEntryList fstabEntries;
62     QFile fstabFile( fstabPath );
63     if ( fstabFile.open( QIODevice::ReadOnly | QIODevice::Text ) )
64     {
65         const QStringList fstabLines = QString::fromLocal8Bit(fstabFile.readAll()).split( QLatin1Char('\n') );
66         for ( const QString& rawLine : fstabLines )
67         {
68             QString line = rawLine.trimmed();
69             if ( line.startsWith( QLatin1Char('#') ) || line.isEmpty()) {
70                 fstabEntries.push_back( { {}, {}, {}, {}, {}, {}, line } );
71                 continue;
72             }
73 
74             QString comment = line.section( QLatin1Char('#'), 1 );
75             QStringList splitLine = line.section( QLatin1Char('#'), 0, 0 ).split( QRegularExpression(QStringLiteral("[\\s]+")), Qt::SkipEmptyParts );
76 
77             // We now split the standard components of /etc/fstab entry:
78             // (0) path, or UUID, or LABEL, etc,
79             // (1) mount point,
80             // (2) file system type,
81             // (3) options,
82             // (4) dump frequency (optional, defaults to 0), no comment is allowed if omitted,
83             // (5) pass number (optional, defaults to 0), no comment is allowed if omitted,
84             // (#) comment (optional).
85 
86             // Handle deprecated subtypes, e.g. sshfs#example. They are not relevant for partitioning, ignore them.
87             if (splitLine.size() < 3) {
88                 continue;
89             }
90 
91             auto fsSpec = splitLine.at(0);
92             auto mountPoint = unescapeSpaces(splitLine.at(1));
93             auto fsType = splitLine.at(2);
94             // Options may be omitted in some rare cases like NixOS generated fstab.
95             auto options = splitLine.length() >= 4 ? splitLine.at(3) : QStringLiteral("defaults");
96 
97             switch (splitLine.length()) {
98                 case 4:
99                     fstabEntries.push_back( {fsSpec, mountPoint, fsType, options } );
100                     break;
101                 case 5:
102                     fstabEntries.push_back( {fsSpec, mountPoint, fsType, options, splitLine.at(4).toInt() } );
103                     break;
104                 case 6:
105                     fstabEntries.push_back( {fsSpec, mountPoint, fsType, options, splitLine.at(4).toInt(), splitLine.at(5).toInt(), comment.isEmpty() ? QString() : QLatin1Char('#') + comment } );
106                     break;
107                 default:
108                     fstabEntries.push_back( { {}, {}, {}, {}, {}, {}, QLatin1Char('#') + line } );
109             }
110         }
111 
112         fstabFile.close();
113         if (fstabEntries.back().entryType() == FstabEntry::Type::comment && fstabEntries.back().comment().isEmpty())
114             fstabEntries.pop_back();
115     }
116 
117     return fstabEntries;
118 }
119 
escapeSpaces(const QString & mountPoint)120 QString escapeSpaces(const QString& mountPoint)
121 {
122     QString tmp = mountPoint;
123     tmp.replace(QStringLiteral(" "), QStringLiteral("\\040"));
124     tmp.replace(QStringLiteral("\t"), QStringLiteral("\\011"));
125     return tmp;
126 }
127 
unescapeSpaces(const QString & mountPoint)128 QString unescapeSpaces(const QString& mountPoint)
129 {
130     QString tmp = mountPoint;
131     tmp.replace(QStringLiteral("\\040"), QStringLiteral(" "));
132     tmp.replace(QStringLiteral("\\011"), QStringLiteral("\t"));
133     return tmp;
134 }
135 
setFsSpec(const QString & s)136 void FstabEntry::setFsSpec(const QString& s)
137 {
138     d->m_fsSpec = s;
139     parseFsSpec(d->m_fsSpec, d->m_entryType, d->m_deviceNode);
140 }
141 
fsSpec() const142 const QString& FstabEntry::fsSpec() const
143 {
144     return d->m_fsSpec;
145 }
146 
deviceNode() const147 const QString& FstabEntry::deviceNode() const
148 {
149     return d->m_deviceNode;
150 }
151 
mountPoint() const152 const QString& FstabEntry::mountPoint() const
153 {
154     return d->m_mountPoint;
155 }
156 
type() const157 const QString& FstabEntry::type() const
158 {
159     return d->m_type;
160 }
161 
options() const162 const QStringList& FstabEntry::options() const
163 {
164     return d->m_options;
165 }
166 
optionsString() const167 const QString FstabEntry::optionsString() const
168 {
169     return options().size() > 0 ? options().join(QLatin1Char(',')) : QStringLiteral("defaults");
170 }
171 
dumpFreq() const172 int FstabEntry::dumpFreq() const
173 {
174     return d->m_dumpFreq;
175 }
176 
passNumber() const177 int FstabEntry::passNumber() const
178 {
179     return d->m_passNumber;
180 }
181 
comment() const182 const QString& FstabEntry::comment() const
183 {
184     return d->m_comment;
185 }
186 
entryType() const187 FstabEntry::Type FstabEntry::entryType() const
188 {
189     return d->m_entryType;
190 }
191 
setMountPoint(const QString & s)192 void FstabEntry::setMountPoint(const QString& s)
193 {
194     d->m_mountPoint = s;
195 }
196 
setOptions(const QStringList & s)197 void FstabEntry::setOptions(const QStringList& s)
198 {
199     d->m_options = s;
200 }
201 
setDumpFreq(int s)202 void FstabEntry::setDumpFreq(int s)
203 {
204     d->m_dumpFreq = s;
205 }
206 
setPassNumber(int s)207 void FstabEntry::setPassNumber(int s)
208 {
209     d->m_passNumber = s;
210 }
211 
possibleMountPoints(const QString & deviceNode,const QString & fstabPath)212 QStringList possibleMountPoints(const QString& deviceNode, const QString& fstabPath)
213 {
214     QStringList mountPoints;
215     QString canonicalPath = QFileInfo(deviceNode).canonicalFilePath();
216     const FstabEntryList fstabEntryList = readFstabEntries( fstabPath );
217     for (const FstabEntry &entry : fstabEntryList)
218         if (QFileInfo(entry.deviceNode()).canonicalFilePath() == canonicalPath)
219             mountPoints.append(entry.mountPoint());
220 
221     return mountPoints;
222 }
223 
findBlkIdDevice(const char * token,const QString & value)224 static QString findBlkIdDevice(const char *token, const QString& value)
225 {
226     QString rval;
227 
228 #if defined(Q_OS_LINUX)
229     if (char* c = blkid_evaluate_tag(token, value.toLocal8Bit().constData(), nullptr)) {
230         rval = QString::fromLocal8Bit(c);
231         free(c);
232     }
233 #else
234     Q_UNUSED(token)
235     Q_UNUSED(value)
236 #endif
237 
238     return rval;
239 }
240 
parseFsSpec(const QString & m_fsSpec,FstabEntry::Type & m_entryType,QString & m_deviceNode)241 static void parseFsSpec(const QString& m_fsSpec, FstabEntry::Type& m_entryType, QString& m_deviceNode)
242 {
243     m_entryType = FstabEntry::Type::comment;
244     if (m_fsSpec.startsWith(QStringLiteral("UUID="))) {
245         m_entryType = FstabEntry::Type::uuid;
246         m_deviceNode = findBlkIdDevice("UUID", QString(m_fsSpec).remove(QStringLiteral("UUID=")));
247     } else if (m_fsSpec.startsWith(QStringLiteral("LABEL="))) {
248         m_entryType = FstabEntry::Type::label;
249         m_deviceNode = findBlkIdDevice("LABEL", QString(m_fsSpec).remove(QStringLiteral("LABEL=")));
250     } else if (m_fsSpec.startsWith(QStringLiteral("PARTUUID="))) {
251         m_entryType = FstabEntry::Type::uuid;
252         m_deviceNode = findBlkIdDevice("PARTUUID", QString(m_fsSpec).remove(QStringLiteral("PARTUUID=")));
253     } else if (m_fsSpec.startsWith(QStringLiteral("PARTLABEL="))) {
254         m_entryType = FstabEntry::Type::label;
255         m_deviceNode = findBlkIdDevice("PARTLABEL", QString(m_fsSpec).remove(QStringLiteral("PARTLABEL=")));
256     } else if (m_fsSpec.startsWith(QStringLiteral("/"))) {
257         m_entryType = FstabEntry::Type::deviceNode;
258         m_deviceNode = m_fsSpec;
259     }
260 }
261 
262 
263 // Used to nicely format fstab file
fstabColumnWidth(const FstabEntryList & fstabEntries)264 std::array<unsigned int, 4> fstabColumnWidth(const FstabEntryList& fstabEntries)
265 {
266     std::array<unsigned int, 4> columnWidth;
267 
268 #define FIELD_WIDTH(x) 3 + escapeSpaces(std::max_element(fstabEntries.begin(), fstabEntries.end(), [](const FstabEntry& a, const FstabEntry& b) {return escapeSpaces(a.x()).length() < escapeSpaces(b.x()).length(); })->x()).length();
269 
270     columnWidth[0] = FIELD_WIDTH(fsSpec);
271     columnWidth[1] = FIELD_WIDTH(mountPoint);
272     columnWidth[2] = FIELD_WIDTH(type);
273     columnWidth[3] = FIELD_WIDTH(optionsString);
274 
275     return columnWidth;
276 }
277 
writeEntry(QTextStream & s,const FstabEntry & entry,std::array<unsigned int,4> columnWidth)278 static void writeEntry(QTextStream& s, const FstabEntry& entry, std::array<unsigned int, 4> columnWidth)
279 {
280     if (entry.entryType() == FstabEntry::Type::comment) {
281         s << entry.comment() << "\n";
282         return;
283     }
284 
285     s.setFieldAlignment(QTextStream::AlignLeft);
286     s.setFieldWidth(columnWidth[0]);
287     s << entry.fsSpec()
288       << qSetFieldWidth(columnWidth[1]) << (entry.mountPoint().isEmpty() ? QStringLiteral("none") : escapeSpaces(entry.mountPoint()))
289       << qSetFieldWidth(columnWidth[2]) << entry.type()
290       << qSetFieldWidth(columnWidth[3]) << entry.optionsString() << qSetFieldWidth(0)
291       << entry.dumpFreq() << " "
292       << entry.passNumber() << " "
293       << entry.comment() << "\n";
294 }
295 
writeMountpoints(const FstabEntryList & fstabEntries,const QString & filename)296 bool writeMountpoints(const FstabEntryList& fstabEntries, const QString& filename)
297 {
298     QString fstabContents;
299     QTextStream out(&fstabContents);
300 
301     std::array<unsigned int, 4> columnWidth = fstabColumnWidth(fstabEntries);
302 
303     for (const auto &e : fstabEntries)
304         writeEntry(out, e, columnWidth);
305 
306     ExternalCommand cmd;
307     return cmd.createFile(fstabContents.toLocal8Bit(), filename);
308 }
309