/* SPDX-FileCopyrightText: 2017-2020 Andrius Štikonas SPDX-FileCopyrightText: 2018-2019 Caio Jordão Carvalho SPDX-FileCopyrightText: 2019 Shubham Jangra SPDX-FileCopyrightText: 2020 Gaël PORTAY SPDX-FileCopyrightText: 2020 Adriaan de Groot SPDX-License-Identifier: GPL-3.0-or-later */ /** @file */ #include "plugins/sfdisk/sfdiskbackend.h" #include "plugins/sfdisk/sfdiskdevice.h" #include "plugins/sfdisk/sfdiskgptattributes.h" #include "core/copysourcedevice.h" #include "core/copytargetbytearray.h" #include "core/diskdevice.h" #include "core/lvmdevice.h" #include "core/partitiontable.h" #include "core/partitionalignment.h" #include "core/raid/softwareraid.h" #include "fs/filesystemfactory.h" #include "fs/luks.h" #include "fs/luks2.h" #include "util/globallog.h" #include "util/externalcommand.h" #include "util/helpers.h" #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(SfdiskBackendFactory, "pmsfdiskbackendplugin.json", registerPlugin();) SfdiskBackend::SfdiskBackend(QObject*, const QList&) : CoreBackend() { } void SfdiskBackend::initFSSupport() { } QList SfdiskBackend::scanDevices(bool excludeReadOnly) { return scanDevices(excludeReadOnly ? ScanFlags() : ScanFlag::includeReadOnly); } QList SfdiskBackend::scanDevices(const ScanFlags scanFlags) { const bool includeReadOnly = scanFlags.testFlag(ScanFlag::includeReadOnly); const bool includeLoopback = scanFlags.testFlag(ScanFlag::includeLoopback); QList result; QStringList deviceNodes; ExternalCommand cmd(QStringLiteral("lsblk"), { QStringLiteral("--nodeps"), QStringLiteral("--paths"), QStringLiteral("--sort"), QStringLiteral("name"), QStringLiteral("--json"), QStringLiteral("--output"), QStringLiteral("type,name") }); if (cmd.run(-1) && cmd.exitCode() == 0) { const QJsonDocument jsonDocument = QJsonDocument::fromJson(cmd.rawOutput()); const QJsonObject jsonObject = jsonDocument.object(); const QJsonArray jsonArray = jsonObject[QLatin1String("blockdevices")].toArray(); for (const auto &deviceLine : jsonArray) { QJsonObject deviceObject = deviceLine.toObject(); if (! (deviceObject[QLatin1String("type")].toString() == QLatin1String("disk") || (includeLoopback && deviceObject[QLatin1String("type")].toString() == QLatin1String("loop")) )) { continue; } const QString deviceNode = deviceObject[QLatin1String("name")].toString(); if (!includeReadOnly) { QString deviceName = deviceNode; deviceName.remove(QStringLiteral("/dev/")); QFile f(QStringLiteral("/sys/block/%1/ro").arg(deviceName)); if (f.open(QIODevice::ReadOnly)) if (f.readLine().trimmed().toInt() == 1) continue; } deviceNodes << deviceNode; } int totalDevices = deviceNodes.length(); for (int i = 0; i < totalDevices; ++i) { const QString deviceNode = deviceNodes[i]; emitScanProgress(deviceNode, i * 100 / totalDevices); Device* device = scanDevice(deviceNode); if (device != nullptr) { result.append(device); } } } VolumeManagerDevice::scanDevices(result); // scan all types of VolumeManagerDevices return result; } /*** @brief Fix up bogus JSON from `sfdisk --json /dev/sdb` * * The command `sfdisk --json /dev/sdb` outputs a JSON representation * of the partition table, with general device characteristics and * the list of partitions, **but**.. * * This isn't necessarily valid JSON: in particular, when there are * no partitions on the disk because it is empty / was recently zeroed / * is a USB stick for testing purposes, the output is changed **only** * by there being no partitions in the partition table. However, * the comma (",") after sectorsize is still printed. Bogus output looks * like this: * * { * "partitiontable": { * "label":"gpt", * "id":"1F9E80D9-DD78-024F-94A3-B61EC82B18C8", * "device":"/dev/sdb", * "unit":"sectors", * "firstlba":2048, * "lastlba":30949342, * "sectorsize":512, * } * } * * That's not valid JSON because of the "," followed by nothing until * the brace, and yields an empty object is passed to fromJson(). * * We'll go through and check if there's a "," followed by whitespace * and then a }. If there is, replace the ,. * * This is also fixed in util-linux 2.37. */ static void fixInvalidJsonFromSFDisk( QByteArray& s ) { // -1 if there is no comma (but then there's no useful JSON either), // not is 0 a valid place (the start) for a , in a JSON document. int lastComma = s.lastIndexOf(','); if ( lastComma > 0 ) { for ( int charIndex = lastComma + 1; charIndex < s.length(); ++charIndex ) { if ( s[charIndex] == '}' ) { s[lastComma] = ' '; // Erase that comma } if ( !isspace( s[charIndex] ) ) { break; } } } } /** Create a Device for the given device_node and scan it for partitions. @param deviceNode the device node (e.g. "/dev/sda") @return the created Device object. callers need to free this. */ Device* SfdiskBackend::scanDevice(const QString& deviceNode) { ExternalCommand modelCommand(QStringLiteral("lsblk"), { QStringLiteral("--nodeps"), QStringLiteral("--noheadings"), QStringLiteral("--output"), QStringLiteral("model"), deviceNode }); ExternalCommand sizeCommand(QStringLiteral("blockdev"), { QStringLiteral("--getsize64"), deviceNode }); ExternalCommand sizeCommand2(QStringLiteral("blockdev"), { QStringLiteral("--getss"), deviceNode }); ExternalCommand sfdiskJsonCommand(QStringLiteral("sfdisk"), { QStringLiteral("--json"), deviceNode }, QProcess::ProcessChannelMode::SeparateChannels ); if ( sizeCommand.run(-1) && sizeCommand.exitCode() == 0 && sizeCommand2.run(-1) && sizeCommand2.exitCode() == 0 && sfdiskJsonCommand.run(-1) ) { Device* d = nullptr; qint64 deviceSize = sizeCommand.output().trimmed().toLongLong(); int logicalSectorSize = sizeCommand2.output().trimmed().toLongLong(); QFile mdstat(QStringLiteral("/proc/mdstat")); if (mdstat.open(QIODevice::ReadOnly)) { QTextStream stream(&mdstat); QString content = stream.readAll(); mdstat.close(); QRegularExpression re(QStringLiteral("md([\\/\\w]+)\\s+:")); QRegularExpressionMatchIterator i = re.globalMatch(content); while (i.hasNext()) { QRegularExpressionMatch reMatch = i.next(); QString name = reMatch.captured(1); if ((QStringLiteral("/dev/md") + name) == deviceNode) { Log(Log::Level::information) << xi18nc("@info:status", "Software RAID Device found: %1", deviceNode); d = new SoftwareRAID( QStringLiteral("md") + name, SoftwareRAID::Status::Active ); break; } } } if ( d == nullptr && modelCommand.run(-1) && modelCommand.exitCode() == 0 ) { QString name = modelCommand.output(); name = name.left(name.length() - 1).replace(QLatin1Char('_'), QLatin1Char(' ')); if (name.trimmed().isEmpty()) { // Get 'lsblk --output kname' in the cases where the model name is not available. // As lsblk doesn't have an option to include a separator in its output, it is // necessary to run it again getting only the kname as output. ExternalCommand kname(QStringLiteral("lsblk"), {QStringLiteral("--nodeps"), QStringLiteral("--noheadings"), QStringLiteral("--output"), QStringLiteral("kname"), deviceNode}); if (kname.run(-1) && kname.exitCode() == 0) name = kname.output().trimmed(); } ExternalCommand transport(QStringLiteral("lsblk"), {QStringLiteral("--nodeps"), QStringLiteral("--noheadings"), QStringLiteral("--output"), QStringLiteral("tran"), deviceNode}); QString icon; if (transport.run(-1) && transport.exitCode() == 0) if (transport.output().trimmed() == QStringLiteral("usb")) icon = QStringLiteral("drive-removable-media-usb"); Log(Log::Level::information) << xi18nc("@info:status", "Device found: %1", name); d = new DiskDevice(name, deviceNode, 255, 63, deviceSize / logicalSectorSize / 255 / 63, logicalSectorSize, icon); } if ( d ) { if (sfdiskJsonCommand.exitCode() != 0) { scanWholeDevicePartition(*d); return d; } auto s = sfdiskJsonCommand.rawOutput(); fixInvalidJsonFromSFDisk(s); const QJsonObject jsonObject = QJsonDocument::fromJson(s).object(); const QJsonObject partitionTable = jsonObject[QLatin1String("partitiontable")].toObject(); if (!updateDevicePartitionTable(*d, partitionTable)) return nullptr; return d; } } else { // Look if this device is a LVM VG ExternalCommand checkVG(QStringLiteral("lvm"), { QStringLiteral("vgdisplay"), deviceNode }); if (checkVG.run(-1) && checkVG.exitCode() == 0) { QList availableDevices = scanDevices(); for (Device *device : std::as_const(availableDevices)) if (device && device->deviceNode() == deviceNode) return device; } } return nullptr; } /** Scans a Device for FileSystems spanning the whole block device This method will scan a Device for a FileSystem. It tries to determine the FileSystem usage, reads the FileSystem label and creates PartitionTable of type "none" and a single Partition object. */ void SfdiskBackend::scanWholeDevicePartition(Device& d) { const QString partitionNode = d.deviceNode(); constexpr qint64 firstSector = 0; const qint64 lastSector = d.totalLogical() - 1; setPartitionTableForDevice(d, new PartitionTable(PartitionTable::TableType::none, firstSector, lastSector)); Partition *partition = scanPartition(d, partitionNode, firstSector, lastSector, QString(), false); if (partition->fileSystem().type() == FileSystem::Type::Unknown) { setPartitionTableForDevice(d, nullptr); delete d.partitionTable(); } if (!partition->roles().has(PartitionRole::Luks)) readSectorsUsed(d, *partition, partition->mountPoint()); } /** Scans a Device for Partitions. This method will scan a Device for all Partitions on it, detect the FileSystem for each Partition, try to determine the FileSystem usage, read the FileSystem label and store it all in newly created objects that are in the end added to the Device's PartitionTable. */ void SfdiskBackend::scanDevicePartitions(Device& d, const QJsonArray& jsonPartitions) { Q_ASSERT(d.partitionTable()); QList partitions; for (const auto &partition : jsonPartitions) { const QJsonObject partitionObject = partition.toObject(); const QString partitionNode = partitionObject[QLatin1String("node")].toString(); const qint64 start = partitionObject[QLatin1String("start")].toVariant().toLongLong(); const qint64 size = partitionObject[QLatin1String("size")].toVariant().toLongLong(); const QString partitionType = partitionObject[QLatin1String("type")].toString(); const bool bootable = partitionObject[QLatin1String("bootable")].toBool(); const auto lastSector = start + size - 1; Partition* part = scanPartition(d, partitionNode, start, lastSector, partitionType, bootable); setupPartitionInfo(d, part, partitionObject); partitions.append(part); } d.partitionTable()->updateUnallocated(d); if (d.partitionTable()->isSectorBased(d)) d.partitionTable()->setType(d, PartitionTable::msdos_sectorbased); for (const Partition *part : std::as_const(partitions)) PartitionAlignment::isAligned(d, *part); } Partition* SfdiskBackend::scanPartition(Device& d, const QString& partitionNode, const qint64 firstSector, const qint64 lastSector, const QString& partitionType, const bool bootable) { PartitionTable::Flags activeFlags = bootable ? PartitionTable::Flag::Boot : PartitionTable::Flag::None; if (partitionType == QStringLiteral("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")) activeFlags |= PartitionTable::Flag::Boot; else if (partitionType == QStringLiteral("21686148-6449-6E6F-744E-656564454649")) activeFlags |= PartitionTable::Flag::BiosGrub; FileSystem::Type type = detectFileSystem(partitionNode); PartitionRole::Roles r = PartitionRole::Primary; if ( (d.partitionTable()->type() == PartitionTable::msdos || d.partitionTable()->type() == PartitionTable::msdos_sectorbased) && ( partitionType == QStringLiteral("5") || partitionType == QStringLiteral("f") ) ) { r = PartitionRole::Extended; type = FileSystem::Type::Extended; } // Find an extended partition this partition is in. PartitionNode* parent = d.partitionTable()->findPartitionBySector(firstSector, PartitionRole(PartitionRole::Extended)); // None found, so it's a primary in the device's partition table. if (parent == nullptr) parent = d.partitionTable(); else r = PartitionRole::Logical; FileSystem* fs = FileSystemFactory::create(type, firstSector, lastSector, d.logicalSize()); fs->scan(partitionNode); QString mountPoint; bool mounted; // sfdisk does not handle LUKS partitions if (fs->type() == FileSystem::Type::Luks || fs->type() == FileSystem::Type::Luks2) { r |= PartitionRole::Luks; FS::luks* luksFs = static_cast(fs); luksFs->initLUKS(); QString mapperNode = luksFs->mapperName(); mountPoint = FileSystem::detectMountPoint(fs, mapperNode); mounted = FileSystem::detectMountStatus(fs, mapperNode); } else { mountPoint = FileSystem::detectMountPoint(fs, partitionNode); mounted = FileSystem::detectMountStatus(fs, partitionNode); } Partition* partition = new Partition(parent, d, PartitionRole(r), fs, firstSector, lastSector, partitionNode, availableFlags(d.partitionTable()->type()), mountPoint, mounted, activeFlags); if (fs->supportGetLabel() != FileSystem::cmdSupportNone) fs->setLabel(fs->readLabel(partition->deviceNode())); if (fs->supportGetUUID() != FileSystem::cmdSupportNone) fs->setUUID(fs->readUUID(partition->deviceNode())); parent->append(partition); return partition; } void SfdiskBackend::setupPartitionInfo(const Device &d, Partition *partition, const QJsonObject& partitionObject) { if (!partition->roles().has(PartitionRole::Luks)) readSectorsUsed(d, *partition, partition->mountPoint()); if (d.partitionTable()->type() == PartitionTable::TableType::gpt) { partition->setLabel(partitionObject[QLatin1String("name")].toString()); partition->setUUID(partitionObject[QLatin1String("uuid")].toString()); partition->setType(partitionObject[QLatin1String("type")].toString()); QString attrs = partitionObject[QLatin1String("attrs")].toString(); partition->setAttributes(SfdiskGptAttributes::toULongLong(attrs.split(QLatin1Char(' ')))); } } bool SfdiskBackend::updateDevicePartitionTable(Device &d, const QJsonObject &jsonPartitionTable) { QString tableType = jsonPartitionTable[QLatin1String("label")].toString(); const PartitionTable::TableType type = PartitionTable::nameToTableType(tableType); qint64 firstUsableSector = 0; qint64 lastUsableSector = 0; if (d.type() == Device::Type::Disk_Device) { const DiskDevice* diskDevice = static_cast(&d); lastUsableSector = diskDevice->totalSectors(); } else if (d.type() == Device::Type::SoftwareRAID_Device) { const SoftwareRAID* raidDevice = static_cast(&d); lastUsableSector = raidDevice->totalLogical() - 1; } if (type == PartitionTable::gpt) { firstUsableSector = jsonPartitionTable[QLatin1String("firstlba")].toVariant().toLongLong(); lastUsableSector = jsonPartitionTable[QLatin1String("lastlba")].toVariant().toLongLong(); } if (lastUsableSector < firstUsableSector) { return false; } setPartitionTableForDevice(d, new PartitionTable(type, firstUsableSector, lastUsableSector)); switch (type) { case PartitionTable::gpt: { // Read the maximum number of GPT partitions qint32 maxEntries; QByteArray gptHeader; CopySourceDevice source(d, 512, 1023); CopyTargetByteArray target(gptHeader); ExternalCommand copyCmd; if (copyCmd.copyBlocks(source, target)) { QByteArray gptMaxEntries = gptHeader.mid(80, 4); QDataStream stream(&gptMaxEntries, QIODevice::ReadOnly); stream.setByteOrder(QDataStream::LittleEndian); stream >> maxEntries; } else maxEntries = 128; CoreBackend::setPartitionTableMaxPrimaries(*d.partitionTable(), maxEntries); break; } default: break; } scanDevicePartitions(d, jsonPartitionTable[QLatin1String("partitions")].toArray()); return true; } /** Reads the sectors used in a FileSystem and stores the result in the Partition's FileSystem object. @param p the Partition the FileSystem is on @param mountPoint mount point of the partition in question */ void SfdiskBackend::readSectorsUsed(const Device& d, Partition& p, const QString& mountPoint) { if (!mountPoint.isEmpty() && p.fileSystem().type() != FileSystem::Type::LinuxSwap && p.fileSystem().type() != FileSystem::Type::Lvm2_PV) { const QStorageInfo storage = QStorageInfo(mountPoint); if (p.isMounted() && storage.isValid()) p.fileSystem().setSectorsUsed( (storage.bytesTotal() - storage.bytesFree()) / d.logicalSize()); } else if (p.fileSystem().supportGetUsed() == FileSystem::cmdSupportFileSystem) p.fileSystem().setSectorsUsed(p.fileSystem().readUsedCapacity(p.deviceNode()) / d.logicalSize()); } FileSystem::Type SfdiskBackend::detectFileSystem(const QString& partitionPath) { FileSystem::Type rval = FileSystem::Type::Unknown; ExternalCommand udevCommand(QStringLiteral("udevadm"), { QStringLiteral("info"), QStringLiteral("--query=property"), partitionPath }); QString typeRegExp = QStringLiteral("ID_FS_TYPE=(\\w+)"); QString versionRegExp = QStringLiteral("ID_FS_VERSION=(\\w+)"); QString name = {}; rval = runDetectFileSystemCommand(udevCommand, typeRegExp, versionRegExp, name); // Fallback to blkid which has slightly worse detection but it works on whole block device filesystems. if (rval == FileSystem::Type::Unknown) { ExternalCommand blkidCommand(QStringLiteral("blkid"), { partitionPath }); typeRegExp = QStringLiteral("TYPE=\"(\\w+)\""); versionRegExp = QStringLiteral("SEC_TYPE=\"(\\w+)\""); rval = runDetectFileSystemCommand(blkidCommand, typeRegExp, versionRegExp, name); } if (rval == FileSystem::Type::Unknown) { qWarning() << "unknown file system type " << name << " on " << partitionPath; } return rval; } FileSystem::Type SfdiskBackend::runDetectFileSystemCommand(ExternalCommand& command, QString& typeRegExp, QString& versionRegExp, QString& name) { FileSystem::Type rval = FileSystem::Type::Unknown; if (command.run(-1) && command.exitCode() == 0) { QRegularExpression re(typeRegExp); QRegularExpression re2(versionRegExp); QRegularExpressionMatch reFileSystemType = re.match(command.output()); QRegularExpressionMatch reFileSystemVersion = re2.match(command.output()); if (reFileSystemType.hasMatch()) { name = reFileSystemType.captured(1); } QString version = {}; if (reFileSystemVersion.hasMatch()) { version = reFileSystemVersion.captured(1); } rval = fileSystemNameToType(name, version); } return rval; } FileSystem::Type SfdiskBackend::fileSystemNameToType(const QString& name, const QString& version) { FileSystem::Type rval = FileSystem::Type::Unknown; if (name == QStringLiteral("ext2")) rval = FileSystem::Type::Ext2; else if (name == QStringLiteral("ext3")) rval = FileSystem::Type::Ext3; else if (name.startsWith(QStringLiteral("ext4"))) rval = FileSystem::Type::Ext4; else if (name == QStringLiteral("swap")) rval = FileSystem::Type::LinuxSwap; else if (name == QStringLiteral("ntfs")) rval = FileSystem::Type::Ntfs; else if (name == QStringLiteral("reiserfs")) rval = FileSystem::Type::ReiserFS; else if (name == QStringLiteral("reiser4")) rval = FileSystem::Type::Reiser4; else if (name == QStringLiteral("xfs")) rval = FileSystem::Type::Xfs; else if (name == QStringLiteral("jfs")) rval = FileSystem::Type::Jfs; else if (name == QStringLiteral("hfs")) rval = FileSystem::Type::Hfs; else if (name == QStringLiteral("hfsplus")) rval = FileSystem::Type::HfsPlus; else if (name == QStringLiteral("ufs")) rval = FileSystem::Type::Ufs; else if (name == QStringLiteral("vfat")) { if (version == QStringLiteral("FAT32")) rval = FileSystem::Type::Fat32; else if (version == QStringLiteral("FAT16") || version == QStringLiteral("msdos")) // blkid uses msdos for both FAT16 and FAT12 rval = FileSystem::Type::Fat16; else if (version == QStringLiteral("FAT12")) rval = FileSystem::Type::Fat12; } else if (name == QStringLiteral("btrfs")) rval = FileSystem::Type::Btrfs; else if (name == QStringLiteral("ocfs2")) rval = FileSystem::Type::Ocfs2; else if (name == QStringLiteral("zfs_member")) rval = FileSystem::Type::Zfs; else if (name == QStringLiteral("hpfs")) rval = FileSystem::Type::Hpfs; else if (name == QStringLiteral("crypto_LUKS")) { if (version == QStringLiteral("1")) rval = FileSystem::Type::Luks; else if (version == QStringLiteral("2")) { rval = FileSystem::Type::Luks2; } } else if (name == QStringLiteral("exfat")) rval = FileSystem::Type::Exfat; else if (name == QStringLiteral("nilfs2")) rval = FileSystem::Type::Nilfs2; else if (name == QStringLiteral("LVM2_member")) rval = FileSystem::Type::Lvm2_PV; else if (name == QStringLiteral("f2fs")) rval = FileSystem::Type::F2fs; else if (name == QStringLiteral("udf")) rval = FileSystem::Type::Udf; else if (name == QStringLiteral("iso9660")) rval = FileSystem::Type::Iso9660; else if (name == QStringLiteral("linux_raid_member")) rval = FileSystem::Type::LinuxRaidMember; else if (name == QStringLiteral("BitLocker")) rval = FileSystem::Type::BitLocker; else if (name == QStringLiteral("apfs")) rval = FileSystem::Type::Apfs; else if (name == QStringLiteral("minix")) rval = FileSystem::Type::Minix; return rval; } QString SfdiskBackend::readLabel(const QString& deviceNode) const { ExternalCommand udevCommand(QStringLiteral("udevadm"), { QStringLiteral("info"), QStringLiteral("--query=property"), deviceNode }); udevCommand.run(); QRegularExpression re(QStringLiteral("ID_FS_LABEL=(.*)")); QRegularExpressionMatch reFileSystemLabel = re.match(udevCommand.output()); if (reFileSystemLabel.hasMatch()) return reFileSystemLabel.captured(1); return QString(); } QString SfdiskBackend::readUUID(const QString& deviceNode) const { ExternalCommand udevCommand(QStringLiteral("udevadm"), { QStringLiteral("info"), QStringLiteral("--query=property"), deviceNode }); udevCommand.run(); QRegularExpression re(QStringLiteral("ID_FS_UUID=(.*)")); QRegularExpressionMatch reFileSystemUUID = re.match(udevCommand.output()); if (reFileSystemUUID.hasMatch()) return reFileSystemUUID.captured(1); return QString(); } PartitionTable::Flags SfdiskBackend::availableFlags(PartitionTable::TableType type) { PartitionTable::Flags flags; if (type == PartitionTable::gpt) { // These are not really flags but for now keep them for compatibility // We should implement changing partition type flags = PartitionTable::Flag::BiosGrub | PartitionTable::Flag::Boot; } else if (type == PartitionTable::msdos || type == PartitionTable::msdos_sectorbased) flags = PartitionTable::Flag::Boot; return flags; } std::unique_ptr SfdiskBackend::openDevice(const Device& d) { std::unique_ptr device = std::make_unique(d); if (!device->open()) device = nullptr; return device; } std::unique_ptr SfdiskBackend::openDeviceExclusive(const Device& d) { std::unique_ptr device = std::make_unique(d); if (!device->openExclusive()) device = nullptr; return device; } bool SfdiskBackend::closeDevice(std::unique_ptr coreDevice) { return coreDevice->close(); } #include "sfdiskbackend.moc"