1 /* === This file is part of Calamares - <https://calamares.io> ===
2  *
3  *   SPDX-FileCopyrightText: 2015-2016 Teo Mrnjavac <teo@kde.org>
4  *   SPDX-FileCopyrightText: 2018-2019 Adriaan de Groot <groot@kde.org>
5  *   SPDX-FileCopyrightText: 2019 Collabora Ltd <arnaud.ferraris@collabora.com>
6  *   SPDX-License-Identifier: GPL-3.0-or-later
7  *
8  *   Calamares is Free Software: see the License-Identifier above.
9  *
10  */
11 
12 #include "PartUtils.h"
13 
14 #include "core/DeviceModel.h"
15 #include "core/KPMHelpers.h"
16 #include "core/PartitionInfo.h"
17 
18 #include "GlobalStorage.h"
19 #include "JobQueue.h"
20 #include "partition/Mount.h"
21 #include "partition/PartitionIterator.h"
22 #include "partition/PartitionQuery.h"
23 #include "utils/CalamaresUtilsSystem.h"
24 #include "utils/Logger.h"
25 #include "utils/RAII.h"
26 
27 #include <kpmcore/backend/corebackend.h>
28 #include <kpmcore/backend/corebackendmanager.h>
29 #include <kpmcore/core/device.h>
30 #include <kpmcore/core/partition.h>
31 
32 #include <QProcess>
33 #include <QTemporaryDir>
34 
35 using CalamaresUtils::Partition::isPartitionFreeSpace;
36 using CalamaresUtils::Partition::isPartitionNew;
37 
38 namespace PartUtils
39 {
40 
41 QString
convenienceName(const Partition * const candidate)42 convenienceName( const Partition* const candidate )
43 {
44     if ( !candidate->mountPoint().isEmpty() )
45     {
46         return candidate->mountPoint();
47     }
48     if ( !candidate->partitionPath().isEmpty() )
49     {
50         return candidate->partitionPath();
51     }
52     if ( !candidate->devicePath().isEmpty() )
53     {
54         return candidate->devicePath();
55     }
56     if ( !candidate->deviceNode().isEmpty() )
57     {
58         return candidate->devicePath();
59     }
60 
61     QString p;
62     QTextStream s( &p );
63     s << static_cast< const void* >( candidate );  // No good name available, use pointer address
64 
65     return p;
66 }
67 
68 /** @brief Get the globalStorage setting for required space. */
69 static double
getRequiredStorageGiB(bool & ok)70 getRequiredStorageGiB( bool& ok )
71 {
72     return Calamares::JobQueue::instance()->globalStorage()->value( "requiredStorageGiB" ).toDouble( &ok );
73 }
74 
75 bool
canBeReplaced(Partition * candidate,const Logger::Once & o)76 canBeReplaced( Partition* candidate, const Logger::Once& o )
77 {
78     if ( !candidate )
79     {
80         cDebug() << o << "Partition* is NULL";
81         return false;
82     }
83 
84     cDebug() << o << "Checking if" << convenienceName( candidate ) << "can be replaced.";
85     if ( candidate->isMounted() )
86     {
87         cDebug() << Logger::SubEntry << "NO, it is mounted.";
88         return false;
89     }
90 
91     bool ok = false;
92     double requiredStorageGiB = getRequiredStorageGiB( ok );
93     if ( !ok )
94     {
95         cDebug() << Logger::SubEntry << "NO, requiredStorageGiB is not set correctly.";
96         return false;
97     }
98 
99     qint64 availableStorageB = candidate->capacity();
100     qint64 requiredStorageB = CalamaresUtils::GiBtoBytes( requiredStorageGiB + 0.5 );
101 
102     if ( availableStorageB > requiredStorageB )
103     {
104         cDebug() << o << "Partition" << convenienceName( candidate ) << "authorized for replace install.";
105         return true;
106     }
107     else
108     {
109         Logger::CDebug deb;
110         deb << Logger::SubEntry << "NO, insufficient storage";
111         deb << Logger::Continuation << "Required  storage B:" << requiredStorageB
112             << QString( "(%1GiB)" ).arg( requiredStorageGiB );
113         deb << Logger::Continuation << "Available storage B:" << availableStorageB
114             << QString( "(%1GiB)" ).arg( CalamaresUtils::BytesToGiB( availableStorageB ) );
115         return false;
116     }
117 }
118 
119 
120 bool
canBeResized(Partition * candidate,const Logger::Once & o)121 canBeResized( Partition* candidate, const Logger::Once& o )
122 {
123     if ( !candidate )
124     {
125         cDebug() << o << "Partition* is NULL";
126         return false;
127     }
128 
129     if ( !candidate->fileSystem().supportGrow() || !candidate->fileSystem().supportShrink() )
130     {
131         cDebug() << o << "Can not resize" << convenienceName( candidate ) << ", filesystem"
132                  << candidate->fileSystem().name() << "does not support resize.";
133         return false;
134     }
135 
136     if ( isPartitionFreeSpace( candidate ) )
137     {
138         cDebug() << o << "Can not resize" << convenienceName( candidate ) << ", partition is free space";
139         return false;
140     }
141 
142     if ( candidate->isMounted() )
143     {
144         cDebug() << o << "Can not resize" << convenienceName( candidate ) << ", partition is mounted";
145         return false;
146     }
147 
148     if ( candidate->roles().has( PartitionRole::Primary ) )
149     {
150         PartitionTable* table = dynamic_cast< PartitionTable* >( candidate->parent() );
151         if ( !table )
152         {
153             cDebug() << o << "Can not resize" << convenienceName( candidate ) << ", no partition table found";
154             return false;
155         }
156 
157         if ( table->numPrimaries() >= table->maxPrimaries() )
158         {
159             cDebug() << o << "Can not resize" << convenienceName( candidate ) << ", partition table already has"
160                      << table->maxPrimaries() << "primary partitions.";
161             return false;
162         }
163     }
164 
165     bool ok = false;
166     double requiredStorageGiB = getRequiredStorageGiB( ok );
167     if ( !ok )
168     {
169         cDebug() << o << "Can not resize" << convenienceName( candidate )
170                  << ", requiredStorageGiB is not set correctly.";
171         return false;
172     }
173 
174     // We require a little more for partitioning overhead and swap file
175     double advisedStorageGiB = requiredStorageGiB + 0.5 + 2.0;
176     qint64 availableStorageB = candidate->available();
177     qint64 advisedStorageB = CalamaresUtils::GiBtoBytes( advisedStorageGiB );
178 
179     if ( availableStorageB > advisedStorageB )
180     {
181         cDebug() << o << "Partition" << convenienceName( candidate )
182                  << "authorized for resize + autopartition install.";
183         return true;
184     }
185     else
186     {
187         Logger::CDebug deb;
188         deb << Logger::SubEntry << "NO, insufficient storage";
189         deb << Logger::Continuation << "Required  storage B:" << advisedStorageB
190             << QString( "(%1GiB)" ).arg( advisedStorageGiB );
191         deb << Logger::Continuation << "Available storage B:" << availableStorageB
192             << QString( "(%1GiB)" ).arg( CalamaresUtils::BytesToGiB( availableStorageB ) ) << "for"
193             << convenienceName( candidate ) << "length:" << candidate->length()
194             << "sectorsUsed:" << candidate->sectorsUsed() << "fsType:" << candidate->fileSystem().name();
195         return false;
196     }
197 }
198 
199 
200 bool
canBeResized(DeviceModel * dm,const QString & partitionPath,const Logger::Once & o)201 canBeResized( DeviceModel* dm, const QString& partitionPath, const Logger::Once& o )
202 {
203     if ( partitionPath.startsWith( "/dev/" ) )
204     {
205         for ( int i = 0; i < dm->rowCount(); ++i )
206         {
207             Device* dev = dm->deviceForIndex( dm->index( i ) );
208             Partition* candidate = CalamaresUtils::Partition::findPartitionByPath( { dev }, partitionPath );
209             if ( candidate )
210             {
211                 return canBeResized( candidate, o );
212             }
213         }
214         cWarning() << "Can not resize" << partitionPath << ", no Partition* found.";
215         return false;
216     }
217     else
218     {
219         cWarning() << "Can not resize" << partitionPath << ", does not start with /dev";
220         return false;
221     }
222 }
223 
224 
225 static FstabEntryList
lookForFstabEntries(const QString & partitionPath)226 lookForFstabEntries( const QString& partitionPath )
227 {
228     QStringList mountOptions { "ro" };
229 
230     auto r = CalamaresUtils::System::runCommand( CalamaresUtils::System::RunLocation::RunInHost,
231                                                  { "blkid", "-s", "TYPE", "-o", "value", partitionPath } );
232     if ( r.getExitCode() )
233     {
234         cWarning() << "blkid on" << partitionPath << "failed.";
235     }
236     else
237     {
238         QString fstype = r.getOutput().trimmed();
239         if ( ( fstype == "ext3" ) || ( fstype == "ext4" ) )
240         {
241             mountOptions.append( "noload" );
242         }
243     }
244 
245     cDebug() << "Checking device" << partitionPath << "for fstab (fs=" << r.getOutput() << ')';
246 
247     FstabEntryList fstabEntries;
248 
249     CalamaresUtils::Partition::TemporaryMount mount( partitionPath, QString(), mountOptions.join( ',' ) );
250     if ( mount.isValid() )
251     {
252         QFile fstabFile( mount.path() + "/etc/fstab" );
253 
254         if ( fstabFile.open( QIODevice::ReadOnly | QIODevice::Text ) )
255         {
256             const QStringList fstabLines = QString::fromLocal8Bit( fstabFile.readAll() ).split( '\n' );
257 
258             for ( const QString& rawLine : fstabLines )
259             {
260                 fstabEntries.append( FstabEntry::fromEtcFstab( rawLine ) );
261             }
262             fstabFile.close();
263             const int lineCount = fstabEntries.count();
264             std::remove_if(
265                 fstabEntries.begin(), fstabEntries.end(), []( const FstabEntry& x ) { return !x.isValid(); } );
266             cDebug() << Logger::SubEntry << "got" << fstabEntries.count() << "fstab entries from" << lineCount
267                      << "lines in" << fstabFile.fileName();
268         }
269         else
270         {
271             cWarning() << "Could not read fstab from mounted fs";
272         }
273     }
274     else
275     {
276         cWarning() << "Could not mount existing fs";
277     }
278 
279     return fstabEntries;
280 }
281 
282 
283 static QString
findPartitionPathForMountPoint(const FstabEntryList & fstab,const QString & mountPoint)284 findPartitionPathForMountPoint( const FstabEntryList& fstab, const QString& mountPoint )
285 {
286     if ( fstab.isEmpty() )
287     {
288         return QString();
289     }
290 
291     for ( const FstabEntry& entry : fstab )
292     {
293         if ( entry.mountPoint == mountPoint )
294         {
295             QProcess readlink;
296             QString partPath;
297 
298             if ( entry.partitionNode.startsWith( "/dev" ) )  // plain dev node
299             {
300                 partPath = entry.partitionNode;
301             }
302             else if ( entry.partitionNode.startsWith( "LABEL=" ) )
303             {
304                 partPath = entry.partitionNode.mid( 6 );
305                 partPath.remove( "\"" );
306                 partPath.replace( "\\040", "\\ " );
307                 partPath.prepend( "/dev/disk/by-label/" );
308             }
309             else if ( entry.partitionNode.startsWith( "UUID=" ) )
310             {
311                 partPath = entry.partitionNode.mid( 5 );
312                 partPath.remove( "\"" );
313                 partPath = partPath.toLower();
314                 partPath.prepend( "/dev/disk/by-uuid/" );
315             }
316             else if ( entry.partitionNode.startsWith( "PARTLABEL=" ) )
317             {
318                 partPath = entry.partitionNode.mid( 10 );
319                 partPath.remove( "\"" );
320                 partPath.replace( "\\040", "\\ " );
321                 partPath.prepend( "/dev/disk/by-partlabel/" );
322             }
323             else if ( entry.partitionNode.startsWith( "PARTUUID=" ) )
324             {
325                 partPath = entry.partitionNode.mid( 9 );
326                 partPath.remove( "\"" );
327                 partPath = partPath.toLower();
328                 partPath.prepend( "/dev/disk/by-partuuid/" );
329             }
330 
331             // At this point we either have /dev/sda1, or /dev/disk/by-something/...
332 
333             if ( partPath.startsWith( "/dev/disk/by-" ) )  // we got a fancy node
334             {
335                 readlink.start( "readlink", { "-en", partPath } );
336                 if ( !readlink.waitForStarted( 1000 ) )
337                 {
338                     return QString();
339                 }
340                 if ( !readlink.waitForFinished( 1000 ) )
341                 {
342                     return QString();
343                 }
344                 if ( readlink.exitCode() != 0 || readlink.exitStatus() != QProcess::NormalExit )
345                 {
346                     return QString();
347                 }
348                 partPath = QString::fromLocal8Bit( readlink.readAllStandardOutput() ).trimmed();
349             }
350 
351             return partPath;
352         }
353     }
354 
355     return QString();
356 }
357 
358 
359 OsproberEntryList
runOsprober(DeviceModel * dm)360 runOsprober( DeviceModel* dm )
361 {
362     Logger::Once o;
363 
364     QString osproberOutput;
365     QProcess osprober;
366     osprober.setProgram( "os-prober" );
367     osprober.setProcessChannelMode( QProcess::SeparateChannels );
368     osprober.start();
369     if ( !osprober.waitForStarted() )
370     {
371         cError() << "os-prober cannot start.";
372     }
373     else if ( !osprober.waitForFinished( 60000 ) )
374     {
375         cError() << "os-prober timed out.";
376     }
377     else
378     {
379         osproberOutput.append( QString::fromLocal8Bit( osprober.readAllStandardOutput() ).trimmed() );
380     }
381 
382     QStringList osproberCleanLines;
383     OsproberEntryList osproberEntries;
384     const auto lines = osproberOutput.split( '\n' );
385     for ( const QString& line : lines )
386     {
387         if ( !line.simplified().isEmpty() )
388         {
389             QStringList lineColumns = line.split( ':' );
390             QString prettyName;
391             if ( !lineColumns.value( 1 ).simplified().isEmpty() )
392             {
393                 prettyName = lineColumns.value( 1 ).simplified();
394             }
395             else if ( !lineColumns.value( 2 ).simplified().isEmpty() )
396             {
397                 prettyName = lineColumns.value( 2 ).simplified();
398             }
399 
400             QString file, path = lineColumns.value( 0 ).simplified();
401             if ( !path.startsWith( "/dev/" ) )  //basic sanity check
402             {
403                 continue;
404             }
405 
406             // strip extra file after device: /dev/name@/path/to/file
407             int index = path.indexOf( '@' );
408             if ( index != -1 )
409             {
410                 file = path.right( path.length() - index - 1 );
411                 path = path.left( index );
412             }
413 
414             FstabEntryList fstabEntries = lookForFstabEntries( path );
415             QString homePath = findPartitionPathForMountPoint( fstabEntries, "/home" );
416 
417             osproberEntries.append( { prettyName,
418                                       path,
419                                       file,
420                                       QString(),
421                                       canBeResized( dm, path, o ),
422                                       lineColumns,
423                                       fstabEntries,
424                                       homePath } );
425             osproberCleanLines.append( line );
426         }
427     }
428 
429     if ( osproberCleanLines.count() > 0 )
430     {
431         cDebug() << o << "os-prober lines after cleanup:" << Logger::DebugList( osproberCleanLines );
432     }
433     else
434     {
435         cDebug() << o << "os-prober gave no output.";
436     }
437 
438     Calamares::JobQueue::instance()->globalStorage()->insert( "osproberLines", osproberCleanLines );
439 
440     return osproberEntries;
441 }
442 
443 bool
isEfiSystem()444 isEfiSystem()
445 {
446     return QDir( "/sys/firmware/efi/efivars" ).exists();
447 }
448 
449 bool
isEfiFilesystemSuitableType(const Partition * candidate)450 isEfiFilesystemSuitableType( const Partition* candidate )
451 {
452     auto type = candidate->fileSystem().type();
453 
454     switch ( type )
455     {
456     case FileSystem::Type::Fat32:
457         return true;
458 #ifdef WITH_KPMCORE4API
459     case FileSystem::Type::Fat12:
460 #endif
461     case FileSystem::Type::Fat16:
462         cWarning() << "FAT12 and FAT16 are probably not supported by EFI";
463         return false;
464     default:
465         cWarning() << "EFI boot partition must be FAT32";
466         return false;
467     }
468 }
469 
470 bool
isEfiFilesystemSuitableSize(const Partition * candidate)471 isEfiFilesystemSuitableSize( const Partition* candidate )
472 {
473     auto size = candidate->capacity();  // bytes
474 
475     using CalamaresUtils::Units::operator""_MiB;
476     if ( size >= 300_MiB )
477     {
478         return true;
479     }
480     else
481     {
482         cWarning() << "Filesystem for EFI is too small (" << size << "bytes)";
483         return false;
484     }
485 }
486 
487 
488 bool
isEfiBootable(const Partition * candidate)489 isEfiBootable( const Partition* candidate )
490 {
491     const auto flags = PartitionInfo::flags( candidate );
492 
493 #if defined( WITH_KPMCORE4API )
494     // In KPMCore4, the flags are remapped, and the ESP flag is the same as Boot.
495     static_assert( KPM_PARTITION_FLAG_ESP == KPM_PARTITION_FLAG( Boot ), "KPMCore API enum changed" );
496     return flags.testFlag( KPM_PARTITION_FLAG_ESP );
497 #else
498     // In KPMCore3, bit 17 is the old-style Esp flag, and it's OK
499     if ( flags.testFlag( KPM_PARTITION_FLAG_ESP ) )
500     {
501         return true;
502     }
503 
504     /* Otherwise, if it's a GPT table, Boot (bit 0) is the same as Esp */
505     const PartitionTable* table = CalamaresUtils::Partition::getPartitionTable( candidate );
506     if ( !table )
507     {
508         cWarning() << "Root of partition table is not a PartitionTable object";
509         return false;
510     }
511     if ( table->type() == PartitionTable::TableType::gpt )
512     {
513         const auto bootFlag = KPM_PARTITION_FLAG( Boot );
514         return flags.testFlag( bootFlag );
515     }
516     return false;
517 #endif
518 }
519 
520 // TODO: this is configurable via the config file **already**
521 size_t
efiFilesystemMinimumSize()522 efiFilesystemMinimumSize()
523 {
524     using CalamaresUtils::Units::operator""_MiB;
525     return 300_MiB;
526 }
527 
528 
529 QString
canonicalFilesystemName(const QString & fsName,FileSystem::Type * fsType)530 canonicalFilesystemName( const QString& fsName, FileSystem::Type* fsType )
531 {
532     cScopedAssignment type( fsType );
533     if ( fsName.isEmpty() )
534     {
535         type = FileSystem::Ext4;
536         return QStringLiteral( "ext4" );
537     }
538 
539     QStringList fsLanguage { QLatin1String( "C" ) };  // Required language list to turn off localization
540 
541     if ( ( type = FileSystem::typeForName( fsName, fsLanguage ) ) != FileSystem::Unknown )
542     {
543         return fsName;
544     }
545 
546     // Second pass: try case-insensitive
547     const auto fstypes = FileSystem::types();
548     for ( FileSystem::Type t : fstypes )
549     {
550         if ( 0 == QString::compare( fsName, FileSystem::nameForType( t, fsLanguage ), Qt::CaseInsensitive ) )
551         {
552             QString fsRealName = FileSystem::nameForType( t, fsLanguage );
553             if ( fsType )
554             {
555                 *fsType = t;
556             }
557             return fsRealName;
558         }
559     }
560 
561     cWarning() << "Filesystem" << fsName << "not found, using ext4";
562     // fsType can be used to check whether fsName was a valid filesystem.
563     if ( fsType )
564     {
565         *fsType = FileSystem::Unknown;
566     }
567 #ifdef DEBUG_FILESYSTEMS
568     // This bit is for distro's debugging their settings, and shows
569     // all the strings that KPMCore is matching against for FS type.
570     {
571         Logger::CDebug d;
572         using TR = Logger::DebugRow< int, QString >;
573         const auto fstypes = FileSystem::types();
574         d << "Available types (" << fstypes.count() << ')';
575         for ( FileSystem::Type t : fstypes )
576         {
577             d << TR( static_cast< int >( t ), FileSystem::nameForType( t, fsLanguage ) );
578         }
579     }
580 #endif
581     type = FileSystem::Unknown;
582     return QStringLiteral( "ext4" );
583 }
584 
585 }  // namespace PartUtils
586 
587 /* Implementation of methods for FstabEntry, from OsproberEntry.h */
588 
589 bool
isValid() const590 FstabEntry::isValid() const
591 {
592     return !partitionNode.isEmpty() && !mountPoint.isEmpty() && !fsType.isEmpty();
593 }
594 
595 FstabEntry
fromEtcFstab(const QString & rawLine)596 FstabEntry::fromEtcFstab( const QString& rawLine )
597 {
598     QString line = rawLine.simplified();
599     if ( line.startsWith( '#' ) )
600         return FstabEntry { QString(), QString(), QString(), QString(), 0, 0 };
601 
602     QStringList splitLine = line.split( ' ' );
603     if ( splitLine.length() != 6 )
604         return FstabEntry { QString(), QString(), QString(), QString(), 0, 0 };
605 
606     return FstabEntry {
607         splitLine.at( 0 ),  // path, or UUID, or LABEL, etc.
608         splitLine.at( 1 ),  // mount point
609         splitLine.at( 2 ),  // fs type
610         splitLine.at( 3 ),  // options
611         splitLine.at( 4 ).toInt(),  //dump
612         splitLine.at( 5 ).toInt()  //pass
613     };
614 }
615