/* Copyright (c) 2010-2014 Benjamin Dobell, Glass Echidna Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.*/ #ifdef WIN32 #pragma warning(disable : 4996) #endif // C/C++ Standard Library #include // zlib #include "zlib.h" // Qt #include #include #include #include // Heimdall Frontend #include "Alerts.h" #include "Packaging.h" #include "GZipFile.h" using namespace HeimdallFrontend; const qint64 Packaging::kMaxFileSize = 4294967295ll; const char *Packaging::ustarMagic = "ustar"; bool Packaging::DecompressGZippedFile(const QString &path, const QString &outputPath) { GZipFile gzipFile{path}; if (!gzipFile.Open(GZipFile::ReadOnly)) { Alerts::DisplayError(QString("Failed to open file:\n%1").arg(path)); return (false); } qint64 compressedFileSize = gzipFile.Size(); QFile outputFile(outputPath); if (!outputFile.open(QIODevice::WriteOnly)) { Alerts::DisplayError(QString("Failed to open decompression output file:\n%1").arg(outputFile.fileName())); return (false); } QProgressDialog progressDialog("Decompressing...", "Cancel", 0, 1000); progressDialog.setWindowModality(Qt::ApplicationModal); progressDialog.setWindowTitle("Heimdall Frontend"); char buffer[kExtractBufferLength]; int bytesRead; do { bytesRead = gzipFile.Read(buffer, kExtractBufferLength); if (bytesRead == -1) { progressDialog.close(); Alerts::DisplayError("Error decompressing archive."); return (false); } outputFile.write(buffer, bytesRead); progressDialog.setValue((1000ll * gzipFile.Offset()) / compressedFileSize); if (progressDialog.wasCanceled()) { return (false); } } while (bytesRead > 0); progressDialog.close(); return (true); }; bool Packaging::ExtractTar(QFile& tarFile, const QDir& outputDirectory, QList& outputFilePaths) { TarHeader tarHeader; if (!tarFile.open(QFile::ReadOnly)) { Alerts::DisplayError(QString("Error opening archive:\n%1").arg(tarFile.fileName())); return (false); } bool previousEmpty = false; qint64 tarFileSize = tarFile.size(); QProgressDialog progressDialog("Extracting archive...", "Cancel", 0, 1000); progressDialog.setWindowModality(Qt::ApplicationModal); progressDialog.setWindowTitle("Heimdall Frontend"); while (!tarFile.atEnd()) { qint64 dataRead = tarFile.read(tarHeader.buffer, TarHeader::kBlockLength); if (dataRead != TarHeader::kBlockLength) { progressDialog.close(); Alerts::DisplayError("Package's TAR archive is malformed."); tarFile.close(); return (false); } progressDialog.setValue(tarFile.pos()); if (progressDialog.wasCanceled()) { tarFile.close(); progressDialog.close(); return (false); } bool empty = true; for (int i = 0; i < TarHeader::kBlockLength; i++) { if (tarHeader.buffer[i] != 0) { empty = false; break; } } if (empty) { if (previousEmpty) { // Two empty blocks in a row means we've reached the end of the archive. break; } } else { bool parsed = false; // The size field is not always null terminated, so we must create a copy and null terminate it for parsing. char fileSizeString[13]; memcpy(fileSizeString, tarHeader.fields.size, 12); fileSizeString[12] = '\0'; qulonglong fileSize = QString(fileSizeString).toULongLong(&parsed, 8); if (!parsed) { progressDialog.close(); Alerts::DisplayError("Tar header contained an invalid file size."); tarFile.close(); return (false); } if (fileSize > 0 && tarHeader.fields.typeFlag == '0') { // We're working with a file. QString filename = QString::fromUtf8(tarHeader.fields.name); QString filePath = outputDirectory.path() + "/" + filename; QFile outputFile(filePath); if (!outputFile.open(QFile::WriteOnly)) { progressDialog.close(); Alerts::DisplayError(QString("Failed to open output file: \n%1").arg(outputFile.fileName())); tarFile.close(); return (false); } outputFilePaths.append(filePath); qulonglong dataRemaining = fileSize; char readBuffer[TarHeader::kBlockReadCount * TarHeader::kBlockLength]; // Copy the file contents from tarFile to outputFile while (dataRemaining > 0) { qint64 fileDataToRead = (dataRemaining < TarHeader::kBlockReadCount * TarHeader::kBlockLength) ? dataRemaining : TarHeader::kBlockReadCount * TarHeader::kBlockLength; qint64 dataRead = tarFile.read(readBuffer, fileDataToRead + (TarHeader::kBlockLength - fileDataToRead % TarHeader::kBlockLength) % TarHeader::kBlockLength); if (dataRead < fileDataToRead || dataRead % TarHeader::kBlockLength != 0) { progressDialog.close(); Alerts::DisplayError("Unexpected read error whilst extracting package files."); tarFile.close(); outputFile.close(); outputFile.remove(); return (false); } outputFile.write(readBuffer, fileDataToRead); dataRemaining -= fileDataToRead; progressDialog.setValue((1000ll * tarFile.pos()) / tarFileSize); if (progressDialog.wasCanceled()) { tarFile.close(); outputFile.close(); outputFile.remove(); progressDialog.close(); return (false); } } outputFile.close(); } else { progressDialog.close(); Alerts::DisplayError("Heimdall packages shouldn't contain links or directories."); tarFile.close(); return (false); } } previousEmpty = empty; } progressDialog.close(); tarFile.close(); return (true); } bool Packaging::WriteTarEntry(const QString& entryFilename, const QString& filePath, QFile& outputTarFile) { TarHeader tarHeader; memset(tarHeader.buffer, 0, TarHeader::kBlockLength); QFile file(filePath); if (!file.open(QFile::ReadOnly)) { Alerts::DisplayError(QString("Failed to open file: \n%1").arg(file.fileName())); return (false); } if (file.size() > Packaging::kMaxFileSize) { Alerts::DisplayError(QString("File is too large to be packaged:\n%1").arg(file.fileName())); return (false); } QFileInfo qtFileInfo(file); QByteArray utfFilename; utfFilename = entryFilename.toUtf8(); if (utfFilename.length() > 100) { Alerts::DisplayError(QString("File name is too long:\n%1").arg(qtFileInfo.fileName())); return (false); } strcpy(tarHeader.fields.name, utfFilename.constData()); unsigned int mode = 0; QFile::Permissions permissions = file.permissions(); // Other if (permissions.testFlag(QFile::ExeOther)) mode |= TarHeader::kModeOtherExecute; if (permissions.testFlag(QFile::WriteOther)) mode |= TarHeader::kModeOtherWrite; if (permissions.testFlag(QFile::ReadOther)) mode |= TarHeader::kModeOtherRead; // Group if (permissions.testFlag(QFile::ExeGroup)) mode |= TarHeader::kModeGroupExecute; if (permissions.testFlag(QFile::WriteGroup)) mode |= TarHeader::kModeGroupWrite; if (permissions.testFlag(QFile::ReadGroup)) mode |= TarHeader::kModeGroupRead; // Owner if (permissions.testFlag(QFile::ExeOwner)) mode |= TarHeader::kModeOwnerExecute; if (permissions.testFlag(QFile::WriteOwner)) mode |= TarHeader::kModeOwnerWrite; if (permissions.testFlag(QFile::ReadOwner)) mode |= TarHeader::kModeOwnerRead; sprintf(tarHeader.fields.mode, "%07o", mode); // Owner id uint id = qtFileInfo.ownerId(); if (id < 2097151) sprintf(tarHeader.fields.userId, "%07o", id); else sprintf(tarHeader.fields.userId, "%07o", 0); // Group id id = qtFileInfo.groupId(); if (id < 2097151) sprintf(tarHeader.fields.groupId, "%07o", id); else sprintf(tarHeader.fields.groupId, "%07o", 0); // Note: We don't support base-256 encoding. Support could be added later. sprintf(tarHeader.fields.size, "%011llo", file.size()); sprintf(tarHeader.fields.modifiedTime, "%u", qtFileInfo.lastModified().toTime_t()); // Regular File tarHeader.fields.typeFlag = '0'; // Calculate checksum int checksum = 0; memset(tarHeader.fields.checksum, ' ', 8); for (int i = 0; i < TarHeader::kTarHeaderLength; i++) checksum += static_cast(tarHeader.buffer[i]); sprintf(tarHeader.fields.checksum, "%07o", checksum); // Write the header to the TAR file. outputTarFile.write(tarHeader.buffer, TarHeader::kBlockLength); char buffer[TarHeader::kBlockWriteCount * TarHeader::kBlockLength]; qint64 offset = 0; while (offset < file.size()) { qint64 dataRead = file.read(buffer, TarHeader::kBlockWriteCount * TarHeader::kBlockLength); if (outputTarFile.write(buffer, dataRead) != dataRead) { Alerts::DisplayError("Failed to write data to the temporary TAR file."); return (false); } if (dataRead % TarHeader::kBlockLength != 0) { int remainingBlockLength = TarHeader::kBlockLength - dataRead % TarHeader::kBlockLength; memset(buffer, 0, remainingBlockLength); if (outputTarFile.write(buffer, remainingBlockLength) != remainingBlockLength) { Alerts::DisplayError("Failed to write data to the temporary TAR file."); return (false); } } offset += dataRead; } return (true); } bool Packaging::CreateTar(const FirmwareInfo *firmwareInfo, QFile& outputTarFile) { const QList& fileInfos = firmwareInfo->GetFileInfos(); QProgressDialog progressDialog("Packaging files...", "Cancel", 0, fileInfos.length() + 2); progressDialog.setWindowModality(Qt::ApplicationModal); progressDialog.setWindowTitle("Heimdall Frontend"); QTemporaryFile firmwareXmlFile("XXXXXX-firmware.xml"); if (!firmwareXmlFile.open()) { progressDialog.close(); Alerts::DisplayError(QString("Failed to create temporary file: \n%1").arg(firmwareXmlFile.fileName())); return (false); } QXmlStreamWriter xml(&firmwareXmlFile); firmwareInfo->WriteXml(xml); firmwareXmlFile.close(); if (!outputTarFile.open(QFile::WriteOnly)) { progressDialog.close(); Alerts::DisplayError(QString("Failed to open file: \n%1").arg(outputTarFile.fileName())); return (false); } for (int i = 0; i < fileInfos.length(); i++) { // If the file was already compressed we don't compress it again. bool skip = false; for (int j = 0; j < i; j++) { if (fileInfos[i]->GetFilename() == fileInfos[j]->GetFilename()) { skip = true; break; } } if (skip) { progressDialog.setValue(i); continue; } QString filename = ClashlessFilename(fileInfos, i); if (filename == "firmware.xml") { Alerts::DisplayError("You cannot name your partition files \"firmware.xml\".\nIt is a reserved name."); return (false); } if (!WriteTarEntry(filename, fileInfos[i]->GetFilename(), outputTarFile)) { outputTarFile.resize(0); outputTarFile.close(); progressDialog.close(); return (false); } progressDialog.setValue(i); if (progressDialog.wasCanceled()) { outputTarFile.resize(0); outputTarFile.close(); progressDialog.close(); return (false); } } int lastSlash = firmwareInfo->GetPitFilename().lastIndexOf('/'); if (lastSlash < 0) lastSlash = firmwareInfo->GetPitFilename().lastIndexOf('\\'); QString pitFilename = ClashlessFilename(fileInfos, firmwareInfo->GetPitFilename().mid(lastSlash + 1)); if (pitFilename == "firmware.xml") { Alerts::DisplayError("You cannot name your PIT file \"firmware.xml\".\nIt is a reserved name."); return (false); } if (!WriteTarEntry(pitFilename, firmwareInfo->GetPitFilename(), outputTarFile)) { outputTarFile.resize(0); outputTarFile.close(); return (false); } progressDialog.setValue(progressDialog.value() + 1); if (progressDialog.wasCanceled()) { outputTarFile.resize(0); outputTarFile.close(); progressDialog.close(); return (false); } if (!WriteTarEntry("firmware.xml", firmwareXmlFile.fileName(), outputTarFile)) { outputTarFile.resize(0); outputTarFile.close(); return (false); } progressDialog.setValue(progressDialog.value() + 1); progressDialog.close(); // Write two empty blocks to signify the end of the archive. char emptyEntry[TarHeader::kBlockLength]; memset(emptyEntry, 0, TarHeader::kBlockLength); outputTarFile.write(emptyEntry, TarHeader::kBlockLength); outputTarFile.write(emptyEntry, TarHeader::kBlockLength); outputTarFile.close(); return (true); } bool Packaging::ExtractPackage(const QString& packagePath, PackageData *packageData) { QTemporaryDir outputDirectory; if (!outputDirectory.isValid()) { Alerts::DisplayError("Failed to create package output directory."); return (false); } QTemporaryFile tarFile; tarFile.open(); tarFile.close(); QList decompressedFilePaths; if (!DecompressGZippedFile(packagePath, tarFile.fileName()) || !ExtractTar(tarFile, QDir(outputDirectory.path()), decompressedFilePaths)) { return (false); } // Find and read firmware.xml for (const QString& path : decompressedFilePaths) { if (path.endsWith("firmware.xml")) { if (!packageData->ReadFirmwareInfo(path)) { packageData->Clear(); return (false); } outputDirectory.setAutoRemove(false); packageData->GetFilePaths().append(decompressedFilePaths); packageData->SetPackagePath(outputDirectory.path()); return (true); } } Alerts::DisplayError("firmware.xml is missing from the package."); return (false); } bool Packaging::BuildPackage(const QString& packagePath, const FirmwareInfo *firmwareInfo) { GZipFile packageFile{packagePath}; if (!packageFile.Open(GZipFile::WriteOnly)) { Alerts::DisplayError(QString("Failed to create package:\n%1").arg(packagePath)); return (false); } packageFile.SetTemporary(true); QTemporaryFile tar("XXXXXX.tar"); if (!CreateTar(firmwareInfo, tar)) { return (false); } if (!tar.open()) { Alerts::DisplayError(QString("Failed to open temporary file: \n%1").arg(tar.fileName())); return (false); } char buffer[kCompressBufferLength]; qint64 totalBytesRead = 0; quint64 tarSize = tar.size(); QProgressDialog progressDialog("Compressing package...", "Cancel", 0, 1000); progressDialog.setWindowModality(Qt::ApplicationModal); progressDialog.setWindowTitle("Heimdall Frontend"); int bytesRead; do { bytesRead = tar.read(buffer, kCompressBufferLength); if (bytesRead == -1) { progressDialog.close(); Alerts::DisplayError("Error reading temporary TAR file."); return (false); } if (!packageFile.Write(buffer, bytesRead)) { progressDialog.close(); Alerts::DisplayError("Error compressing package."); return (false); } totalBytesRead += bytesRead; progressDialog.setValue((1000ll * totalBytesRead) / tarSize); if (progressDialog.wasCanceled()) { progressDialog.close(); return (false); } } while (bytesRead > 0); progressDialog.close(); packageFile.SetTemporary(false); return (true); }; QString Packaging::ClashlessFilename(const QList& fileInfos, int fileInfoIndex) { int lastSlash = fileInfos[fileInfoIndex]->GetFilename().lastIndexOf('/'); if (lastSlash < 0) lastSlash = fileInfos[fileInfoIndex]->GetFilename().lastIndexOf('\\'); QString filename = fileInfos[fileInfoIndex]->GetFilename().mid(lastSlash + 1); unsigned int renameIndex = 0; // Check for name clashes for (int i = 0; i < fileInfoIndex; i++) { lastSlash = fileInfos[i]->GetFilename().lastIndexOf('/'); if (lastSlash < 0) lastSlash = fileInfos[i]->GetFilename().lastIndexOf('\\'); QString otherFilename = fileInfos[i]->GetFilename().mid(lastSlash + 1); // If the filenames are the same, but the files themselves aren't the same (i.e. not the same path), then rename. if (filename == otherFilename && fileInfos[i]->GetFilename() != fileInfos[fileInfoIndex]->GetFilename()) renameIndex++; } if (renameIndex > 0) { int lastPeriodIndex = filename.lastIndexOf(QChar('.')); QString shortFilename; QString fileType; if (lastPeriodIndex >= 0) { shortFilename = filename.left(lastPeriodIndex); fileType = filename.mid(lastPeriodIndex); } else { shortFilename = filename; } unsigned int renameIndexOffset = 0; bool validIndexOffset = true; // Before we append a rename index we must ensure it doesn't produce further collisions. for (const FileInfo *fileInfo : fileInfos) { int lastSlash = fileInfo->GetFilename().lastIndexOf('/'); if (lastSlash < 0) lastSlash = fileInfo->GetFilename().lastIndexOf('\\'); QString otherFilename = fileInfo->GetFilename().mid(lastSlash + 1); if (otherFilename.length() > filename.length() + 1) { QString trimmedOtherFilename = otherFilename.left(shortFilename.length()); if (shortFilename == trimmedOtherFilename) { lastPeriodIndex = otherFilename.lastIndexOf(QChar('.')); QString shortOtherFilename; if (lastPeriodIndex >= 0) shortOtherFilename = otherFilename.left(lastPeriodIndex); else shortOtherFilename = otherFilename; QRegExp renameExp("-[0-9]+"); if (renameExp.lastIndexIn(shortOtherFilename) == shortFilename.length()) { unsigned int trailingInteger = shortOtherFilename.mid(shortFilename.length() + 1).toUInt(&validIndexOffset); if (!validIndexOffset) break; if (trailingInteger > renameIndexOffset) renameIndexOffset = trailingInteger; } } } } if (validIndexOffset) { // Ensure renaming won't involve integer overflow! if (renameIndex > static_cast(-1) - renameIndexOffset) validIndexOffset = false; } if (validIndexOffset) { shortFilename.append(QChar('-')); shortFilename.append(QString::number(renameIndexOffset + renameIndex)); return (shortFilename + fileType); } else { // Fallback behaviour... an absolutely terrible brute force implementation! QString filename; QString renamePrefix; for (;;) { renamePrefix.append(QChar('+')); for (unsigned int i = 0; i < static_cast(-1); i++) { filename = shortFilename + renamePrefix + QString::number(i) + fileType; bool valid = true; for (const FileInfo *fileInfo : fileInfos) { int lastSlash = fileInfo->GetFilename().lastIndexOf('/'); if (lastSlash < 0) lastSlash = fileInfo->GetFilename().lastIndexOf('\\'); if (filename == fileInfo->GetFilename().mid(lastSlash + 1)) { valid = false; break; } } if (valid) return (filename); } } } } else { return (filename); } } QString Packaging::ClashlessFilename(const QList& fileInfos, const QString& filename) { unsigned int renameIndex = 0; // Check for name clashes for (const FileInfo *fileInfo : fileInfos) { int lastSlash = fileInfo->GetFilename().lastIndexOf('/'); if (lastSlash < 0) lastSlash = fileInfo->GetFilename().lastIndexOf('\\'); QString otherFilename = fileInfo->GetFilename().mid(lastSlash + 1); if (filename == otherFilename) renameIndex++; } if (renameIndex > 0) { int lastPeriodIndex = filename.lastIndexOf(QChar('.')); QString shortFilename; QString fileType; if (lastPeriodIndex >= 0) { shortFilename = filename.left(lastPeriodIndex); fileType = filename.mid(lastPeriodIndex); } else { shortFilename = filename; } unsigned int renameIndexOffset = 0; bool validIndexOffset = true; // Before we append a rename index we must ensure it doesn't produce further collisions. for (const FileInfo *fileInfo : fileInfos) { int lastSlash = fileInfo->GetFilename().lastIndexOf('/'); if (lastSlash < 0) lastSlash = fileInfo->GetFilename().lastIndexOf('\\'); QString otherFilename = fileInfo->GetFilename().mid(lastSlash + 1); if (otherFilename.length() > filename.length() + 1) { QString trimmedOtherFilename = otherFilename.left(filename.length()); if (filename == trimmedOtherFilename) { lastPeriodIndex = otherFilename.lastIndexOf(QChar('.')); QString shortOtherFilename; if (lastPeriodIndex >= 0) shortOtherFilename = otherFilename.left(lastPeriodIndex); else shortOtherFilename = otherFilename; QRegExp renameExp("-[0-9]+"); if (renameExp.lastIndexIn(shortOtherFilename) == shortFilename.length()) { unsigned int trailingInteger = shortOtherFilename.mid(shortFilename.length() + 1).toUInt(&validIndexOffset); if (!validIndexOffset) break; if (trailingInteger > renameIndexOffset) renameIndexOffset = trailingInteger; } } } } if (validIndexOffset) { // Ensure renaming won't involve integer overflow! if (renameIndex > static_cast(-1) - renameIndexOffset) validIndexOffset = false; } if (validIndexOffset) { shortFilename.append(QChar('-')); shortFilename.append(QString::number(renameIndexOffset + renameIndex)); return (shortFilename + fileType); } else { // Fallback behaviour, brute-force/semi-random. bool valid; QString filename; do { valid = true; filename = shortFilename + "-"; for (int i = 0; i < 8; i++) filename.append(QChar(qrand() % ('Z' - 'A' + 1) + 'A')); for (const FileInfo *fileInfo : fileInfos) { int lastSlash = fileInfo->GetFilename().lastIndexOf('/'); if (lastSlash < 0) lastSlash = fileInfo->GetFilename().lastIndexOf('\\'); if (filename == fileInfo->GetFilename().mid(lastSlash + 1)) { valid = false; break; } } } while (!valid); return (filename); } } else { return (filename); } }