From 142ddfc0370e75a2f49e8c119e5b467f38e6f839 Mon Sep 17 00:00:00 2001 From: Floris Bos Date: Mon, 14 Nov 2022 20:18:36 +0100 Subject: [PATCH] Add classes for mounting FAT16/32 without help from OS Minimal implementation for reading/writing files in the root directory of a FAT16/FAT32 file system. Can read/write from raw disk devices, and no longer relies on operating system support for mounting the file system. Currently assumes Imager will always be run on 'little endian' architectures such as Intel and ARM (at least under Linux). If there is a use-case for big-endian (anybody still using Sparc?) this may be revisited later. --- src/CMakeLists.txt | 10 +- src/devicewrapper.cpp | 175 ++++++++ src/devicewrapper.h | 50 +++ src/devicewrapperblockcacheentry.cpp | 19 + src/devicewrapperblockcacheentry.h | 21 + src/devicewrapperfatpartition.cpp | 634 +++++++++++++++++++++++++++ src/devicewrapperfatpartition.h | 54 +++ src/devicewrapperpartition.cpp | 54 +++ src/devicewrapperpartition.h | 32 ++ src/devicewrapperstructs.h | 145 ++++++ 10 files changed, 1193 insertions(+), 1 deletion(-) create mode 100644 src/devicewrapper.cpp create mode 100644 src/devicewrapper.h create mode 100644 src/devicewrapperblockcacheentry.cpp create mode 100644 src/devicewrapperblockcacheentry.h create mode 100644 src/devicewrapperfatpartition.cpp create mode 100644 src/devicewrapperfatpartition.h create mode 100644 src/devicewrapperpartition.cpp create mode 100644 src/devicewrapperpartition.h create mode 100644 src/devicewrapperstructs.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9b86c3a..df5bf53 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -23,6 +23,7 @@ set(CMAKE_AUTORCC ON) # Adding headers explicity so they are displayed in Qt Creator set(HEADERS config.h imagewriter.h networkaccessmanagerfactory.h nan.h drivelistitem.h drivelistmodel.h drivelistmodelpollthread.h driveformatthread.h powersaveblocker.h cli.h + devicewrapper.h devicewrapperblockcacheentry.h devicewrapperpartition.h devicewrapperstructs.h devicewrapperfatpartition.h downloadthread.h downloadextractthread.h localfileextractthread.h downloadstatstelemetry.h dependencies/mountutils/src/mountutils.hpp dependencies/sha256crypt/sha256crypt.h) # Add dependencies @@ -88,9 +89,16 @@ if (NOT atomicbuiltin) endif() endif() +include(TestBigEndian) +test_big_endian(IS_BIG_ENDIAN) +if( IS_BIG_ENDIAN ) + message( FATAL_ERROR "We currently only support 'little endian' CPU architectures" ) +endif( IS_BIG_ENDIAN ) + set(SOURCES "main.cpp" "imagewriter.cpp" "networkaccessmanagerfactory.cpp" "drivelistitem.cpp" "drivelistmodel.cpp" "drivelistmodelpollthread.cpp" "downloadthread.cpp" "downloadextractthread.cpp" - "driveformatthread.cpp" "localfileextractthread.cpp" "powersaveblocker.cpp" "downloadstatstelemetry.cpp" "qml.qrc" "dependencies/sha256crypt/sha256crypt.c" "cli.cpp") + "devicewrapper.cpp" "devicewrapperblockcacheentry.cpp" "devicewrapperpartition.cpp" "devicewrapperfatpartition.cpp" + "driveformatthread.cpp" "localfileextractthread.cpp" "powersaveblocker.cpp" "downloadstatstelemetry.cpp" "qml.qrc" "dependencies/sha256crypt/sha256crypt.c" "cli.cpp") find_package(Qt5 COMPONENTS Core Quick LinguistTools Svg OPTIONAL_COMPONENTS Widgets) if (Qt5Widgets_FOUND) diff --git a/src/devicewrapper.cpp b/src/devicewrapper.cpp new file mode 100644 index 0000000..988b6b9 --- /dev/null +++ b/src/devicewrapper.cpp @@ -0,0 +1,175 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2022 Raspberry Pi Ltd + */ + +#include "devicewrapper.h" +#include "devicewrapperblockcacheentry.h" +#include "devicewrapperstructs.h" +#include "devicewrapperfatpartition.h" +#include + +DeviceWrapper::DeviceWrapper(DeviceWrapperFile *file, QObject *parent) + : QObject(parent), _dirty(false), _file(file) +{ + +} + +DeviceWrapper::~DeviceWrapper() +{ + sync(); +} + +void DeviceWrapper::_seekToBlock(quint64 blockNr) +{ + if (!_file->seek(blockNr*4096)) + { + throw std::runtime_error("Error seeking device"); + } +} + +void DeviceWrapper::sync() +{ + if (!_dirty) + return; + + const auto blockNrs = _blockcache.keys(); + for (auto blockNr : blockNrs) + { + if (blockNr == 0) + continue; /* Save writing first block with MBR for last */ + + auto block = _blockcache.value(blockNr); + + if (!block->dirty) + continue; + + _seekToBlock(blockNr); + if (_file->write(block->block, 4096) != 4096) + { + std::string errmsg = "Error writing to device: "+_file->errorString().toStdString(); + throw std::runtime_error(errmsg); + } + block->dirty = false; + } + + if (_blockcache.contains(0)) + { + /* Write first block with MBR */ + auto block = _blockcache.value(0); + + if (block->dirty) + { + _seekToBlock(0); + if (_file->write(block->block, 4096) != 4096) + { + std::string errmsg = "Error writing MBR to device: "+_file->errorString().toStdString(); + throw std::runtime_error(errmsg); + } + block->dirty = false; + } + } + + _dirty = false; +} + +void DeviceWrapper::_readIntoBlockCacheIfNeeded(quint64 offset, quint64 size) +{ + if (!size) + return; + + quint64 firstBlock = offset/4096; + quint64 lastBlock = (offset+size)/4096; + + for (auto i = firstBlock; i <= lastBlock; i++) + { + if (!_blockcache.contains(i)) + { + _seekToBlock(i); + + auto cacheEntry = new DeviceWrapperBlockCacheEntry(this); + int bytesRead = _file->read(cacheEntry->block, 4096); + if (bytesRead != 4096) + { + std::string errmsg = "Error reading from device: "+_file->errorString().toStdString(); + throw std::runtime_error(errmsg); + } + _blockcache.insert(i, cacheEntry); + } + } +} + +void DeviceWrapper::pread(char *buf, quint64 size, quint64 offset) +{ + if (!size) + return; + + _readIntoBlockCacheIfNeeded(offset, size); + quint64 firstBlock = offset / 4096; + quint64 offsetInBlock = offset % 4096; + + for (auto i = firstBlock; size; i++) + { + auto block = _blockcache.value(i); + size_t bytesToCopyFromBlock = qMin(4096-offsetInBlock, size); + memcpy(buf, block->block + offsetInBlock, bytesToCopyFromBlock); + + buf += bytesToCopyFromBlock; + size -= bytesToCopyFromBlock; + offsetInBlock = 0; + } +} + +void DeviceWrapper::pwrite(const char *buf, quint64 size, quint64 offset) +{ + if (!size) + return; + + quint64 firstBlock = offset / 4096; + quint64 offsetInBlock = offset % 4096; + + if (offsetInBlock || size % 4096) + { + /* Need to read existing data from disk + as we will only be replacing a part of a block. */ + _readIntoBlockCacheIfNeeded(offset, size); + } + + for (auto i = firstBlock; size; i++) + { + auto block = _blockcache.value(i, NULL); + if (!block) + { + block = new DeviceWrapperBlockCacheEntry(this); + _blockcache.insert(i, block); + } + + block->dirty = true; + size_t bytesToCopyFromBlock = qMin(4096-offsetInBlock, size); + memcpy(block->block + offsetInBlock, buf, bytesToCopyFromBlock); + + buf += bytesToCopyFromBlock; + size -= bytesToCopyFromBlock; + offsetInBlock = 0; + } + + _dirty = true; +} + +DeviceWrapperFatPartition *DeviceWrapper::fatPartition(int nr) +{ + if (nr > 4 || nr < 1) + throw std::runtime_error("Only basic partitions 1-4 supported"); + + struct mbr_table mbr; + pread((char *) &mbr, sizeof(mbr), 0); + + if (mbr.signature[0] != 0x55 || mbr.signature[1] != 0xAA) + throw std::runtime_error("MBR does not have valid signature"); + + if (!mbr.part[nr-1].starting_sector || !mbr.part[nr-1].nr_of_sectors) + throw std::runtime_error("Partition does not exist"); + + return new DeviceWrapperFatPartition(this, mbr.part[nr-1].starting_sector*512, mbr.part[nr-1].nr_of_sectors*512, this); +} + diff --git a/src/devicewrapper.h b/src/devicewrapper.h new file mode 100644 index 0000000..c5b6d14 --- /dev/null +++ b/src/devicewrapper.h @@ -0,0 +1,50 @@ +#ifndef DEVICEWRAPPER_H +#define DEVICEWRAPPER_H + +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2022 Raspberry Pi Ltd + */ + +#include +#include +#include + +class DeviceWrapperBlockCacheEntry; +class DeviceWrapperFatPartition; + +#ifdef Q_OS_WIN +#include "windows/winfile.h" +typedef WinFile DeviceWrapperFile; +#elif defined(Q_OS_DARWIN) +#include "mac/macfile.h" +typedef MacFile DeviceWrapperFile; +#else +typedef QFile DeviceWrapperFile; +#endif + + +class DeviceWrapper : public QObject +{ + Q_OBJECT +public: + explicit DeviceWrapper(DeviceWrapperFile *file, QObject *parent = nullptr); + virtual ~DeviceWrapper(); + void sync(); + void pwrite(const char *buf, quint64 size, quint64 offset); + void pread(char *buf, quint64 size, quint64 offset); + DeviceWrapperFatPartition *fatPartition(int nr); + +protected: + bool _dirty; + QMap _blockcache; + DeviceWrapperFile *_file; + + void _readIntoBlockCacheIfNeeded(quint64 offset, quint64 size); + void _seekToBlock(quint64 blockNr); + +signals: + +}; + +#endif // DEVICEWRAPPER_H diff --git a/src/devicewrapperblockcacheentry.cpp b/src/devicewrapperblockcacheentry.cpp new file mode 100644 index 0000000..2879dc5 --- /dev/null +++ b/src/devicewrapperblockcacheentry.cpp @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2022 Raspberry Pi Ltd + */ + +#include "devicewrapperblockcacheentry.h" +#include + +DeviceWrapperBlockCacheEntry::DeviceWrapperBlockCacheEntry(QObject *parent, size_t blocksize) + : QObject(parent), dirty(false) +{ + /* Windows requires buffers to be 4k aligned when reading/writing raw disk devices */ + block = (char *) qMallocAligned(blocksize, 4096); +} + +DeviceWrapperBlockCacheEntry::~DeviceWrapperBlockCacheEntry() +{ + qFreeAligned(block); +} diff --git a/src/devicewrapperblockcacheentry.h b/src/devicewrapperblockcacheentry.h new file mode 100644 index 0000000..402cc2e --- /dev/null +++ b/src/devicewrapperblockcacheentry.h @@ -0,0 +1,21 @@ +#ifndef DEVICEWRAPPERBLOCKCACHEENTRY_H +#define DEVICEWRAPPERBLOCKCACHEENTRY_H + +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2022 Raspberry Pi Ltd + */ + +#include + +class DeviceWrapperBlockCacheEntry : QObject +{ + Q_OBJECT +public: + DeviceWrapperBlockCacheEntry(QObject *parent, size_t blocksize = 4096); + ~DeviceWrapperBlockCacheEntry(); + char *block; + bool dirty; +}; + +#endif // DEVICEWRAPPERBLOCKCACHEENTRY_H diff --git a/src/devicewrapperfatpartition.cpp b/src/devicewrapperfatpartition.cpp new file mode 100644 index 0000000..28340df --- /dev/null +++ b/src/devicewrapperfatpartition.cpp @@ -0,0 +1,634 @@ +#include "devicewrapperfatpartition.h" +#include "devicewrapperstructs.h" +#include "qdebug.h" + +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2022 Raspberry Pi Ltd + */ + +DeviceWrapperFatPartition::DeviceWrapperFatPartition(DeviceWrapper *dw, quint64 partStart, quint64 partLen, QObject *parent) + : DeviceWrapperPartition(dw, partStart, partLen, parent) +{ + union fat_bpb bpb; + + read((char *) &bpb, sizeof(bpb)); + + if (bpb.fat16.Signature[0] != 0x55 || bpb.fat16.Signature[1] != 0xAA) + throw std::runtime_error("Partition does not have a FAT file system"); + + /* Determine FAT type as per p. 14 https://academy.cba.mit.edu/classes/networking_communications/SD/FAT.pdf */ + _bytesPerSector = bpb.fat16.BPB_BytsPerSec; + uint32_t totalSectors, dataSectors, countOfClusters; + _fat16_rootDirSectors = ((bpb.fat16.BPB_RootEntCnt * 32) + (_bytesPerSector - 1)) / _bytesPerSector; + + if (bpb.fat16.BPB_FATSz16) + _fatSize = bpb.fat16.BPB_FATSz16; + else + _fatSize = bpb.fat32.BPB_FATSz32; + + if (bpb.fat16.BPB_TotSec16) + totalSectors = bpb.fat16.BPB_TotSec16; + else + totalSectors = bpb.fat32.BPB_TotSec32; + + dataSectors = totalSectors - (bpb.fat16.BPB_RsvdSecCnt + (bpb.fat16.BPB_NumFATs * _fatSize) + _fat16_rootDirSectors); + countOfClusters = dataSectors / bpb.fat16.BPB_SecPerClus; + _bytesPerCluster = bpb.fat16.BPB_SecPerClus * _bytesPerSector; + _fat16_firstRootDirSector = bpb.fat16.BPB_RsvdSecCnt + (bpb.fat16.BPB_NumFATs * bpb.fat16.BPB_FATSz16); + _fat32_firstRootDirCluster = bpb.fat32.BPB_RootClus; + + if (!_bytesPerSector) + _type = EXFAT; + else if (countOfClusters < 4085) + _type = FAT12; + else if (countOfClusters < 65525) + _type = FAT16; + else + _type = FAT32; + + if (_type == FAT12) + throw std::runtime_error("FAT12 file system not supported"); + if (_type == EXFAT) + throw std::runtime_error("exFAT file system not supported"); + if (_bytesPerSector % 4) + throw std::runtime_error("FAT file system: invalid bytes per sector"); + + _firstFatStartOffset = bpb.fat16.BPB_RsvdSecCnt * _bytesPerSector; + for (int i = 0; i < bpb.fat16.BPB_NumFATs; i++) + { + _fatStartOffset.append(_firstFatStartOffset + (i * _fatSize * _bytesPerSector)); + } + + if (_type == FAT16) + { + _fat32_fsinfoSector = 0; + _clusterOffset = (_fat16_firstRootDirSector+_fat16_rootDirSectors) * _bytesPerSector; + } + else + { + _fat32_fsinfoSector = bpb.fat32.BPB_FSInfo; + _clusterOffset = _firstFatStartOffset + (bpb.fat16.BPB_NumFATs * _fatSize * _bytesPerSector); + } +} + +uint32_t DeviceWrapperFatPartition::allocateCluster() +{ + char sector[_bytesPerSector]; + int bytesPerEntry = (_type == FAT16 ? 2 : 4); + int entriesPerSector = _bytesPerSector/bytesPerEntry; + uint32_t cluster; + uint16_t *f16 = (uint16_t *) §or; + uint32_t *f32 = (uint32_t *) §or; + + seek(_firstFatStartOffset); + + for (int i = 0; i < _fatSize; i++) + { + read(sector, sizeof(sector)); + + for (int j=0; j < entriesPerSector; j++) + { + if (_type == FAT16) + { + if (f16[j] == 0) + { + /* Found available FAT16 cluster, mark it used/EOF */ + cluster = j+i*entriesPerSector; + setFAT16(cluster, 0xFFFF); + return cluster; + } + } + else + { + if ( (f32[j] & 0x0FFFFFFF) == 0) + { + /* Found available FAT32 cluster, mark it used/EOF */ + cluster = j+i*entriesPerSector; + setFAT32(cluster, 0xFFFFFFF); + updateFSinfo(-1, cluster); + return cluster; + } + } + } + } + + throw std::runtime_error("Out of disk space on FAT partition"); +} + +uint32_t DeviceWrapperFatPartition::allocateCluster(uint32_t previousCluster) +{ + uint32_t newCluster = allocateCluster(); + + if (previousCluster) + { + if (_type == FAT16) + setFAT16(previousCluster, newCluster); + else + setFAT32(previousCluster, newCluster); + } + + return newCluster; +} + +void DeviceWrapperFatPartition::setFAT16(uint16_t cluster, uint16_t value) +{ + /* Modify all FATs (usually 2) */ + for (auto fatStart : qAsConst(_fatStartOffset)) + { + seek(fatStart + cluster * 2); + write((char *) &value, 2); + } +} + +void DeviceWrapperFatPartition::setFAT32(uint32_t cluster, uint32_t value) +{ + uint32_t prev_value, reserved_bits; + + /* Modify all FATs (usually 2) */ + for (auto fatStart : qAsConst(_fatStartOffset)) + { + /* Spec (p. 16) mentions we must preserve high 4 bits of FAT32 FAT entry when modifiying */ + seek(fatStart + cluster * 4); + read( (char *) &prev_value, 4); + reserved_bits = prev_value & 0xF0000000; + value |= reserved_bits; + + seek(fatStart + cluster * 4); + write((char *) &value, 4); + } +} + +void DeviceWrapperFatPartition::setFAT(uint32_t cluster, uint32_t value) +{ + if (_type == FAT16) + setFAT16(cluster, value); + else + setFAT32(cluster, value); +} + +uint32_t DeviceWrapperFatPartition::getFAT(uint32_t cluster) +{ + if (_type == FAT16) + { + uint16_t result; + seek(_firstFatStartOffset + cluster * 2); + read((char *) &result, 2); + return result; + } + else + { + uint32_t result; + seek(_firstFatStartOffset + cluster * 4); + read((char *) &result, 4); + return result & 0x0FFFFFFF; + } +} + +QList DeviceWrapperFatPartition::getClusterChain(uint32_t firstCluster) +{ + QList list; + uint32_t cluster = firstCluster; + + while (true) + { + if ( (_type == FAT16 && cluster > 0xFFF7) + || (_type == FAT32 && cluster > 0xFFFFFF7)) + { + /* Reached EOF */ + break; + } + + if (list.contains(cluster)) + throw std::runtime_error("Corrupt file system. Circular references in FAT table"); + + list.append(cluster); + cluster = getFAT(cluster); + } + + return list; +} + +void DeviceWrapperFatPartition::seekCluster(uint32_t cluster) +{ + seek(_clusterOffset + (cluster-2)*_bytesPerCluster); +} + +bool DeviceWrapperFatPartition::fileExists(const QString &filename) +{ + struct dir_entry entry; + return getDirEntry(filename, &entry); +} + +QByteArray DeviceWrapperFatPartition::readFile(const QString &filename) +{ + struct dir_entry entry; + + if (!getDirEntry(filename, &entry)) + return QByteArray(); /* File not found */ + + uint32_t firstCluster = entry.DIR_FstClusLO; + if (_type == FAT32) + firstCluster |= (entry.DIR_FstClusHI << 16); + QList clusterList = getClusterChain(firstCluster); + uint32_t len = entry.DIR_FileSize, pos = 0; + QByteArray result(len, 0); + + for (uint32_t cluster : qAsConst(clusterList)) + { + seekCluster(cluster); + read(result.data()+pos, qMin(_bytesPerCluster, len-pos)); + + pos += _bytesPerCluster; + if (pos >= len) + break; + } + + return result; +} + +void DeviceWrapperFatPartition::writeFile(const QString &filename, const QByteArray &contents) +{ + QList clusterList; + uint32_t pos = 0; + uint32_t firstCluster; + int clustersNeeded = (contents.length() + _bytesPerCluster - 1) / _bytesPerCluster; + struct dir_entry entry; + + getDirEntry(filename, &entry, true); + firstCluster = entry.DIR_FstClusLO; + if (_type == FAT32) + firstCluster |= (entry.DIR_FstClusHI << 16); + + if (firstCluster) + clusterList = getClusterChain(firstCluster); + + if (clusterList.length() < clustersNeeded) + { + /* We need to allocate more clusters */ + uint32_t lastCluster = 0; + int extraClustersNeeded = clustersNeeded - clusterList.length(); + + if (!clusterList.isEmpty()) + lastCluster = clusterList.last(); + + for (int i = 0; i < extraClustersNeeded; i++) + { + lastCluster = allocateCluster(lastCluster); + clusterList.append(lastCluster); + } + } + else if (clusterList.length() > clustersNeeded) + { + /* We need to remove excess clusters */ + int clustersToRemove = clusterList.length() - clustersNeeded; + uint32_t clusterToRemove = 0; + QByteArray zeroes(_bytesPerCluster, 0); + + for (int i=0; i < clustersToRemove; i++) + { + clusterToRemove = clusterList.takeLast(); + + /* Zero out previous data in excess clusters, + just in case someone wants to take a disk image later */ + seekCluster(clusterToRemove); + write(zeroes.data(), zeroes.length()); + + /* Mark cluster available again in FAT */ + setFAT(clusterToRemove, 0); + } + updateFSinfo(clustersToRemove, clusterToRemove); + + if (!clusterList.isEmpty()) + { + if (_type == FAT16) + setFAT16(clusterList.last(), 0xFFFF); + else + setFAT32(clusterList.last(), 0xFFFFFFF); + } + } + + //qDebug() << "First cluster:" << firstCluster << "Clusters:" << clusterList; + + /* Write file data */ + for (uint32_t cluster : qAsConst(clusterList)) + { + seekCluster(cluster); + write(contents.data()+pos, qMin(_bytesPerCluster, contents.length()-pos)); + + pos += _bytesPerCluster; + if (pos >= contents.length()) + break; + } + + if (clustersNeeded) + { + /* Zero out last cluster tip */ + uint32_t extraBytesAtEndOfCluster = _bytesPerCluster - (contents.length() % _bytesPerCluster); + if (extraBytesAtEndOfCluster) + { + QByteArray zeroes(extraBytesAtEndOfCluster, 0); + write(zeroes.data(), zeroes.length()); + } + } + + /* Update directory entry */ + if (clusterList.isEmpty()) + firstCluster = (_type == FAT16 ? 0xFFFF : 0xFFFFFFF); + else + firstCluster = clusterList.first(); + + entry.DIR_FstClusLO = (firstCluster & 0xFFFF); + entry.DIR_FstClusHI = (firstCluster >> 16); + entry.DIR_WrtDate = QDateToFATdate( QDate::currentDate() ); + entry.DIR_WrtTime = QTimeToFATtime( QTime::currentTime() ); + entry.DIR_LstAccDate = entry.DIR_WrtDate; + entry.DIR_FileSize = contents.length(); + updateDirEntry(&entry); +} + +bool DeviceWrapperFatPartition::getDirEntry(const QString &longFilename, struct dir_entry *entry, bool createIfNotExist) +{ + QString filenameRead, longFilenameLower = longFilename.toLower(); + + if (longFilename.isEmpty()) + throw std::runtime_error("Filename cannot not be empty"); + + openDir(); + while (readDir(entry)) + { + if (entry->DIR_Attr & ATTR_LONG_NAME) + { + struct longfn_entry *l = (struct longfn_entry *) entry; + /* A part can have 13 UTF-16 characters */ + QString lnamePart(13, QChar::Null); + char *lnamePartStr = (char *) lnamePart.data(); + /* Using memcpy() because it has no problems accessing unaligned struct members */ + memcpy(lnamePartStr, l->LDIR_Name1, 10); + memcpy(lnamePartStr+10, l->LDIR_Name2, 12); + memcpy(lnamePartStr+22, l->LDIR_Name3, 4); + filenameRead = lnamePart + filenameRead; + } + else + { + if (entry->DIR_Name[0] != 0xE5) + { + filenameRead.truncate(filenameRead.indexOf(QChar::Null)); + + //qDebug() << "Long filename:" << filenameRead << "Short:" << QByteArray(entry->DIR_Name, sizeof(entry->DIR_Name)); + + /* FIXME: should we check short file names as well, if they are not preceeded by long file name entry? */ + + if (filenameRead.toLower() == longFilenameLower) + { + return true; + } + } + + filenameRead.clear(); + } + } + + if (createIfNotExist) + { + QByteArray shortFilename; + uint8_t shortFileNameChecksum = 0; + struct longfn_entry longEntry; + + if (longFilename.count(".") == 1) + { + QList fnParts = longFilename.toLatin1().toUpper().split('.'); + shortFilename = fnParts[0].leftJustified(8, ' ', true)+fnParts[1].leftJustified(3, ' ', true); + } + else + { + shortFilename = longFilename.toLatin1().leftJustified(11, ' ', true); + } + + /* Verify short file name has not been taken yet, and if not try inserting numbers into the name */ + if (dirNameExists(shortFilename)) + { + for (int i=0; i<100; i++) + { + shortFilename = shortFilename.left( (i < 10 ? 7 : 6) )+QByteArray::number(i)+shortFilename.right(3); + + if (!dirNameExists(shortFilename)) + { + break; + } + else if (i == 99) + { + throw std::runtime_error("Error finding available short filename"); + } + } + } + + for(int i = 0; i < shortFilename.length(); i++) + { + shortFileNameChecksum = ((shortFileNameChecksum & 1) ? 0x80 : 0) + (shortFileNameChecksum >> 1) + shortFilename[i]; + } + + QString longFilenameWithNull = longFilename + QChar::Null; + char *longFilenameStr = (char *) longFilenameWithNull.data(); + int lfnFragments = (longFilenameWithNull.length()+12)/13; + int lenBytes = longFilenameWithNull.length()*2; + + /* long file name directory entries are added in reverse order before the 8.3 entry */ + for (int i = lfnFragments; i > 0; i--) + { + memset(&longEntry, 0xff, sizeof(longEntry)); + longEntry.LDIR_Attr = ATTR_LONG_NAME; + longEntry.LDIR_Chksum = shortFileNameChecksum; + longEntry.LDIR_Ord = (i == lfnFragments) ? lfnFragments | LAST_LONG_ENTRY : lfnFragments; + longEntry.LDIR_FstClusLO = 0; + longEntry.LDIR_Type = 0; + + size_t start = (i-1) * 26; + memcpy(longEntry.LDIR_Name1, longFilenameStr+start, qMin(lenBytes-start, sizeof(longEntry.LDIR_Name1))); + start += sizeof(longEntry.LDIR_Name1); + if (start < lenBytes) + { + memcpy(longEntry.LDIR_Name2, longFilenameStr+start, qMin(lenBytes-start, sizeof(longEntry.LDIR_Name2))); + start += sizeof(longEntry.LDIR_Name2); + if (start < lenBytes) + { + memcpy(longEntry.LDIR_Name3, longFilenameStr+start, qMin(lenBytes-start, sizeof(longEntry.LDIR_Name3))); + } + } + + writeDirEntryAtCurrentPos((struct dir_entry *) &longEntry); + } + + memset(entry, 0, sizeof(*entry)); + memcpy(entry->DIR_Name, shortFilename.data(), sizeof(entry->DIR_Name)); + entry->DIR_Attr = ATTR_ARCHIVE; + entry->DIR_CrtDate = QDateToFATdate( QDate::currentDate() ); + entry->DIR_CrtTime = QTimeToFATtime( QTime::currentTime() ); + + writeDirEntryAtCurrentPos(entry); + + /* Add an end-of-directory marker after our newly appended file */ + struct dir_entry endOfDir = {0}; + writeDirEntryAtCurrentPos(&endOfDir); + } + + return false; +} + +bool DeviceWrapperFatPartition::dirNameExists(const QByteArray dirname) +{ + struct dir_entry entry; + + openDir(); + while (readDir(&entry)) + { + if (!(entry.DIR_Attr & ATTR_LONG_NAME) + && dirname == QByteArray(entry.DIR_Name, sizeof(entry.DIR_Name))) + { + return true; + } + } + + return false; +} + +void DeviceWrapperFatPartition::updateDirEntry(struct dir_entry *dirEntry) +{ + struct dir_entry iterEntry; + + openDir(); + while (readDir(&iterEntry)) + { + /* Look for existing entry with same short filename */ + if (!(iterEntry.DIR_Attr & ATTR_LONG_NAME) + && memcmp(dirEntry->DIR_Name, iterEntry.DIR_Name, sizeof(iterEntry.DIR_Name)) == 0) + { + /* seek() back and write out new entry */ + _offset -= sizeof(*dirEntry); + write((char *) dirEntry, sizeof(*dirEntry)); + return; + } + } + + throw std::runtime_error("Error locating existing directory entry"); +} + +void DeviceWrapperFatPartition::writeDirEntryAtCurrentPos(struct dir_entry *dirEntry) +{ + write((char *) dirEntry, sizeof(*dirEntry)); + + if (_type == FAT32) + { + if ((pos()-_clusterOffset) % _bytesPerCluster == 0) + { + /* We reached the end of the cluster, allocate/seek to next cluster */ + uint32_t nextCluster = getFAT(_fat32_currentRootDirCluster); + /* FIXME: should we check for circular cluster references? */ + + if (nextCluster > 0xFFFFFF7) + { + nextCluster = allocateCluster(_fat32_currentRootDirCluster); + } + + _fat32_currentRootDirCluster = nextCluster; + seekCluster(_fat32_currentRootDirCluster); + } + } + else if (pos() > (_fat16_firstRootDirSector+_fat16_rootDirSectors)*_bytesPerSector) + { + throw std::runtime_error("FAT16: ran out of root directory entry space"); + } +} + +void DeviceWrapperFatPartition::openDir() +{ + /* Seek to start of root directory */ + if (_type == FAT16) + { + seek(_fat16_firstRootDirSector * _bytesPerSector); + } + else + { + _fat32_currentRootDirCluster = _fat32_firstRootDirCluster; + seekCluster(_fat32_currentRootDirCluster); + } +} + +bool DeviceWrapperFatPartition::readDir(struct dir_entry *result) +{ + quint64 oldOffset = _offset; + read((char *) result, sizeof(*result)); + + if (result->DIR_Name[0] == 0) + { + /* seek() back to start of the entry marking end of directory */ + _offset = oldOffset; + return false; + } + + if (_type == FAT32) + { + if ((pos()-_clusterOffset) % _bytesPerCluster == 0) + { + /* We reached the end of the cluster, seek to next cluster */ + uint32_t nextCluster = getFAT(_fat32_currentRootDirCluster); + /* FIXME: should we check for circular cluster references? */ + + if (nextCluster > 0xFFFFFF7) + throw std::runtime_error("Reached end of FAT32 root directory, but no end-of-directory marker found"); + + _fat32_currentRootDirCluster = nextCluster; + seekCluster(_fat32_currentRootDirCluster); + } + } + else if (pos() > (_fat16_firstRootDirSector+_fat16_rootDirSectors)*_bytesPerSector) + { + throw std::runtime_error("Reached end of FAT16 root directory section, but no end-of-directory marker found"); + } + + return true; +} + +void DeviceWrapperFatPartition::updateFSinfo(int deltaClusters, uint32_t nextFreeClusterHint) +{ + struct FSInfo fsinfo; + + if (!_fat32_fsinfoSector) + return; + + seek(_fat32_fsinfoSector * _bytesPerSector); + read((char *) &fsinfo, sizeof(fsinfo)); + + if (fsinfo.FSI_LeadSig[0] != 0x52 || fsinfo.FSI_LeadSig[1] != 0x52 + || fsinfo.FSI_LeadSig[2] != 0x61 || fsinfo.FSI_LeadSig[3] != 0x41 + || fsinfo.FSI_StrucSig[0] != 0x72 || fsinfo.FSI_StrucSig[1] != 0x72 + || fsinfo.FSI_StrucSig[2] != 0x41 || fsinfo.FSI_StrucSig[3] != 0x61 + || fsinfo.FSI_TrailSig[0] != 0x00 || fsinfo.FSI_TrailSig[1] != 0x00 + || fsinfo.FSI_TrailSig[2] != 0x55 || fsinfo.FSI_TrailSig[3] != 0xAA) + { + throw std::runtime_error("FAT32 FSinfo structure corrupt. Signature does not match."); + } + + if (deltaClusters != 0 && fsinfo.FSI_Free_Count != 0xFFFFFFFF) + { + fsinfo.FSI_Free_Count += deltaClusters; + } + + if (nextFreeClusterHint) + { + fsinfo.FSI_Nxt_Free = nextFreeClusterHint; + } + + seek(_fat32_fsinfoSector * _bytesPerSector); + write((char *) &fsinfo, sizeof(fsinfo)); +} + +uint16_t DeviceWrapperFatPartition::QTimeToFATtime(const QTime &time) +{ + return (time.hour() << 11) | (time.minute() << 5) | (time.second() >> 1) ; +} + +uint16_t DeviceWrapperFatPartition::QDateToFATdate(const QDate &date) +{ + return ((date.year() - 1980) << 9) | (date.month() << 5) | date.day(); +} diff --git a/src/devicewrapperfatpartition.h b/src/devicewrapperfatpartition.h new file mode 100644 index 0000000..ed22afc --- /dev/null +++ b/src/devicewrapperfatpartition.h @@ -0,0 +1,54 @@ +#ifndef DEVICEWRAPPERFATPARTITION_H +#define DEVICEWRAPPERFATPARTITION_H + +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2022 Raspberry Pi Ltd + */ + +#include "devicewrapperpartition.h" +#include +#include +#include + +enum fatType { FAT12, FAT16, FAT32, EXFAT }; +struct dir_entry; + +class DeviceWrapperFatPartition : public DeviceWrapperPartition +{ + Q_OBJECT +public: + DeviceWrapperFatPartition(DeviceWrapper *dw, quint64 partStart, quint64 partLen, QObject *parent = nullptr); + + QByteArray readFile(const QString &filename); + void writeFile(const QString &filename, const QByteArray &contents); + bool fileExists(const QString &filename); + +protected: + enum fatType _type; + uint32_t _firstFatStartOffset, _fatSize, _bytesPerCluster, _clusterOffset; + uint32_t _fat16_rootDirSectors, _fat16_firstRootDirSector; + uint32_t _fat32_firstRootDirCluster, _fat32_currentRootDirCluster; + uint16_t _bytesPerSector, _fat32_fsinfoSector; + QList _fatStartOffset; + + QList getClusterChain(uint32_t firstCluster); + void setFAT16(uint16_t cluster, uint16_t value); + void setFAT32(uint32_t cluster, uint32_t value); + void setFAT(uint32_t cluster, uint32_t value); + uint32_t getFAT(uint32_t cluster); + void seekCluster(uint32_t cluster); + uint32_t allocateCluster(); + uint32_t allocateCluster(uint32_t previousCluster); + bool getDirEntry(const QString &longFilename, struct dir_entry *entry, bool createIfNotExist = false); + bool dirNameExists(const QByteArray dirname); + void updateDirEntry(struct dir_entry *dirEntry); + void writeDirEntryAtCurrentPos(struct dir_entry *dirEntry); + void openDir(); + bool readDir(struct dir_entry *result); + void updateFSinfo(int deltaClusters, uint32_t nextFreeClusterHint); + uint16_t QTimeToFATtime(const QTime &time); + uint16_t QDateToFATdate(const QDate &date); +}; + +#endif // DEVICEWRAPPERFATPARTITION_H diff --git a/src/devicewrapperpartition.cpp b/src/devicewrapperpartition.cpp new file mode 100644 index 0000000..fc292e6 --- /dev/null +++ b/src/devicewrapperpartition.cpp @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2022 Raspberry Pi Ltd + */ + +#include "devicewrapperpartition.h" +#include "devicewrapper.h" + +DeviceWrapperPartition::DeviceWrapperPartition(DeviceWrapper *dw, quint64 partStart, quint64 partLen, QObject *parent) + : QObject{parent}, _dw(dw), _partStart(partStart), _partLen(partLen), _offset(partStart) +{ + _partEnd = _partStart + _partLen; +} + +DeviceWrapperPartition::~DeviceWrapperPartition() +{ + +} + +void DeviceWrapperPartition::read(char *data, qint64 size) +{ + if (_offset+size > _partEnd) + { + throw std::runtime_error("Error: trying to read beyond partition"); + } + + _dw->pread(data, size, _offset); + _offset += size; +} + +void DeviceWrapperPartition::seek(qint64 pos) +{ + if (pos > _partLen) + { + throw std::runtime_error("Error: trying to seek beyond partition"); + } + _offset = pos+_partStart; +} + +qint64 DeviceWrapperPartition::pos() const +{ + return _offset-_partStart; +} + +void DeviceWrapperPartition::write(const char *data, qint64 size) +{ + if (_offset+size > _partEnd) + { + throw std::runtime_error("Error: trying to write beyond partition"); + } + + _dw->pwrite(data, size, _offset); + _offset += size; +} diff --git a/src/devicewrapperpartition.h b/src/devicewrapperpartition.h new file mode 100644 index 0000000..ab06402 --- /dev/null +++ b/src/devicewrapperpartition.h @@ -0,0 +1,32 @@ +#ifndef DEVICEWRAPPERPARTITION_H +#define DEVICEWRAPPERPARTITION_H + +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2022 Raspberry Pi Ltd + */ + +#include + +class DeviceWrapper; + +class DeviceWrapperPartition : public QObject +{ + Q_OBJECT +public: + explicit DeviceWrapperPartition(DeviceWrapper *dw, quint64 partStart, quint64 partLen, QObject *parent = nullptr); + virtual ~DeviceWrapperPartition(); + void read(char *data, qint64 size); + void seek(qint64 pos); + qint64 pos() const; + void write(const char *data, qint64 size); + +protected: + DeviceWrapper *_dw; + quint64 _partStart, _partLen, _partEnd, _offset; + +signals: + +}; + +#endif // DEVICEWRAPPERPARTITION_H diff --git a/src/devicewrapperstructs.h b/src/devicewrapperstructs.h new file mode 100644 index 0000000..fbbf0c8 --- /dev/null +++ b/src/devicewrapperstructs.h @@ -0,0 +1,145 @@ +#ifndef DEVICEWRAPPERSTRUCTS_H +#define DEVICEWRAPPERSTRUCTS_H + +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2022 Raspberry Pi Ltd + */ + +/* MBR on-disk structures */ + +struct mbr_partition_entry { + unsigned char bootable; + char begin_hsc[3]; + unsigned char id; + char end_hsc[3]; + unsigned int starting_sector; + unsigned int nr_of_sectors; +} __attribute__ ((packed)); + +struct mbr_table { + char bootcode[440]; + unsigned char diskid[4]; + unsigned char flags[2]; + mbr_partition_entry part[4]; + unsigned char signature[2]; +} __attribute__ ((packed)); + + +/* File Allocation Table + * https://academy.cba.mit.edu/classes/networking_communications/SD/FAT.pdf + */ + +struct fat16_bpb { + uint8_t BS_jmpBoot[3]; + char BS_OEMName[8]; + uint16_t BPB_BytsPerSec; + uint8_t BPB_SecPerClus; + uint16_t BPB_RsvdSecCnt; + uint8_t BPB_NumFATs; + uint16_t BPB_RootEntCnt; + uint16_t BPB_TotSec16; + uint8_t BPB_Media; + uint16_t BPB_FATSz16; + uint16_t BPB_SecPerTrk; + uint16_t BPB_NumHeads; + uint32_t BPB_HiddSec; + uint32_t BPB_TotSec32; + + uint8_t BS_DrvNum; + uint8_t BS_Reserved1; + uint8_t BS_BootSig; + uint32_t BS_VolID; + char BS_VolLab[11]; + char BS_FilSysType[8]; + + uint8_t Zeroes[448]; + uint8_t Signature[2]; /* 0x55aa */ +} __attribute__ ((packed)); + +struct fat32_bpb { + uint8_t BS_jmpBoot[3]; + char BS_OEMName[8]; + uint16_t BPB_BytsPerSec; + uint8_t BPB_SecPerClus; + uint16_t BPB_RsvdSecCnt; + uint8_t BPB_NumFATs; + uint16_t BPB_RootEntCnt; + uint16_t BPB_TotSec16; + uint8_t BPB_Media; + uint16_t BPB_FATSz16; + uint16_t BPB_SecPerTrk; + uint16_t BPB_NumHeads; + uint32_t BPB_HiddSec; + uint32_t BPB_TotSec32; + + uint32_t BPB_FATSz32; + uint16_t BPB_ExtFlags; + uint16_t BPB_FSVer; + uint32_t BPB_RootClus; + uint16_t BPB_FSInfo; + uint16_t BPB_BkBootSec; + uint8_t BPB_Reserved[12]; + uint8_t BS_DrvNum; + uint8_t BS_Reserved1; + uint8_t BS_BootSig; + uint32_t BS_VolID; + char BS_VolLab[11]; + char BS_FilSysType[8]; + + uint8_t Zeroes[420]; + uint8_t Signature[2]; /* 0x55aa */ +} __attribute__ ((packed)); + +union fat_bpb { + struct fat16_bpb fat16; + struct fat32_bpb fat32; +}; + +struct dir_entry { + char DIR_Name[11]; + uint8_t DIR_Attr; + uint8_t DIR_NTRes; + uint8_t DIR_CrtTimeTenth; + uint16_t DIR_CrtTime; + uint16_t DIR_CrtDate; + uint16_t DIR_LstAccDate; + uint16_t DIR_FstClusHI; + uint16_t DIR_WrtTime; + uint16_t DIR_WrtDate; + uint16_t DIR_FstClusLO; + uint32_t DIR_FileSize; +} __attribute__ ((packed)); + +struct longfn_entry { + uint8_t LDIR_Ord; + char LDIR_Name1[10]; + uint8_t LDIR_Attr; + uint8_t LDIR_Type; + uint8_t LDIR_Chksum; + char LDIR_Name2[12]; + uint16_t LDIR_FstClusLO; + char LDIR_Name3[4]; +} __attribute__ ((packed)); + +#define LAST_LONG_ENTRY 0x40 + +#define ATTR_READ_ONLY 0x01 +#define ATTR_HIDDEN 0x02 +#define ATTR_SYSTEM 0x04 +#define ATTR_VOLUME_ID 0x08 +#define ATTR_DIRECTORY 0x10 +#define ATTR_ARCHIVE 0x20 +#define ATTR_LONG_NAME (ATTR_READ_ONLY | ATTR_HIDDEN | ATTR_SYSTEM | ATTR_VOLUME_ID) + +struct FSInfo { + uint8_t FSI_LeadSig[4]; /* 0x52 0x52 0x61 0x41 */ + uint8_t FSI_Reserved1[480]; + uint8_t FSI_StrucSig[4]; /* 0x72 0x72 0x41 0x61 */ + uint32_t FSI_Free_Count; + uint32_t FSI_Nxt_Free; + uint8_t FSI_Reserved2[12]; + uint8_t FSI_TrailSig[4]; /* 0x00 0x00 0x55 0xAA */ +} __attribute__ ((packed)); + +#endif // DEVICEWRAPPERSTRUCTS_H