diff options
Diffstat (limited to '')
-rwxr-xr-x | heimdall-frontend/Source/FirmwareInfo.cpp | 188 | ||||
-rwxr-xr-x | heimdall-frontend/Source/FirmwareInfo.h | 16 | ||||
-rwxr-xr-x | heimdall-frontend/Source/Packaging.cpp | 496 | ||||
-rwxr-xr-x | heimdall-frontend/Source/Packaging.h | 16 | ||||
-rw-r--r-- | heimdall-frontend/Source/mainwindow.cpp | 88 | ||||
-rw-r--r-- | heimdall-frontend/Source/mainwindow.h | 2 | ||||
-rw-r--r-- | heimdall-frontend/aboutform.ui | 2 | ||||
-rw-r--r-- | heimdall-frontend/mainwindow.ui | 96 |
8 files changed, 693 insertions, 211 deletions
diff --git a/heimdall-frontend/Source/FirmwareInfo.cpp b/heimdall-frontend/Source/FirmwareInfo.cpp index 7d11605..39ec242 100755 --- a/heimdall-frontend/Source/FirmwareInfo.cpp +++ b/heimdall-frontend/Source/FirmwareInfo.cpp @@ -50,7 +50,7 @@ bool DeviceInfo::ParseXml(QXmlStreamReader& xml) { if (foundManufacturer) { - // TODO: "found multiple device manufacturers." + // TODO: "Found multiple device manufacturers." return (false); } @@ -62,7 +62,7 @@ bool DeviceInfo::ParseXml(QXmlStreamReader& xml) { if (foundProduct) { - // TODO: "found multiple device product identifiers." + // TODO: "Found multiple device product identifiers." return (false); } @@ -74,7 +74,7 @@ bool DeviceInfo::ParseXml(QXmlStreamReader& xml) { if (foundName) { - // TODO: "found multiple device names.")); + // TODO: "Found multiple device names.")); return (false); } @@ -101,6 +101,25 @@ bool DeviceInfo::ParseXml(QXmlStreamReader& xml) return (false); } +void DeviceInfo::WriteXml(QXmlStreamWriter& xml) const +{ + xml.writeStartElement("device"); + + xml.writeStartElement("manufacturer"); + xml.writeCharacters(manufacturer); + xml.writeEndElement(); + + xml.writeStartElement("product"); + xml.writeCharacters(product); + xml.writeEndElement(); + + xml.writeStartElement("name"); + xml.writeCharacters(name); + xml.writeEndElement(); + + xml.writeEndElement(); +} + PlatformInfo::PlatformInfo() @@ -135,7 +154,7 @@ bool PlatformInfo::ParseXml(QXmlStreamReader& xml) { if (foundName) { - // TODO: "found multiple platform names." + // TODO: "Found multiple platform names." return (false); } @@ -147,7 +166,7 @@ bool PlatformInfo::ParseXml(QXmlStreamReader& xml) { if (foundVersion) { - // TODO: "found multiple platform versions." + // TODO: "Found multiple platform versions." return (false); } @@ -179,6 +198,21 @@ bool PlatformInfo::ParseXml(QXmlStreamReader& xml) return (false); } +void PlatformInfo::WriteXml(QXmlStreamWriter& xml) const +{ + xml.writeStartElement("platform"); + + xml.writeStartElement("name"); + xml.writeCharacters(name); + xml.writeEndElement(); + + xml.writeStartElement("version"); + xml.writeCharacters(version); + xml.writeEndElement(); + + xml.writeEndElement(); +} + FileInfo::FileInfo() @@ -206,7 +240,7 @@ bool FileInfo::ParseXml(QXmlStreamReader& xml) { if (foundId) { - // TODO: "found multiple file IDs." + // TODO: "Found multiple file IDs." return (false); } @@ -218,7 +252,7 @@ bool FileInfo::ParseXml(QXmlStreamReader& xml) { if (foundFilename) { - // TODO: "found multiple file filenames." + // TODO: "Found multiple file filenames." return (false); } @@ -245,11 +279,34 @@ bool FileInfo::ParseXml(QXmlStreamReader& xml) return (false); } +void FileInfo::WriteXml(QXmlStreamWriter& xml) const +{ + xml.writeStartElement("file"); + + xml.writeStartElement("id"); + xml.writeCharacters(QString::number(partitionId)); + xml.writeEndElement(); + + xml.writeStartElement("filename"); + + int lastSlash = filename.lastIndexOf('/'); + + if (lastSlash < 0) + lastSlash = filename.lastIndexOf('\\'); + + xml.writeCharacters(filename.mid(lastSlash + 1)); + + xml.writeEndElement(); + + xml.writeEndElement(); +} + FirmwareInfo::FirmwareInfo() { repartition = false; + noReboot = false; } void FirmwareInfo::Clear(void) @@ -267,13 +324,15 @@ void FirmwareInfo::Clear(void) pitFilename.clear(); repartition = false; + noReboot = false; + fileInfos.clear(); } bool FirmwareInfo::IsCleared(void) const { return (name.isEmpty() && version.isEmpty() && platformInfo.IsCleared() && developers.isEmpty() && url.isEmpty() && url.isEmpty() && donateUrl.isEmpty() - && deviceInfos.isEmpty() && pitFilename.isEmpty() && !repartition && fileInfos.isEmpty()); + && deviceInfos.isEmpty() && pitFilename.isEmpty() && !repartition && !noReboot && fileInfos.isEmpty()); } bool FirmwareInfo::ParseXml(QXmlStreamReader& xml) @@ -289,6 +348,7 @@ bool FirmwareInfo::ParseXml(QXmlStreamReader& xml) bool foundDevices = false; bool foundPit = false; bool foundRepartition = false; + bool foundNoReboot = false; bool foundFiles = false; if (!xml.readNextStartElement()) @@ -337,7 +397,7 @@ bool FirmwareInfo::ParseXml(QXmlStreamReader& xml) { if (foundName) { - // TODO: "found multiple firmware names." + // TODO: "Found multiple firmware names." return (false); } @@ -348,7 +408,7 @@ bool FirmwareInfo::ParseXml(QXmlStreamReader& xml) { if (foundVersion) { - // TODO: "found multiple firmware versions." + // TODO: "Found multiple firmware versions." return (false); } @@ -359,7 +419,7 @@ bool FirmwareInfo::ParseXml(QXmlStreamReader& xml) { if (foundPlatform) { - // TODO: "found multiple firmware platforms." + // TODO: "Found multiple firmware platforms." return (false); } @@ -372,7 +432,7 @@ bool FirmwareInfo::ParseXml(QXmlStreamReader& xml) { if (foundDevelopers) { - // TODO: "found multiple sets of firmware developers." + // TODO: "Found multiple sets of firmware developers." return (false); } @@ -406,7 +466,7 @@ bool FirmwareInfo::ParseXml(QXmlStreamReader& xml) { if (foundUrl) { - // TODO: "found multiple firmware URLs." + // TODO: "Found multiple firmware URLs." return (false); } @@ -418,7 +478,7 @@ bool FirmwareInfo::ParseXml(QXmlStreamReader& xml) { if (foundDonateUrl) { - // TODO: "found multiple firmware donate URLs." + // TODO: "Found multiple firmware donate URLs." return (false); } @@ -430,7 +490,7 @@ bool FirmwareInfo::ParseXml(QXmlStreamReader& xml) { if (foundDevices) { - // TODO: "found multiple sets of firmware devices." + // TODO: "Found multiple sets of firmware devices." return (false); } @@ -471,7 +531,7 @@ bool FirmwareInfo::ParseXml(QXmlStreamReader& xml) { if (foundPit) { - // TODO: "found multiple firmware PIT files." + // TODO: "Found multiple firmware PIT files." return (false); } @@ -483,7 +543,7 @@ bool FirmwareInfo::ParseXml(QXmlStreamReader& xml) { if (foundRepartition) { - // TODO: "found multiple firmware repartition values." + // TODO: "Found multiple firmware repartition values." return (false); } @@ -491,11 +551,23 @@ bool FirmwareInfo::ParseXml(QXmlStreamReader& xml) repartition = (xml.readElementText().toInt() != 0); } + else if (xml.name() == "noreboot") + { + if (foundNoReboot) + { + // TODO: "Found multiple firmware noreboot values." + return (false); + } + + foundNoReboot = true; + + noReboot = (xml.readElementText().toInt() != 0); + } else if (xml.name() == "files") { if (foundFiles) { - // TODO: "found multiple sets of firmware files." + // TODO: "Found multiple sets of firmware files." return (false); } @@ -542,7 +614,7 @@ bool FirmwareInfo::ParseXml(QXmlStreamReader& xml) { if (xml.name() == "firmware") { - if (!(foundName && foundVersion && foundPlatform && foundDevelopers && foundDevices && foundPit && foundRepartition && foundFiles)) + if (!(foundName && foundVersion && foundPlatform && foundDevelopers && foundDevices && foundPit && foundRepartition && foundNoReboot && foundFiles)) return (false); else break; @@ -569,3 +641,81 @@ bool FirmwareInfo::ParseXml(QXmlStreamReader& xml) return (true); } + +void FirmwareInfo::WriteXml(QXmlStreamWriter& xml) const +{ + xml.writeStartDocument(); + xml.writeStartElement("firmware"); + xml.writeAttribute("version", QString::number(FirmwareInfo::kVersion)); + + xml.writeStartElement("name"); + xml.writeCharacters(name); + xml.writeEndElement(); + + xml.writeStartElement("version"); + xml.writeCharacters(version); + xml.writeEndElement(); + + platformInfo.WriteXml(xml); + + xml.writeStartElement("developers"); + + for (int i = 0; i < developers.length(); i++) + { + xml.writeStartElement("name"); + xml.writeCharacters(developers[i]); + xml.writeEndElement(); + } + + xml.writeEndElement(); + + if (!url.isEmpty()) + { + xml.writeStartElement("url"); + xml.writeCharacters(url); + xml.writeEndElement(); + } + + if (!donateUrl.isEmpty()) + { + xml.writeStartElement("donateurl"); + xml.writeCharacters(donateUrl); + xml.writeEndElement(); + } + + xml.writeStartElement("devices"); + + for (int i = 0; i < deviceInfos.length(); i++) + deviceInfos[i].WriteXml(xml); + + xml.writeEndElement(); + + xml.writeStartElement("pit"); + + int lastSlash = pitFilename.lastIndexOf('/'); + + if (lastSlash < 0) + lastSlash = pitFilename.lastIndexOf('\\'); + + xml.writeCharacters(pitFilename.mid(lastSlash + 1)); + + xml.writeEndElement(); + + xml.writeStartElement("repartition"); + xml.writeCharacters((repartition) ? "1" : "0"); + xml.writeEndElement(); + + xml.writeStartElement("noreboot"); + xml.writeCharacters((noReboot) ? "1" : "0"); + xml.writeEndElement(); + + xml.writeStartElement("files"); + + for (int i = 0; i < fileInfos.length(); i++) + fileInfos[i].WriteXml(xml); + + xml.writeEndElement(); + + xml.writeEndElement(); + xml.writeEndDocument(); +} diff --git a/heimdall-frontend/Source/FirmwareInfo.h b/heimdall-frontend/Source/FirmwareInfo.h index a72dab1..3fd6341 100755 --- a/heimdall-frontend/Source/FirmwareInfo.h +++ b/heimdall-frontend/Source/FirmwareInfo.h @@ -42,6 +42,7 @@ namespace HeimdallFrontend DeviceInfo(const QString& manufacturer, const QString& product, const QString& name); bool ParseXml(QXmlStreamReader& xml); + void WriteXml(QXmlStreamWriter& xml) const; const QString& GetManufacturer(void) const { @@ -89,6 +90,7 @@ namespace HeimdallFrontend bool IsCleared(void) const; bool ParseXml(QXmlStreamReader& xml); + void WriteXml(QXmlStreamWriter& xml) const; const QString& GetName(void) const { @@ -124,6 +126,7 @@ namespace HeimdallFrontend FileInfo(unsigned int partitionId, const QString& filename); bool ParseXml(QXmlStreamReader& xml); + void WriteXml(QXmlStreamWriter& xml) const; unsigned int GetPartitionId(void) const { @@ -170,6 +173,8 @@ namespace HeimdallFrontend QString pitFilename; bool repartition; + bool noReboot; + QList<FileInfo> fileInfos; public: @@ -180,6 +185,7 @@ namespace HeimdallFrontend bool IsCleared(void) const; bool ParseXml(QXmlStreamReader& xml); + void WriteXml(QXmlStreamWriter& xml) const; const QString& GetName(void) const { @@ -271,6 +277,16 @@ namespace HeimdallFrontend this->repartition = repartition; } + bool GetNoReboot(void) const + { + return (noReboot); + } + + void SetNoReboot(bool noReboot) + { + this->noReboot = noReboot; + } + const QList<FileInfo>& GetFileInfos(void) const { return (fileInfos); diff --git a/heimdall-frontend/Source/Packaging.cpp b/heimdall-frontend/Source/Packaging.cpp index cbd03a4..1034539 100755 --- a/heimdall-frontend/Source/Packaging.cpp +++ b/heimdall-frontend/Source/Packaging.cpp @@ -18,6 +18,10 @@ 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 <stdio.h> @@ -27,6 +31,7 @@ // Qt #include <QDateTime> #include <QDir> +#include <QProgressDialog> // Heimdall Frontend #include "Packaging.h" @@ -35,20 +40,44 @@ using namespace HeimdallFrontend; const char *Packaging::ustarMagic = "ustar"; -bool Packaging::ExtractTar(QTemporaryFile& tarFile, PackageData *outputPackageData) +bool Packaging::ExtractTar(QTemporaryFile& tarFile, PackageData *packageData) { TarHeader tarHeader; - tarFile.reset(); + if (!tarFile.open()) + { + // TODO: "Error opening temporary TAR archive." + return (false); + } bool previousEmpty = false; + QProgressDialog progressDialog("Extracting files...", "Cancel", 0, tarFile.size()); + progressDialog.setWindowModality(Qt::ApplicationModal); + progressDialog.setWindowTitle("Heimdall Frontend"); + while (!tarFile.atEnd()) { qint64 dataRead = tarFile.read(tarHeader.buffer, TarHeader::kBlockLength); if (dataRead != TarHeader::kBlockLength) + { + // TODO: "Package's TAR archive is malformed." + tarFile.close(); + progressDialog.close(); + return (false); + } + + progressDialog.setValue(tarFile.pos()); + + if (progressDialog.wasCanceled()) + { + tarFile.close(); + progressDialog.close(); + + return (false); + } bool ustarFormat = strcmp(tarHeader.fields.magic, ustarMagic) == 0; bool empty = true; @@ -73,7 +102,20 @@ bool Packaging::ExtractTar(QTemporaryFile& tarFile, PackageData *outputPackageDa } else { - // TODO: Check checksum + int checksum = 0; + + for (char *bufferIndex = tarHeader.buffer; bufferIndex < tarHeader.fields.checksum; bufferIndex++) + checksum += static_cast<unsigned char>(*bufferIndex); + + checksum += 8 * ' '; + checksum += static_cast<unsigned char>(tarHeader.fields.typeFlag); + + // Both the TAR and USTAR formats have terrible documentation, it's not clear if the following code is required. + /*if (ustarFormat) + { + for (char *bufferIndex = tarHeader.fields.linkName; bufferIndex < tarHeader.fields.prefix + 155; bufferIndex++) + checksum += static_cast<unsigned char>(*bufferIndex); + }*/ bool parsed = false; @@ -87,6 +129,7 @@ bool Packaging::ExtractTar(QTemporaryFile& tarFile, PackageData *outputPackageDa if (!parsed) { // TODO: Error message? + tarFile.close(); return (false); } @@ -95,14 +138,15 @@ bool Packaging::ExtractTar(QTemporaryFile& tarFile, PackageData *outputPackageDa // We're working with a file. QString filename = QString::fromUtf8(tarHeader.fields.name); - // This is slightly pointless as we don't support directories... - if (ustarFormat) - filename.prepend(tarHeader.fields.prefix); - QTemporaryFile *outputFile = new QTemporaryFile("XXXXXX-" + filename); - outputFile->open(); + packageData->GetFiles().append(outputFile); - outputPackageData->GetFiles().append(outputFile); + if (!outputFile->open()) + { + // TODO: "Failed to open output file \"%s\"" + tarFile.close(); + return (false); + } qulonglong dataRemaining = fileSize; char readBuffer[TarHeader::kBlockReadCount * TarHeader::kBlockLength]; @@ -116,18 +160,41 @@ bool Packaging::ExtractTar(QTemporaryFile& tarFile, PackageData *outputPackageDa qint64 dataRead = tarFile.read(readBuffer, fileDataToRead + (TarHeader::kBlockLength - fileDataToRead % TarHeader::kBlockLength) % TarHeader::kBlockLength); if (dataRead < fileDataToRead || dataRead % TarHeader::kBlockLength != 0) + { + // TODO: "Unexpected error extracting package files." + tarFile.close(); + outputFile->close(); + + remove(outputFile->fileName().toStdString().c_str()); + return (false); + } outputFile->write(readBuffer, fileDataToRead); dataRemaining -= fileDataToRead; + + progressDialog.setValue(tarFile.pos()); + + if (progressDialog.wasCanceled()) + { + tarFile.close(); + outputFile->close(); + + remove(outputFile->fileName().toStdString().c_str()); + + progressDialog.close(); + + return (false); + } } outputFile->close(); } else { - // We don't support links/directories. + // TODO: "Heimdall packages shouldn't contain links or directories." + tarFile.close(); return (false); } } @@ -135,195 +202,320 @@ bool Packaging::ExtractTar(QTemporaryFile& tarFile, PackageData *outputPackageDa previousEmpty = empty; } + progressDialog.close(); + tarFile.close(); + return (true); } -bool Packaging::CreateTar(const PackageData& packageData, QTemporaryFile *outputTarFile) +bool Packaging::WriteTarEntry(const QString& filename, QTemporaryFile *tarFile, bool firmwareXml) { - const QList<FileInfo>& fileInfos = packageData.GetFirmwareInfo().GetFileInfos(); + TarHeader tarHeader; + memset(tarHeader.buffer, 0, TarHeader::kBlockLength); + + QFile file(filename); - if (!outputTarFile->open()) + if (!file.open(QFile::ReadOnly)) { // TODO: "Failed to open \"%s\"" return (false); } - bool failure = false; - - TarHeader tarHeader; - - for (int i = 0; i < fileInfos.length(); i++) + if (file.size() > TarHeader::kMaxFileSize) { - memset(tarHeader.buffer, 0, TarHeader::kBlockLength); + // TODO: "File is too large to packaged" + return (false); + } - QFile file(fileInfos[i].GetFilename()); + QFileInfo qtFileInfo(file); + QByteArray utfFilename; - if (!file.open(QFile::ReadOnly)) - { - // TODO: "Failed to open \"%s\"" - failure = true; - break; - } + if (firmwareXml) + { + utfFilename = QString("firmware.xml").toUtf8(); + } + else + { + utfFilename = qtFileInfo.fileName().toUtf8(); - if (file.size() > TarHeader::kMaxFileSize) + if (utfFilename.length() > 100) { - // TODO: "File is too large to packaged" - failure = true; - break; + // TODO: "Filename is too long" + return (false); } + } - QFileInfo qtFileInfo(file); - strcpy(tarHeader.fields.name, qtFileInfo.fileName().toUtf8().constData()); + 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, "%o", mode); - - sprintf(tarHeader.fields.userId, "%o", qtFileInfo.ownerId()); - sprintf(tarHeader.fields.groupId, "%o", qtFileInfo.groupId()); - - // Note: We don't support base-256 encoding. Support could be added in future. - sprintf(tarHeader.fields.size, "%o", file.size()); - - sprintf(tarHeader.fields.modifiedTime, "%o", qtFileInfo.lastModified().toMSecsSinceEpoch()); + 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, "%011o", file.size()); + sprintf(tarHeader.fields.modifiedTime, "%011o", qtFileInfo.lastModified().toMSecsSinceEpoch() / 1000); - // Regular File - tarHeader.fields.typeFlag = '0'; + // Regular File + tarHeader.fields.typeFlag = '0'; - // Calculate checksum - int checksum = 0; + // Calculate checksum + int checksum = 0; + memset(tarHeader.fields.checksum, ' ', 8); + + for (int i = 0; i < TarHeader::kTarHeaderLength; i++) + checksum += static_cast<unsigned char>(tarHeader.buffer[i]); - for (int i = 0; i < TarHeader::kTarHeaderLength; i++) - checksum += tarHeader.buffer[i]; + sprintf(tarHeader.fields.checksum, "%07o", checksum); - sprintf(tarHeader.fields.checksum, "%o", checksum); + // Write the header to the TAR file. + tarFile->write(tarHeader.buffer, TarHeader::kBlockLength); - // Write the header to the TAR file. - outputTarFile->write(tarHeader.buffer, TarHeader::kBlockLength); + char buffer[TarHeader::kBlockWriteCount * TarHeader::kBlockLength]; + qint64 offset = 0; - char buffer[TarHeader::kBlockWriteCount * TarHeader::kBlockLength]; + while (offset < file.size()) + { + qint64 dataRead = file.read(buffer, TarHeader::kBlockWriteCount * TarHeader::kBlockLength); + + if (tarFile->write(buffer, dataRead) != dataRead) + { + // TODO: "Failed to write data to the temporary TAR file." + return (false); + } - for (qint64 i = 0; i < file.size(); i++) + if (dataRead % TarHeader::kBlockLength != 0) { - qint64 dataRead = file.read(buffer, TarHeader::kBlockWriteCount * TarHeader::kBlockLength); + int remainingBlockLength = TarHeader::kBlockLength - dataRead % TarHeader::kBlockLength; + memset(buffer, 0, remainingBlockLength); - if (outputTarFile->write(buffer, dataRead) != dataRead) + if (tarFile->write(buffer, remainingBlockLength) != remainingBlockLength) { // TODO: "Failed to write data to the temporary TAR file." - failure = true; - break; + return (false); } + } - if (dataRead % TarHeader::kBlockLength != 0) - { - int remainingBlockLength = TarHeader::kBlockLength - dataRead % TarHeader::kBlockLength; - memset(buffer, 0, remainingBlockLength); + offset += dataRead; + } - if (outputTarFile->write(buffer, remainingBlockLength) != remainingBlockLength) - { - // TODO: "Failed to write data to the temporary TAR file." - failure = true; - break; - } - } + return (true); +} + +bool Packaging::CreateTar(const FirmwareInfo& firmwareInfo, QTemporaryFile *tarFile) +{ + const QList<FileInfo>& 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()) + { + // TODO: "Failed to create temporary file "%s" + return (false); + } + + firmwareInfo.WriteXml(QXmlStreamWriter(&firmwareXmlFile)); + + firmwareXmlFile.close(); + + if (!tarFile->open()) + { + // TODO: "Failed to open \"%s\"" + return (false); + } + + for (int i = 0; i < fileInfos.length(); i++) + { + if (!WriteTarEntry(fileInfos[i].GetFilename(), tarFile)) + { + tarFile->resize(0); + tarFile->close(); + + progressDialog.close(); - i += dataRead; + return (false); } - if (failure) - break; + progressDialog.setValue(i); + + if (progressDialog.wasCanceled()) + { + tarFile->resize(0); + tarFile->close(); + + progressDialog.close(); + + return (false); + } } - if (failure) + if (!WriteTarEntry(firmwareInfo.GetPitFilename(), tarFile)) { - outputTarFile->resize(0); - outputTarFile->close(); + tarFile->resize(0); + tarFile->close(); + return (false); } + progressDialog.setValue(progressDialog.value() + 1); + + if (progressDialog.wasCanceled()) + { + tarFile->resize(0); + tarFile->close(); + + progressDialog.close(); + + return (false); + } + + if (!WriteTarEntry(firmwareXmlFile.fileName(), tarFile, true)) + { + tarFile->resize(0); + tarFile->close(); + + return (false); + } + + progressDialog.setValue(progressDialog.value() + 1); + progressDialog.close(); + // Write two empty blocks to signify the end of the archive. - memset(tarHeader.buffer, 0, TarHeader::kBlockLength); - outputTarFile->write(tarHeader.buffer, TarHeader::kBlockLength); - outputTarFile->write(tarHeader.buffer, TarHeader::kBlockLength); + char emptyEntry[TarHeader::kBlockLength]; + memset(emptyEntry, 0, TarHeader::kBlockLength); + + tarFile->write(emptyEntry, TarHeader::kBlockLength); + tarFile->write(emptyEntry, TarHeader::kBlockLength); - outputTarFile->close(); + tarFile->close(); return (true); } -bool Packaging::ExtractPackage(const QString& packagePath, PackageData *outputPackageData) +bool Packaging::ExtractPackage(const QString& packagePath, PackageData *packageData) { FILE *compressedPackageFile = fopen(packagePath.toStdString().c_str(), "rb"); + + if (fopen == NULL) + { + // TODO: "Failed to open package \"%s\"." + return (false); + } + + fseek(compressedPackageFile, 0, SEEK_END); + quint64 compressedFileSize = ftell(compressedPackageFile); + rewind(compressedPackageFile); + gzFile packageFile = gzdopen(fileno(compressedPackageFile), "rb"); QTemporaryFile outputTar("XXXXXX.tar"); if (!outputTar.open()) { + // TODO: "Error opening temporary TAR archive." gzclose(packageFile); return (false); } - char buffer[32768]; - + char buffer[kExtractBufferLength]; int bytesRead; + quint64 totalBytesRead = 0; + + QProgressDialog progressDialog("Decompressing package...", "Cancel", 0, compressedFileSize); + progressDialog.setWindowModality(Qt::ApplicationModal); + progressDialog.setWindowTitle("Heimdall Frontend"); do { - bytesRead = gzread(packageFile, buffer, 32768); + bytesRead = gzread(packageFile, buffer, kExtractBufferLength); if (bytesRead == -1) { + // TODO: "Error decompressing archive." gzclose(packageFile); + progressDialog.close(); return (false); } outputTar.write(buffer, bytesRead); + + totalBytesRead += bytesRead; + progressDialog.setValue(totalBytesRead); + + if (progressDialog.wasCanceled()) + { + gzclose(packageFile); + progressDialog.close(); + + return (false); + } } while (bytesRead > 0); + progressDialog.close(); + + outputTar.close(); gzclose(packageFile); // Closes packageFile and compressedPackageFile - if (!ExtractTar(outputTar, outputPackageData)) + if (!ExtractTar(outputTar, packageData)) return (false); // Find and read firmware.xml - for (int i = 0; i < outputPackageData->GetFiles().length(); i++) + for (int i = 0; i < packageData->GetFiles().length(); i++) { - QTemporaryFile *file = outputPackageData->GetFiles()[i]; + QTemporaryFile *file = packageData->GetFiles()[i]; if (file->fileTemplate() == "XXXXXX-firmware.xml") { - if (!outputPackageData->ReadFirmwareInfo(file)) + if (!packageData->ReadFirmwareInfo(file)) { - outputPackageData->Clear(); + packageData->Clear(); return (false); } @@ -334,12 +526,82 @@ bool Packaging::ExtractPackage(const QString& packagePath, PackageData *outputPa return (false); } -bool Packaging::BuildPackage(const QString& packagePath, const PackageData& packageData) +bool Packaging::BuildPackage(const QString& packagePath, const FirmwareInfo& firmwareInfo) { - QTemporaryFile temporaryFile("XXXXXX.tar"); + FILE *compressedPackageFile = fopen(packagePath.toStdString().c_str(), "wb"); - if (!CreateTar(packageData, &temporaryFile)) + if (fopen == NULL) + { + // TODO: "Failed to open package "%s" return (false); + } + + QTemporaryFile tar("XXXXXX.tar"); + + if (!CreateTar(firmwareInfo, &tar)) + return (false); + + if (!tar.open()) + { + // TODO: "Failed to open temporary file "%s" + fclose(compressedPackageFile); + remove(packagePath.toStdString().c_str()); + return (false); + } + + gzFile packageFile = gzdopen(fileno(compressedPackageFile), "wb"); + + char buffer[kCompressBufferLength]; + qint64 totalBytesRead = 0; + int bytesRead; + + QProgressDialog progressDialog("Compressing package...", "Cancel", 0, tar.size()); + progressDialog.setWindowModality(Qt::ApplicationModal); + progressDialog.setWindowTitle("Heimdall Frontend"); + + do + { + bytesRead = tar.read(buffer, kCompressBufferLength); + + if (bytesRead == -1) + { + // TODO: "Error reading temporary TAR file." + gzclose(packageFile); + remove(packagePath.toStdString().c_str()); + + progressDialog.close(); + + return (false); + } + + if (gzwrite(packageFile, buffer, bytesRead) != bytesRead) + { + // TODO: "Error compressing package." + gzclose(packageFile); + remove(packagePath.toStdString().c_str()); + + progressDialog.close(); + + return (false); + } + + totalBytesRead += bytesRead; + progressDialog.setValue(totalBytesRead); + + if (progressDialog.wasCanceled()) + { + gzclose(packageFile); + remove(packagePath.toStdString().c_str()); + + progressDialog.close(); + + return (false); + } + } while (bytesRead > 0); + + progressDialog.close(); + + gzclose(packageFile); // Closes packageFile and compressedPackageFile return (true); } diff --git a/heimdall-frontend/Source/Packaging.h b/heimdall-frontend/Source/Packaging.h index c341f0f..97637de 100755 --- a/heimdall-frontend/Source/Packaging.h +++ b/heimdall-frontend/Source/Packaging.h @@ -93,17 +93,25 @@ namespace HeimdallFrontend class Packaging { private: + + enum + { + kExtractBufferLength = 262144, + kCompressBufferLength = 262144 + }; // TODO: Add support for sparse files to both methods. - static bool ExtractTar(QTemporaryFile& tarFile, PackageData *outputPackageData); - static bool CreateTar(const PackageData& packageData, QTemporaryFile *outputTarFile); // Uses original TAR format. + static bool ExtractTar(QTemporaryFile& tarFile, PackageData *packageData); + + static bool WriteTarEntry(const QString& filename, QTemporaryFile *tarFile, bool firmwareXml = false); + static bool CreateTar(const FirmwareInfo& firmwareInfo, QTemporaryFile *tarFile); // Uses original TAR format. public: static const char *ustarMagic; - static bool ExtractPackage(const QString& packagePath, PackageData *outputPackageData); - static bool BuildPackage(const QString& packagePath, const PackageData& packageData); + static bool ExtractPackage(const QString& packagePath, PackageData *packageData); + static bool BuildPackage(const QString& packagePath, const FirmwareInfo& firmwareInfo); }; } diff --git a/heimdall-frontend/Source/mainwindow.cpp b/heimdall-frontend/Source/mainwindow.cpp index 8043599..b44c3de 100644 --- a/heimdall-frontend/Source/mainwindow.cpp +++ b/heimdall-frontend/Source/mainwindow.cpp @@ -91,6 +91,7 @@ void MainWindow::UpdatePackageUserInterface(void) developerDonateButton->setEnabled(false);
repartitionRadioButton->setChecked(false);
+ noRebootRadioButton->setChecked(false);
loadFirmwareButton->setEnabled(false);
}
@@ -126,7 +127,7 @@ void MainWindow::UpdatePackageUserInterface(void) for (int i = 0; i < loadedPackageData.GetFirmwareInfo().GetDeviceInfos().length(); i++)
{
const DeviceInfo& deviceInfo = loadedPackageData.GetFirmwareInfo().GetDeviceInfos()[i];
- supportedDevicesListWidget->addItem(deviceInfo.GetManufacturer() + " " + deviceInfo.GetName() + " (" + deviceInfo.GetProduct() + ")");
+ supportedDevicesListWidget->addItem(deviceInfo.GetManufacturer() + " " + deviceInfo.GetName() + ": " + deviceInfo.GetProduct());
}
for (int i = 0; i < loadedPackageData.GetFirmwareInfo().GetFileInfos().length(); i++)
@@ -136,6 +137,7 @@ void MainWindow::UpdatePackageUserInterface(void) }
repartitionRadioButton->setChecked(loadedPackageData.GetFirmwareInfo().GetRepartition());
+ noRebootRadioButton->setChecked(loadedPackageData.GetFirmwareInfo().GetNoReboot());
loadFirmwareButton->setEnabled(true);
}
@@ -143,7 +145,7 @@ void MainWindow::UpdatePackageUserInterface(void) bool MainWindow::IsArchive(QString path)
{
- // Not a real check but hopefully it gets the message across, don't flash archives!
+ // Not a real check but hopefully it gets the message across, don't directly flash archives!
return (path.endsWith(".tar", Qt::CaseInsensitive) || path.endsWith(".gz", Qt::CaseInsensitive) || path.endsWith(".zip", Qt::CaseInsensitive)
|| path.endsWith(".bz2", Qt::CaseInsensitive) || path.endsWith(".7z", Qt::CaseInsensitive) || path.endsWith(".rar", Qt::CaseInsensitive));
}
@@ -266,7 +268,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) QObject::connect(partitionFileBrowseButton, SIGNAL(clicked()), this, SLOT(SelectPartitionFile()));
QObject::connect(pitBrowseButton, SIGNAL(clicked()), this, SLOT(SelectPit()));
+
QObject::connect(repartitionCheckBox, SIGNAL(stateChanged(int)), this, SLOT(SetRepartition(int)));
+ QObject::connect(noRebootCheckBox, SIGNAL(stateChanged(int)), this, SLOT(SetNoReboot(int)));
QObject::connect(startFlashButton, SIGNAL(clicked()), this, SLOT(StartFlash()));
@@ -394,6 +398,7 @@ void MainWindow::LoadFirmwarePackage(void) UpdateUnusedPartitionIds();
workingPackageData.GetFirmwareInfo().SetRepartition(loadedPackageData.GetFirmwareInfo().GetRepartition());
+ workingPackageData.GetFirmwareInfo().SetNoReboot(loadedPackageData.GetFirmwareInfo().GetNoReboot());
loadedPackageData.Clear();
UpdatePackageUserInterface();
@@ -431,6 +436,9 @@ void MainWindow::LoadFirmwarePackage(void) repartitionCheckBox->setEnabled(true);
repartitionCheckBox->setChecked(workingPackageData.GetFirmwareInfo().GetRepartition());
+ noRebootCheckBox->setEnabled(true);
+ noRebootCheckBox->setChecked(workingPackageData.GetFirmwareInfo().GetNoReboot());
+
partitionsListWidget->setEnabled(true);
addPartitionButton->setEnabled(true);
removePartitionButton->setEnabled(true && partitionsListWidget->currentRow() >= 0);
@@ -466,7 +474,7 @@ void MainWindow::SelectPartitionFile(void) {
QString path = PromptFileSelection();
- if (path != "")
+ if (path != "" && !IsArchive(path))
{
workingPackageData.GetFirmwareInfo().GetFileInfos()[partitionsListWidget->currentRow()].SetFilename(path);
partitionFileLineEdit->setText(path);
@@ -608,6 +616,7 @@ void MainWindow::SelectPit(void) pitLineEdit->setText(workingPackageData.GetFirmwareInfo().GetPitFilename());
repartitionCheckBox->setEnabled(validPit);
+ noRebootCheckBox->setEnabled(validPit);
partitionsListWidget->setEnabled(validPit);
addPartitionButton->setEnabled(validPit);
@@ -620,24 +629,41 @@ void MainWindow::SetRepartition(int enabled) {
workingPackageData.GetFirmwareInfo().SetRepartition(enabled);
}
+void MainWindow::SetNoReboot(int enabled)
+{
+ workingPackageData.GetFirmwareInfo().SetNoReboot(enabled);
+}
void MainWindow::StartFlash(void)
{
+ outputPlainTextEdit->clear();
+
heimdallRunning = true;
heimdallFailed = false;
+
+ const FirmwareInfo& firmwareInfo = workingPackageData.GetFirmwareInfo();
+ const QList<FileInfo>& fileInfos = firmwareInfo.GetFileInfos();
QStringList arguments;
arguments.append("flash");
- if (repartitionCheckBox->isChecked())
- {
+ if (firmwareInfo.GetRepartition())
arguments.append("--repartition");
- arguments.append("--pit");
- arguments.append(pitLineEdit->text());
+ arguments.append("--pit");
+ arguments.append(firmwareInfo.GetPitFilename());
+
+ for (int i = 0; i < fileInfos.length(); i++)
+ {
+ QString flag;
+ flag.sprintf("--%u", fileInfos[i].GetPartitionId());
+
+ arguments.append(flag);
+ arguments.append(fileInfos[i].GetFilename());
}
- // TODO: Loop through partitions and append them.
+ if (firmwareInfo.GetNoReboot())
+ arguments.append("--no-reboot");
flashProgressBar->setEnabled(true);
UpdateStartButton();
@@ -785,10 +811,10 @@ void MainWindow::SelectDevice(int row) void MainWindow::AddDevice(void)
{
- workingPackageData.GetFirmwareInfo().GetDeviceInfos().append(DeviceInfo(deviceManufacturerLineEdit->text(), deviceNameLineEdit->text(),
- deviceProductCodeLineEdit->text()));
+ workingPackageData.GetFirmwareInfo().GetDeviceInfos().append(DeviceInfo(deviceManufacturerLineEdit->text(), deviceProductCodeLineEdit->text(),
+ deviceNameLineEdit->text()));
- createDevicesListWidget->addItem(deviceManufacturerLineEdit->text() + " " + deviceNameLineEdit->text() + " (" + deviceProductCodeLineEdit->text() + ")");
+ createDevicesListWidget->addItem(deviceManufacturerLineEdit->text() + " " + deviceNameLineEdit->text() + ": " + deviceProductCodeLineEdit->text());
deviceManufacturerLineEdit->clear();
deviceNameLineEdit->clear();
deviceProductCodeLineEdit->clear();
@@ -825,12 +851,12 @@ void MainWindow::BuildPackage(void) packagePath.append(".tar.gz");
}
- Packaging::BuildPackage(packagePath, workingPackageData);
+ Packaging::BuildPackage(packagePath, workingPackageData.GetFirmwareInfo());
}
void MainWindow::HandleHeimdallStdout(void)
{
- QString output = process.read(1024);
+ QString output = process.readAll();
// We often receive multiple lots of output from Heimdall at one time. So we use regular expressions
// to ensure we don't miss out on any important information.
@@ -845,35 +871,10 @@ void MainWindow::HandleHeimdallStdout(void) flashProgressBar->setValue(percentString.mid(1, percentString.length() - 2).toInt());
}
- /*// Handle other information
-
- int endOfLastLine = output.length() - 1;
- for (; endOfLastLine > -1; endOfLastLine--)
- {
- if (output[endOfLastLine] != '\n')
- break;
- }
-
- if (endOfLastLine < 0)
- return; // Output was blank or just a bunch of new line characters.
-
- int startOfLastLine = endOfLastLine - 1;
- for (; startOfLastLine > -1; startOfLastLine--)
- {
- if (output[startOfLastLine] == '\n')
- break;
- }
-
- startOfLastLine++;
-
- // Just look at the last line of the output
- output = output.mid(startOfLastLine, endOfLastLine - startOfLastLine + 1); // Work with the last line only
-
- percentExp.setPattern("[0-9]+%");
-
- // If the last line wasn't a uploading message or a percentage transferred then display it.
- if (output.lastIndexOf(uploadingExp) < 0 && output.lastIndexOf(percentExp) < 0)
- flashLabel->setText(output);*/
+ output.remove(QChar('\b'));
+ output.replace(QChar('%'), QString("%\n"));
+ outputPlainTextEdit->insertPlainText(output);
+ outputPlainTextEdit->ensureCursorVisible();
}
void MainWindow::HandleHeimdallReturned(int exitCode, QProcess::ExitStatus exitStatus)
@@ -894,6 +895,9 @@ void MainWindow::HandleHeimdallReturned(int exitCode, QProcess::ExitStatus exitS {
QString error = process.readAllStandardError();
+ outputPlainTextEdit->insertPlainText(error);
+ outputPlainTextEdit->ensureCursorVisible();
+
int firstNewLineChar = error.indexOf('\n');
if (firstNewLineChar == 0)
diff --git a/heimdall-frontend/Source/mainwindow.h b/heimdall-frontend/Source/mainwindow.h index 7525d67..6e16596 100644 --- a/heimdall-frontend/Source/mainwindow.h +++ b/heimdall-frontend/Source/mainwindow.h @@ -99,7 +99,9 @@ namespace HeimdallFrontend void RemovePartition(void);
void SelectPit(void);
+
void SetRepartition(int enabled);
+ void SetNoReboot(int enabled);
void StartFlash(void);
diff --git a/heimdall-frontend/aboutform.ui b/heimdall-frontend/aboutform.ui index 39e87cc..764e751 100644 --- a/heimdall-frontend/aboutform.ui +++ b/heimdall-frontend/aboutform.ui @@ -167,7 +167,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRES </property>
<property name="text">
<string>Heimdall Frontend
-Version 1.3.0
+Version 1.3 (beta)
Copyright © 2010 Benjamin Dobell, Glass Echidna</string>
</property>
<property name="textFormat">
diff --git a/heimdall-frontend/mainwindow.ui b/heimdall-frontend/mainwindow.ui index 6165b8d..020c761 100644 --- a/heimdall-frontend/mainwindow.ui +++ b/heimdall-frontend/mainwindow.ui @@ -108,9 +108,9 @@ <property name="geometry">
<rect>
<x>510</x>
- <y>20</y>
+ <y>10</y>
<width>251</width>
- <height>341</height>
+ <height>331</height>
</rect>
</property>
<property name="title">
@@ -122,7 +122,7 @@ <x>10</x>
<y>20</y>
<width>231</width>
- <height>311</height>
+ <height>301</height>
</rect>
</property>
</widget>
@@ -186,7 +186,7 @@ <property name="geometry">
<rect>
<x>520</x>
- <y>370</y>
+ <y>350</y>
<width>241</width>
<height>21</height>
</rect>
@@ -375,6 +375,22 @@ </property>
</widget>
</widget>
+ <widget class="QRadioButton" name="noRebootRadioButton">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>520</x>
+ <y>380</y>
+ <width>241</width>
+ <height>21</height>
+ </rect>
+ </property>
+ <property name="text">
+ <string>No Reboot Recommended</string>
+ </property>
+ </widget>
</widget>
<widget class="QWidget" name="flashTab">
<attribute name="title">
@@ -410,7 +426,7 @@ </widget>
<widget class="QPlainTextEdit" name="outputPlainTextEdit">
<property name="enabled">
- <bool>false</bool>
+ <bool>true</bool>
</property>
<property name="geometry">
<rect>
@@ -420,6 +436,15 @@ <height>91</height>
</rect>
</property>
+ <property name="undoRedoEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="plainText">
+ <string notr="true"/>
+ </property>
</widget>
<widget class="QLabel" name="flashLabel">
<property name="geometry">
@@ -478,7 +503,7 @@ <x>10</x>
<y>20</y>
<width>381</width>
- <height>91</height>
+ <height>51</height>
</rect>
</property>
<property name="title">
@@ -491,7 +516,7 @@ <property name="geometry">
<rect>
<x>10</x>
- <y>50</y>
+ <y>20</y>
<width>281</width>
<height>21</height>
</rect>
@@ -507,7 +532,7 @@ <property name="geometry">
<rect>
<x>300</x>
- <y>50</y>
+ <y>20</y>
<width>71</width>
<height>23</height>
</rect>
@@ -516,22 +541,6 @@ <string>Browse</string>
</property>
</widget>
- <widget class="QCheckBox" name="repartitionCheckBox">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="geometry">
- <rect>
- <x>10</x>
- <y>20</y>
- <width>91</width>
- <height>17</height>
- </rect>
- </property>
- <property name="text">
- <string>Repartition</string>
- </property>
- </widget>
</widget>
<widget class="QPushButton" name="removePartitionButton">
<property name="enabled">
@@ -540,7 +549,7 @@ <property name="geometry">
<rect>
<x>670</x>
- <y>230</y>
+ <y>240</y>
<width>71</width>
<height>23</height>
</rect>
@@ -669,7 +678,7 @@ <property name="geometry">
<rect>
<x>400</x>
- <y>230</y>
+ <y>240</y>
<width>71</width>
<height>23</height>
</rect>
@@ -687,9 +696,41 @@ <x>400</x>
<y>20</y>
<width>341</width>
- <height>201</height>
+ <height>211</height>
+ </rect>
+ </property>
+ </widget>
+ <widget class="QCheckBox" name="noRebootCheckBox">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>150</x>
+ <y>80</y>
+ <width>121</width>
+ <height>21</height>
+ </rect>
+ </property>
+ <property name="text">
+ <string>No Reboot</string>
+ </property>
+ </widget>
+ <widget class="QCheckBox" name="repartitionCheckBox">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>20</x>
+ <y>80</y>
+ <width>121</width>
+ <height>21</height>
</rect>
</property>
+ <property name="text">
+ <string>Repartition</string>
+ </property>
</widget>
</widget>
</widget>
@@ -1221,7 +1262,6 @@ <tabstop>includedFilesListWidget</tabstop>
<tabstop>repartitionRadioButton</tabstop>
<tabstop>loadFirmwareButton</tabstop>
- <tabstop>repartitionCheckBox</tabstop>
<tabstop>pitLineEdit</tabstop>
<tabstop>pitBrowseButton</tabstop>
<tabstop>partitionNameComboBox</tabstop>
|