mirror of
https://github.com/LongSoft/UEFITool.git
synced 2024-11-22 07:58:22 +08:00
add FFSv3 support with large files and large sections
This commit is contained in:
parent
75225ecc28
commit
73019876cf
@ -1,6 +1,6 @@
|
||||
/* uefipatch_main.cpp
|
||||
|
||||
Copyright (c) 2015, Nikolaj Schlej. All rights reserved.
|
||||
Copyright (c) 2017, LongSoft. All rights reserved.
|
||||
This program and the accompanying materials
|
||||
are licensed and made available under the terms and conditions of the BSD License
|
||||
which accompanies this distribution. The full text of the license may be found at
|
||||
@ -19,8 +19,8 @@ WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication a(argc, argv);
|
||||
a.setOrganizationName("CodeRush");
|
||||
a.setOrganizationDomain("coderush.me");
|
||||
a.setOrganizationName("LongSoft");
|
||||
a.setOrganizationDomain("longsoft.me");
|
||||
a.setApplicationName("UEFIPatch");
|
||||
|
||||
UEFIPatch w;
|
||||
@ -31,7 +31,7 @@ int main(int argc, char *argv[])
|
||||
result = w.patchFromFile(a.arguments().at(1));
|
||||
}
|
||||
else {
|
||||
std::cout << "UEFIPatch 0.3.9 - UEFI image file patching utility" << std::endl << std::endl <<
|
||||
std::cout << "UEFIPatch 0.3.10 - UEFI image file patching utility" << std::endl << std::endl <<
|
||||
"Usage: UEFIPatch image_file" << std::endl << std::endl <<
|
||||
"Patches will be read from patches.txt file\n";
|
||||
return ERR_SUCCESS;
|
||||
|
23
ffs.cpp
23
ffs.cpp
@ -30,7 +30,10 @@ const QVector<QByteArray> FFSv3Volumes =
|
||||
const UINT8 ffsAlignmentTable[] =
|
||||
{ 0, 4, 7, 9, 10, 12, 15, 16 };
|
||||
|
||||
UINT8 calculateChecksum8(const UINT8* buffer, UINT32 bufferSize)
|
||||
const UINT8 ffsAlignment2Table[] =
|
||||
{ 17, 18, 19, 20, 21, 22, 23, 24 };
|
||||
|
||||
UINT8 calculateSum8(const UINT8* buffer, UINT32 bufferSize)
|
||||
{
|
||||
if (!buffer)
|
||||
return 0;
|
||||
@ -40,7 +43,15 @@ UINT8 calculateChecksum8(const UINT8* buffer, UINT32 bufferSize)
|
||||
while (bufferSize--)
|
||||
counter += buffer[bufferSize];
|
||||
|
||||
return (UINT8)0x100 - counter;
|
||||
return counter;
|
||||
}
|
||||
|
||||
UINT8 calculateChecksum8(const UINT8* buffer, UINT32 bufferSize)
|
||||
{
|
||||
if (!buffer)
|
||||
return 0;
|
||||
|
||||
return (UINT8)0x100 - calculateSum8(buffer, bufferSize);
|
||||
}
|
||||
|
||||
UINT16 calculateChecksum16(const UINT16* buffer, UINT32 bufferSize)
|
||||
@ -120,6 +131,8 @@ QString fileTypeToQString(const UINT8 type)
|
||||
case EFI_FV_FILETYPE_FIRMWARE_VOLUME_IMAGE: return QObject::tr("Volume image");
|
||||
case EFI_FV_FILETYPE_COMBINED_SMM_DXE: return QObject::tr("Combined SMM/DXE");
|
||||
case EFI_FV_FILETYPE_SMM_CORE: return QObject::tr("SMM core");
|
||||
case EFI_FV_FILETYPE_SMM_STANDALONE: return QObject::tr("SMM standalone");
|
||||
case EFI_FV_FILETYPE_SMM_CORE_STANDALONE: return QObject::tr("SMM core standalone");
|
||||
case EFI_FV_FILETYPE_PAD: return QObject::tr("Pad");
|
||||
default: return QObject::tr("Unknown");
|
||||
};
|
||||
@ -155,10 +168,10 @@ UINT32 sizeOfSectionHeader(const EFI_COMMON_SECTION_HEADER* header)
|
||||
if (!header)
|
||||
return 0;
|
||||
|
||||
const bool extended = false;
|
||||
/*if (uint24ToUint32(header->Size) == EFI_SECTION2_IS_USED) {
|
||||
bool extended = false;
|
||||
if (uint24ToUint32(header->Size) == EFI_SECTION2_IS_USED) {
|
||||
extended = true;
|
||||
}*/
|
||||
}
|
||||
|
||||
switch (header->Type)
|
||||
{
|
||||
|
14
ffs.h
14
ffs.h
@ -292,7 +292,7 @@ UINT8 Type;
|
||||
UINT8 Attributes;
|
||||
UINT8 Size[3]; // Set to 0xFFFFFF
|
||||
UINT8 State;
|
||||
UINT32 ExtendedSize;
|
||||
UINT64 ExtendedSize;
|
||||
} EFI_FFS_FILE_HEADER2;
|
||||
|
||||
// Standard data checksum, used if FFS_ATTRIB_CHECKSUM is clear
|
||||
@ -314,6 +314,8 @@ UINT32 ExtendedSize;
|
||||
#define EFI_FV_FILETYPE_FIRMWARE_VOLUME_IMAGE 0x0B
|
||||
#define EFI_FV_FILETYPE_COMBINED_SMM_DXE 0x0C
|
||||
#define EFI_FV_FILETYPE_SMM_CORE 0x0D
|
||||
#define EFI_FV_FILETYPE_SMM_STANDALONE 0x0E
|
||||
#define EFI_FV_FILETYPE_SMM_CORE_STANDALONE 0x0F
|
||||
#define EFI_FV_FILETYPE_OEM_MIN 0xC0
|
||||
#define EFI_FV_FILETYPE_OEM_MAX 0xDF
|
||||
#define EFI_FV_FILETYPE_DEBUG_MIN 0xE0
|
||||
@ -326,6 +328,7 @@ UINT32 ExtendedSize;
|
||||
#define FFS_ATTRIB_TAIL_PRESENT 0x01 // Valid only for revision 1 volumes
|
||||
#define FFS_ATTRIB_RECOVERY 0x02 // Valid only for revision 1 volumes
|
||||
#define FFS_ATTRIB_LARGE_FILE 0x01 // Valid only for FFSv3 volumes
|
||||
#define FFS_ATTRIB_DATA_ALIGNMENT2 0x02 // Valid only for revision 2 volumes
|
||||
#define FFS_ATTRIB_FIXED 0x04
|
||||
#define FFS_ATTRIB_DATA_ALIGNMENT 0x38
|
||||
#define FFS_ATTRIB_CHECKSUM 0x40
|
||||
@ -333,6 +336,9 @@ UINT32 ExtendedSize;
|
||||
// FFS alignment table
|
||||
extern const UINT8 ffsAlignmentTable[];
|
||||
|
||||
// Extended FFS alignment table
|
||||
extern const UINT8 ffsAlignment2Table[];
|
||||
|
||||
// File states
|
||||
#define EFI_FILE_HEADER_CONSTRUCTION 0x01
|
||||
#define EFI_FILE_HEADER_VALID 0x02
|
||||
@ -360,7 +366,9 @@ const QByteArray EFI_FFS_PAD_FILE_GUID
|
||||
// FFS size conversion routines
|
||||
extern VOID uint32ToUint24(UINT32 size, UINT8* ffsSize);
|
||||
extern UINT32 uint24ToUint32(const UINT8* ffsSize);
|
||||
// FFS file 8bit checksum calculation routine
|
||||
|
||||
// FFS file 8bit checksum calculation routines
|
||||
extern UINT8 calculateSum8(const UINT8* buffer, UINT32 bufferSize);
|
||||
extern UINT8 calculateChecksum8(const UINT8* buffer, UINT32 bufferSize);
|
||||
|
||||
//*****************************************************************************
|
||||
@ -528,7 +536,7 @@ typedef EFI_COMMON_SECTION_HEADER2 EFI_FIRMWARE_VOLUME_IMAGE_SECTION2;
|
||||
typedef EFI_COMMON_SECTION_HEADER EFI_USER_INTERFACE_SECTION;
|
||||
typedef EFI_COMMON_SECTION_HEADER2 EFI_USER_INTERFACE_SECTION2;
|
||||
|
||||
//Section routines
|
||||
// Section routines
|
||||
extern UINT32 sizeOfSectionHeader(const EFI_COMMON_SECTION_HEADER* header);
|
||||
|
||||
//*****************************************************************************
|
||||
|
295
ffsengine.cpp
295
ffsengine.cpp
@ -287,15 +287,9 @@ UINT8 FfsEngine::parseIntelImage(const QByteArray & intelImage, QModelIndex & in
|
||||
const FLASH_DESCRIPTOR_COMPONENT_SECTION* componentSection = (const FLASH_DESCRIPTOR_COMPONENT_SECTION*)calculateAddress8(descriptor, descriptorMap->ComponentBase);
|
||||
|
||||
// Check descriptor version by getting hardcoded value of FlashParameters.ReadClockFrequency
|
||||
UINT8 descriptorVersion = 0;
|
||||
if (componentSection->FlashParameters.ReadClockFrequency == FLASH_FREQUENCY_20MHZ) // Old descriptor
|
||||
UINT8 descriptorVersion = 2; // Skylake+ by default
|
||||
if (componentSection->FlashParameters.ReadClockFrequency == FLASH_FREQUENCY_20MHZ) // Old descriptor
|
||||
descriptorVersion = 1;
|
||||
else if (componentSection->FlashParameters.ReadClockFrequency == FLASH_FREQUENCY_17MHZ) // Skylake+ descriptor
|
||||
descriptorVersion = 2;
|
||||
else {
|
||||
msg(tr("parseIntelImage: unknown descriptor version with ReadClockFrequency %1h").hexarg(componentSection->FlashParameters.ReadClockFrequency));
|
||||
return ERR_INVALID_FLASH_DESCRIPTOR;
|
||||
}
|
||||
|
||||
// ME region
|
||||
QByteArray me;
|
||||
@ -983,15 +977,16 @@ UINT8 FfsEngine::parseVolume(const QByteArray & volume, QModelIndex & index, co
|
||||
else
|
||||
headerSize = volumeHeader->HeaderLength;
|
||||
|
||||
// Sanity check after some new crazy MSI images
|
||||
// Sanity check after some crazy MSI images
|
||||
headerSize = ALIGN8(headerSize);
|
||||
|
||||
// Check for volume structure to be known
|
||||
bool volumeIsUnknown = true;
|
||||
|
||||
// Check for FFS v2 volume
|
||||
if (FFSv2Volumes.contains(QByteArray::fromRawData((const char*)volumeHeader->FileSystemGuid.Data, sizeof(EFI_GUID)))) {
|
||||
volumeIsUnknown = false;
|
||||
// Check for FFS v2/v3 volume
|
||||
UINT8 subtype = Subtypes::UnknownVolume;
|
||||
if (FFSv2Volumes.contains(QByteArray::fromRawData((const char*)volumeHeader->FileSystemGuid.Data, sizeof(EFI_GUID)))){
|
||||
subtype = Subtypes::Ffs2Volume;
|
||||
}
|
||||
else if (FFSv3Volumes.contains(QByteArray::fromRawData((const char*)volumeHeader->FileSystemGuid.Data, sizeof(EFI_GUID)))) {
|
||||
subtype = Subtypes::Ffs3Volume;
|
||||
}
|
||||
|
||||
// Check attributes
|
||||
@ -1063,10 +1058,10 @@ UINT8 FfsEngine::parseVolume(const QByteArray & volume, QModelIndex & index, co
|
||||
// Add tree item
|
||||
QByteArray header = volume.left(headerSize);
|
||||
QByteArray body = volume.mid(headerSize, volumeSize - headerSize);
|
||||
index = model->addItem(Types::Volume, volumeIsUnknown ? Subtypes::UnknownVolume : Subtypes::Ffs2Volume, COMPRESSION_ALGORITHM_NONE, name, text, info, header, body, parent, mode);
|
||||
index = model->addItem(Types::Volume, subtype, COMPRESSION_ALGORITHM_NONE, name, text, info, header, body, parent, mode);
|
||||
|
||||
// Show messages
|
||||
if (volumeIsUnknown) {
|
||||
if (subtype == Subtypes::UnknownVolume) {
|
||||
msg(tr("parseVolume: unknown file system %1").arg(guidToQString(volumeHeader->FileSystemGuid)), index);
|
||||
// Do not parse unknown volumes
|
||||
return ERR_SUCCESS;
|
||||
@ -1100,19 +1095,42 @@ UINT8 FfsEngine::parseVolume(const QByteArray & volume, QModelIndex & index, co
|
||||
break;
|
||||
}
|
||||
|
||||
result = getFileSize(volume, fileOffset, fileSize);
|
||||
if (result)
|
||||
return result;
|
||||
QByteArray tempFile = volume.mid(fileOffset, sizeof(EFI_FFS_FILE_HEADER));
|
||||
const EFI_FFS_FILE_HEADER* tempFileHeader = (const EFI_FFS_FILE_HEADER*)tempFile.constData();
|
||||
UINT32 fileHeaderSize = sizeof(EFI_FFS_FILE_HEADER);
|
||||
fileSize = uint24ToUint32(tempFileHeader->Size);
|
||||
if (volumeHeader->Revision > 1 && (tempFileHeader->Attributes & FFS_ATTRIB_LARGE_FILE)) {
|
||||
// Check if it's possibly the latest file in the volume
|
||||
if (volumeSize - fileOffset < sizeof(EFI_FFS_FILE_HEADER2)) {
|
||||
// No files are possible after this point
|
||||
// All the rest is either free space or non-UEFI data
|
||||
QByteArray rest = volume.right(volumeSize - fileOffset);
|
||||
if (rest.count(empty) == rest.size()) { // It's a free space
|
||||
model->addItem(Types::FreeSpace, 0, COMPRESSION_ALGORITHM_NONE, tr("Volume free space"), "", tr("Full size: %1h (%2)").hexarg(rest.size()).arg(rest.size()), QByteArray(), rest, index);
|
||||
}
|
||||
else { //It's non-UEFI data
|
||||
QModelIndex dataIndex = model->addItem(Types::Padding, Subtypes::DataPadding, COMPRESSION_ALGORITHM_NONE, tr("Non-UEFI data"), "", tr("Full size: %1h (%2)").hexarg(rest.size()).arg(rest.size()), QByteArray(), rest, index);
|
||||
msg(tr("parseVolume: non-UEFI data found in volume's free space"), dataIndex);
|
||||
}
|
||||
// Exit from loop
|
||||
break;
|
||||
}
|
||||
|
||||
// Check file size to be at least size of EFI_FFS_FILE_HEADER
|
||||
if (fileSize < sizeof(EFI_FFS_FILE_HEADER)) {
|
||||
fileHeaderSize = sizeof(EFI_FFS_FILE_HEADER2);
|
||||
tempFile = volume.mid(fileOffset, sizeof(EFI_FFS_FILE_HEADER2));
|
||||
const EFI_FFS_FILE_HEADER2* tempFileHeader2 = (const EFI_FFS_FILE_HEADER2*)tempFile.constData();
|
||||
fileSize = (UINT32)tempFileHeader2->ExtendedSize;
|
||||
}
|
||||
|
||||
// Check file size to be at least size of the header
|
||||
if (fileSize < fileHeaderSize) {
|
||||
msg(tr("parseVolume: volume has FFS file with invalid size"), index);
|
||||
return ERR_INVALID_FILE;
|
||||
}
|
||||
|
||||
|
||||
QByteArray file = volume.mid(fileOffset, fileSize);
|
||||
QByteArray header = file.left(sizeof(EFI_FFS_FILE_HEADER));
|
||||
|
||||
QByteArray header = file.left(fileHeaderSize);
|
||||
|
||||
// If we are at empty space in the end of volume
|
||||
if (header.count(empty) == header.size()) {
|
||||
// Check free space to be actually free
|
||||
@ -1152,8 +1170,11 @@ UINT8 FfsEngine::parseVolume(const QByteArray & volume, QModelIndex & index, co
|
||||
// Check file alignment
|
||||
const EFI_FFS_FILE_HEADER* fileHeader = (const EFI_FFS_FILE_HEADER*)header.constData();
|
||||
UINT8 alignmentPower = ffsAlignmentTable[(fileHeader->Attributes & FFS_ATTRIB_DATA_ALIGNMENT) >> 3];
|
||||
UINT32 alignment = (UINT32)pow(2.0, alignmentPower);
|
||||
if ((fileOffset + sizeof(EFI_FFS_FILE_HEADER)) % alignment)
|
||||
if (volumeHeader->Revision > 1 && (fileHeader->Attributes & FFS_ATTRIB_DATA_ALIGNMENT2))
|
||||
alignmentPower = ffsAlignment2Table[(fileHeader->Attributes & FFS_ATTRIB_DATA_ALIGNMENT) >> 3];
|
||||
|
||||
UINT32 alignment = (UINT32)(1UL << alignmentPower);
|
||||
if ((fileOffset + fileHeaderSize) % alignment)
|
||||
msgUnalignedFile = true;
|
||||
|
||||
// Check file GUID
|
||||
@ -1165,7 +1186,7 @@ UINT8 FfsEngine::parseVolume(const QByteArray & volume, QModelIndex & index, co
|
||||
|
||||
// Parse file
|
||||
QModelIndex fileIndex;
|
||||
result = parseFile(file, fileIndex, empty == '\xFF' ? ERASE_POLARITY_TRUE : ERASE_POLARITY_FALSE, index);
|
||||
result = parseFile(file, fileIndex, volumeHeader->Revision, empty == '\xFF' ? ERASE_POLARITY_TRUE : ERASE_POLARITY_FALSE, index);
|
||||
if (result && result != ERR_VOLUMES_NOT_FOUND && result != ERR_INVALID_VOLUME)
|
||||
msg(tr("parseVolume: FFS file parsing failed with error \"%1\"").arg(errorMessage(result)), index);
|
||||
|
||||
@ -1183,16 +1204,7 @@ UINT8 FfsEngine::parseVolume(const QByteArray & volume, QModelIndex & index, co
|
||||
return ERR_SUCCESS;
|
||||
}
|
||||
|
||||
UINT8 FfsEngine::getFileSize(const QByteArray & volume, const UINT32 fileOffset, UINT32 & fileSize)
|
||||
{
|
||||
if ((UINT32)volume.size() < fileOffset + sizeof(EFI_FFS_FILE_HEADER))
|
||||
return ERR_INVALID_VOLUME;
|
||||
const EFI_FFS_FILE_HEADER* fileHeader = (const EFI_FFS_FILE_HEADER*)(volume.constData() + fileOffset);
|
||||
fileSize = uint24ToUint32(fileHeader->Size);
|
||||
return ERR_SUCCESS;
|
||||
}
|
||||
|
||||
UINT8 FfsEngine::parseFile(const QByteArray & file, QModelIndex & index, const UINT8 erasePolarity, const QModelIndex & parent, const UINT8 mode)
|
||||
UINT8 FfsEngine::parseFile(const QByteArray & file, QModelIndex & index, const UINT8 revision, const UINT8 erasePolarity, const QModelIndex & parent, const UINT8 mode)
|
||||
{
|
||||
bool msgInvalidHeaderChecksum = false;
|
||||
bool msgInvalidDataChecksum = false;
|
||||
@ -1200,43 +1212,32 @@ UINT8 FfsEngine::parseFile(const QByteArray & file, QModelIndex & index, const U
|
||||
bool msgInvalidType = false;
|
||||
|
||||
// Populate file header
|
||||
if ((UINT32)file.size() < sizeof(EFI_FFS_FILE_HEADER))
|
||||
return ERR_INVALID_FILE;
|
||||
const EFI_FFS_FILE_HEADER* fileHeader = (const EFI_FFS_FILE_HEADER*)file.constData();
|
||||
|
||||
// Check file state
|
||||
// Construct empty byte for this file
|
||||
char empty = erasePolarity ? '\xFF' : '\x00';
|
||||
|
||||
// Check header checksum
|
||||
// Get file header
|
||||
QByteArray header = file.left(sizeof(EFI_FFS_FILE_HEADER));
|
||||
QByteArray tempHeader = header;
|
||||
EFI_FFS_FILE_HEADER* tempFileHeader = (EFI_FFS_FILE_HEADER*)(tempHeader.data());
|
||||
tempFileHeader->IntegrityCheck.Checksum.Header = 0;
|
||||
tempFileHeader->IntegrityCheck.Checksum.File = 0;
|
||||
UINT8 calculated = calculateChecksum8((const UINT8*)tempFileHeader, sizeof(EFI_FFS_FILE_HEADER) - 1);
|
||||
if (fileHeader->IntegrityCheck.Checksum.Header != calculated)
|
||||
if (revision > 1 && (fileHeader->Attributes & FFS_ATTRIB_LARGE_FILE)) {
|
||||
if ((UINT32)file.size() < sizeof(EFI_FFS_FILE_HEADER2))
|
||||
return ERR_INVALID_FILE;
|
||||
header = file.left(sizeof(EFI_FFS_FILE_HEADER2));
|
||||
}
|
||||
|
||||
// Check header checksum
|
||||
UINT8 calculatedHeader = 0x100 -(calculateSum8((const UINT8*)header.constData(), header.size()) - fileHeader->IntegrityCheck.Checksum.Header - fileHeader->IntegrityCheck.Checksum.File - fileHeader->State);
|
||||
if (fileHeader->IntegrityCheck.Checksum.Header != calculatedHeader)
|
||||
msgInvalidHeaderChecksum = true;
|
||||
|
||||
// Check data checksum
|
||||
// Data checksum must be calculated
|
||||
if (fileHeader->Attributes & FFS_ATTRIB_CHECKSUM) {
|
||||
UINT32 bufferSize = file.size() - sizeof(EFI_FFS_FILE_HEADER);
|
||||
// Exclude file tail from data checksum calculation
|
||||
if (fileHeader->Attributes & FFS_ATTRIB_TAIL_PRESENT)
|
||||
bufferSize -= sizeof(UINT16);
|
||||
calculated = calculateChecksum8((const UINT8*)(file.constData() + sizeof(EFI_FFS_FILE_HEADER)), bufferSize);
|
||||
if (fileHeader->IntegrityCheck.Checksum.File != calculated)
|
||||
msgInvalidDataChecksum = true;
|
||||
}
|
||||
// Data checksum must be one of predefined values
|
||||
else if (fileHeader->IntegrityCheck.Checksum.File != FFS_FIXED_CHECKSUM && fileHeader->IntegrityCheck.Checksum.File != FFS_FIXED_CHECKSUM2)
|
||||
msgInvalidDataChecksum = true;
|
||||
|
||||
// Get file body
|
||||
QByteArray body = file.right(file.size() - sizeof(EFI_FFS_FILE_HEADER));
|
||||
QByteArray body = file.mid(header.size());
|
||||
|
||||
// Check for file tail presence
|
||||
QByteArray tail;
|
||||
if (fileHeader->Attributes & FFS_ATTRIB_TAIL_PRESENT)
|
||||
{
|
||||
if (revision == 1 && fileHeader->Attributes & FFS_ATTRIB_TAIL_PRESENT) {
|
||||
//Check file tail;
|
||||
tail = body.right(sizeof(UINT16));
|
||||
UINT16 tailValue = *(UINT16*)tail.constData();
|
||||
@ -1247,48 +1248,47 @@ UINT8 FfsEngine::parseFile(const QByteArray & file, QModelIndex & index, const U
|
||||
body = body.left(body.size() - sizeof(UINT16));
|
||||
}
|
||||
|
||||
// Check data checksum
|
||||
// Data checksum must be calculated
|
||||
UINT8 calculatedData = 0;
|
||||
if (fileHeader->Attributes & FFS_ATTRIB_CHECKSUM) {
|
||||
calculatedData = calculateChecksum8((const UINT8*)body.constData(), body.size());
|
||||
if (fileHeader->IntegrityCheck.Checksum.File != calculatedData)
|
||||
msgInvalidDataChecksum = true;
|
||||
}
|
||||
// Data checksum must be one of predefined values
|
||||
else if ((revision == 1 && fileHeader->IntegrityCheck.Checksum.File != FFS_FIXED_CHECKSUM)
|
||||
|| fileHeader->IntegrityCheck.Checksum.File != FFS_FIXED_CHECKSUM2)
|
||||
msgInvalidDataChecksum = true;
|
||||
|
||||
// Parse current file by default
|
||||
bool parseCurrentFile = true;
|
||||
bool parseCurrentFile = false;
|
||||
bool parseAsBios = false;
|
||||
|
||||
// Check file type
|
||||
switch (fileHeader->Type)
|
||||
{
|
||||
switch (fileHeader->Type) {
|
||||
case EFI_FV_FILETYPE_ALL:
|
||||
parseAsBios = true;
|
||||
break;
|
||||
case EFI_FV_FILETYPE_RAW:
|
||||
parseAsBios = true;
|
||||
break;
|
||||
case EFI_FV_FILETYPE_FREEFORM:
|
||||
break;
|
||||
case EFI_FV_FILETYPE_SECURITY_CORE:
|
||||
break;
|
||||
case EFI_FV_FILETYPE_PEI_CORE:
|
||||
break;
|
||||
case EFI_FV_FILETYPE_DXE_CORE:
|
||||
break;
|
||||
case EFI_FV_FILETYPE_PEIM:
|
||||
break;
|
||||
case EFI_FV_FILETYPE_DRIVER:
|
||||
break;
|
||||
case EFI_FV_FILETYPE_COMBINED_PEIM_DRIVER:
|
||||
break;
|
||||
case EFI_FV_FILETYPE_APPLICATION:
|
||||
break;
|
||||
case EFI_FV_FILETYPE_SMM:
|
||||
break;
|
||||
case EFI_FV_FILETYPE_FIRMWARE_VOLUME_IMAGE:
|
||||
break;
|
||||
case EFI_FV_FILETYPE_COMBINED_SMM_DXE:
|
||||
break;
|
||||
case EFI_FV_FILETYPE_SMM_CORE:
|
||||
break;
|
||||
case EFI_FV_FILETYPE_SMM_STANDALONE:
|
||||
case EFI_FV_FILETYPE_SMM_CORE_STANDALONE:
|
||||
case EFI_FV_FILETYPE_PAD:
|
||||
parseCurrentFile = true;
|
||||
break;
|
||||
default:
|
||||
msgInvalidType = true;
|
||||
parseCurrentFile = false;
|
||||
};
|
||||
|
||||
// Check for empty file
|
||||
@ -1310,25 +1310,27 @@ UINT8 FfsEngine::parseFile(const QByteArray & file, QModelIndex & index, const U
|
||||
else
|
||||
name = parseAsNonEmptyPadFile ? tr("Non-empty pad-file") : tr("Pad-file");
|
||||
|
||||
info = tr("File GUID: %1\nType: %2h\nAttributes: %3h\nFull size: %4h (%5)\nHeader size: %6h (%7)\nBody size: %8h (%9)\nState: %10h")
|
||||
info = tr("File GUID: %1\nType: %2h\nAttributes: %3h\nFull size: %4h (%5)\nHeader size: %6h (%7)\nBody size: %8h (%9)\nState: %10h\nHeader checksum: %11h\nData checksum: %12h")
|
||||
.arg(guidToQString(fileHeader->Name))
|
||||
.hexarg2(fileHeader->Type, 2)
|
||||
.hexarg2(fileHeader->Attributes, 2)
|
||||
.hexarg(header.size() + body.size() + tail.size()).arg(header.size() + body.size() + tail.size())
|
||||
.hexarg(header.size()).arg(header.size())
|
||||
.hexarg(body.size()).arg(body.size())
|
||||
.hexarg2(fileHeader->State, 2);
|
||||
.hexarg2(fileHeader->State, 2)
|
||||
.hexarg2(fileHeader->IntegrityCheck.Checksum.Header, 2)
|
||||
.hexarg2(fileHeader->IntegrityCheck.Checksum.File, 2);
|
||||
|
||||
// Add tree item
|
||||
index = model->addItem(Types::File, fileHeader->Type, COMPRESSION_ALGORITHM_NONE, name, "", info, header, body, parent, mode);
|
||||
|
||||
// Show messages
|
||||
if (msgInvalidHeaderChecksum)
|
||||
msg(tr("parseFile: invalid header checksum"), index);
|
||||
msg(tr("parseFile: invalid header checksum %1h, should be %2h").hexarg2(fileHeader->IntegrityCheck.Checksum.Header, 2).hexarg2(calculatedHeader, 2), index);
|
||||
if (msgInvalidDataChecksum)
|
||||
msg(tr("parseFile: invalid data checksum"), index);
|
||||
msg(tr("parseFile: invalid data checksum %1h, should be %2h").hexarg2(fileHeader->IntegrityCheck.Checksum.File, 2).hexarg2(calculatedData, 2), index);
|
||||
if (msgInvalidTailValue)
|
||||
msg(tr("parseFile: invalid tail value"), index);
|
||||
msg(tr("parseFile: invalid tail value %1h").hexarg(*(UINT16*)tail.data()), index);
|
||||
if (msgInvalidType)
|
||||
msg(tr("parseFile: unknown file type %1h").arg(fileHeader->Type, 2), index);
|
||||
|
||||
@ -1382,8 +1384,15 @@ UINT8 FfsEngine::getSectionSize(const QByteArray & file, const UINT32 sectionOff
|
||||
{
|
||||
if ((UINT32)file.size() < sectionOffset + sizeof(EFI_COMMON_SECTION_HEADER))
|
||||
return ERR_INVALID_FILE;
|
||||
|
||||
const EFI_COMMON_SECTION_HEADER* sectionHeader = (const EFI_COMMON_SECTION_HEADER*)(file.constData() + sectionOffset);
|
||||
sectionSize = uint24ToUint32(sectionHeader->Size);
|
||||
// This may introduce a very rare error with a non-extended section of size equal to 0xFFFFFF
|
||||
if (sectionSize != 0xFFFFFF)
|
||||
return ERR_SUCCESS;
|
||||
|
||||
const EFI_COMMON_SECTION_HEADER2* sectionHeader2 = (const EFI_COMMON_SECTION_HEADER2*)(file.constData() + sectionOffset);
|
||||
sectionSize = sectionHeader2->ExtendedSize;
|
||||
return ERR_SUCCESS;
|
||||
}
|
||||
|
||||
@ -1670,7 +1679,7 @@ UINT8 FfsEngine::parseSection(const QByteArray & section, QModelIndex & index, c
|
||||
}
|
||||
}
|
||||
else if (certificateHeader->CertificateType == WIN_CERT_TYPE_PKCS_SIGNED_DATA) {
|
||||
info += tr("\nSignature type: PCKS7");
|
||||
info += tr("\nSignature type: PKCS7");
|
||||
// TODO: show signature info in Information panel
|
||||
}
|
||||
else {
|
||||
@ -2219,28 +2228,51 @@ UINT8 FfsEngine::create(const QModelIndex & index, const UINT8 type, const QByte
|
||||
if (header.size() != sizeof(EFI_FFS_FILE_HEADER))
|
||||
return ERR_INVALID_FILE;
|
||||
|
||||
QByteArray newHeader = header;
|
||||
QByteArray newBody = body;
|
||||
EFI_FFS_FILE_HEADER* fileHeader = (EFI_FFS_FILE_HEADER*)newHeader.data();
|
||||
QByteArray newObject = header + body;
|
||||
EFI_FFS_FILE_HEADER* fileHeader = (EFI_FFS_FILE_HEADER*)newObject.data();
|
||||
|
||||
// Determine correct file header size
|
||||
bool largeFile = false;
|
||||
UINT32 headerSize = sizeof(EFI_FFS_FILE_HEADER);
|
||||
if (revision == 2 && (fileHeader->Attributes & FFS_ATTRIB_LARGE_FILE)) {
|
||||
largeFile = true;
|
||||
headerSize = sizeof(EFI_FFS_FILE_HEADER2);
|
||||
}
|
||||
|
||||
QByteArray newHeader = newObject.left(headerSize);
|
||||
QByteArray newBody = newObject.mid(headerSize);
|
||||
|
||||
// Check if the file has a tail
|
||||
UINT8 tailSize = fileHeader->Attributes & FFS_ATTRIB_TAIL_PRESENT ? sizeof(UINT16) : 0;
|
||||
UINT8 tailSize = (revision == 1 && (fileHeader->Attributes & FFS_ATTRIB_TAIL_PRESENT)) ? sizeof(UINT16) : 0;
|
||||
if (tailSize) {
|
||||
// Remove the tail, it will then be added back for revision 1 volumes
|
||||
newBody = newBody.left(newBody.size() - tailSize);
|
||||
// Remove the attribute for rev2+ volumes
|
||||
if (revision != 1) {
|
||||
fileHeader->Attributes &= ~(FFS_ATTRIB_TAIL_PRESENT);
|
||||
}
|
||||
}
|
||||
|
||||
// Correct file size
|
||||
uint32ToUint24(sizeof(EFI_FFS_FILE_HEADER) + newBody.size() + tailSize, fileHeader->Size);
|
||||
if (!largeFile) {
|
||||
if (newBody.size() >= 0xFFFFFF) {
|
||||
return ERR_INVALID_FILE;
|
||||
}
|
||||
|
||||
uint32ToUint24(headerSize + newBody.size() + tailSize, fileHeader->Size);
|
||||
}
|
||||
else {
|
||||
uint32ToUint24(0xFFFFFF, fileHeader->Size);
|
||||
EFI_FFS_FILE_HEADER2* fileHeader2 = (EFI_FFS_FILE_HEADER2*)newHeader.data();
|
||||
fileHeader2->ExtendedSize = headerSize + newBody.size() + tailSize;
|
||||
}
|
||||
|
||||
// Set file state
|
||||
UINT8 state = EFI_FILE_DATA_VALID | EFI_FILE_HEADER_VALID | EFI_FILE_HEADER_CONSTRUCTION;
|
||||
if (erasePolarity)
|
||||
state = ~state;
|
||||
fileHeader->State = state;
|
||||
|
||||
// Recalculate header checksum
|
||||
fileHeader->IntegrityCheck.Checksum.Header = 0;
|
||||
fileHeader->IntegrityCheck.Checksum.File = 0;
|
||||
fileHeader->IntegrityCheck.Checksum.Header = calculateChecksum8((const UINT8*)fileHeader, sizeof(EFI_FFS_FILE_HEADER) - 1);
|
||||
fileHeader->IntegrityCheck.Checksum.Header = 0x100 - (calculateSum8((const UINT8*)newHeader.constData(), headerSize) - fileHeader->State);
|
||||
|
||||
// Recalculate data checksum, if needed
|
||||
if (fileHeader->Attributes & FFS_ATTRIB_CHECKSUM)
|
||||
@ -2254,23 +2286,17 @@ UINT8 FfsEngine::create(const QModelIndex & index, const UINT8 type, const QByte
|
||||
created.append(newBody);
|
||||
|
||||
// Append tail, if needed
|
||||
if (revision ==1 && tailSize) {
|
||||
if (revision == 1 && tailSize) {
|
||||
UINT8 ht = ~fileHeader->IntegrityCheck.Checksum.Header;
|
||||
UINT8 ft = ~fileHeader->IntegrityCheck.Checksum.File;
|
||||
created.append(ht).append(ft);
|
||||
}
|
||||
|
||||
// Set file state
|
||||
UINT8 state = EFI_FILE_DATA_VALID | EFI_FILE_HEADER_VALID | EFI_FILE_HEADER_CONSTRUCTION;
|
||||
if (erasePolarity)
|
||||
state = ~state;
|
||||
fileHeader->State = state;
|
||||
|
||||
// Prepend header
|
||||
created.prepend(newHeader);
|
||||
|
||||
// Parse file
|
||||
result = parseFile(created, fileIndex, erasePolarity ? ERASE_POLARITY_TRUE : ERASE_POLARITY_FALSE, index, mode);
|
||||
result = parseFile(created, fileIndex, revision, erasePolarity ? ERASE_POLARITY_TRUE : ERASE_POLARITY_FALSE, index, mode);
|
||||
if (result && result != ERR_VOLUMES_NOT_FOUND && result != ERR_INVALID_VOLUME)
|
||||
return result;
|
||||
|
||||
@ -2428,17 +2454,20 @@ UINT8 FfsEngine::insert(const QModelIndex & index, const QByteArray & object, co
|
||||
}
|
||||
else if (model->type(parent) == Types::File) {
|
||||
type = Types::Section;
|
||||
const EFI_COMMON_SECTION_HEADER* commonHeader = (EFI_COMMON_SECTION_HEADER*)object.constData();
|
||||
const EFI_COMMON_SECTION_HEADER* commonHeader = (const EFI_COMMON_SECTION_HEADER*)object.constData();
|
||||
headerSize = sizeOfSectionHeader(commonHeader);
|
||||
}
|
||||
else if (model->type(parent) == Types::Section) {
|
||||
type = Types::Section;
|
||||
const EFI_COMMON_SECTION_HEADER* commonHeader = (EFI_COMMON_SECTION_HEADER*)object.constData();
|
||||
const EFI_COMMON_SECTION_HEADER* commonHeader = (const EFI_COMMON_SECTION_HEADER*)object.constData();
|
||||
headerSize = sizeOfSectionHeader(commonHeader);
|
||||
}
|
||||
else
|
||||
return ERR_NOT_IMPLEMENTED;
|
||||
|
||||
if ((UINT32)object.size() < headerSize)
|
||||
return ERR_BUFFER_TOO_SMALL;
|
||||
|
||||
return create(index, type, object.left(headerSize), object.right(object.size() - headerSize), mode, Actions::Insert);
|
||||
}
|
||||
|
||||
@ -2873,6 +2902,9 @@ UINT8 FfsEngine::constructPadFile(const QByteArray &guid, const UINT32 size, con
|
||||
if (size < sizeof(EFI_FFS_FILE_HEADER) || erasePolarity == ERASE_POLARITY_UNKNOWN)
|
||||
return ERR_INVALID_PARAMETER;
|
||||
|
||||
if (size >= 0xFFFFFF) // TODO: large file support
|
||||
return ERR_INVALID_PARAMETER;
|
||||
|
||||
pad = QByteArray(size - guid.size(), erasePolarity == ERASE_POLARITY_TRUE ? '\xFF' : '\x00');
|
||||
pad.prepend(guid);
|
||||
EFI_FFS_FILE_HEADER* header = (EFI_FFS_FILE_HEADER*)pad.data();
|
||||
@ -3265,6 +3297,7 @@ UINT8 FfsEngine::reconstructVolume(const QModelIndex & index, QByteArray & recon
|
||||
model->subtype(index.child(i, 0)) == EFI_FV_FILETYPE_COMBINED_PEIM_DRIVER)){
|
||||
QModelIndex peiFile = index.child(i, 0);
|
||||
UINT32 sectionOffset = sizeof(EFI_FFS_FILE_HEADER);
|
||||
// BUGBUG: this parsing is bad and doesn't support large files, but it needs to be performed only for very old images with uncompressed DXE volumes, so whatever
|
||||
// Search for PE32 or TE section
|
||||
for (int j = 0; j < model->rowCount(peiFile); j++) {
|
||||
if (model->subtype(peiFile.child(j, 0)) == EFI_SECTION_PE32 ||
|
||||
@ -3330,6 +3363,9 @@ UINT8 FfsEngine::reconstructVolume(const QModelIndex & index, QByteArray & recon
|
||||
continue;
|
||||
|
||||
EFI_FFS_FILE_HEADER* fileHeader = (EFI_FFS_FILE_HEADER*)file.data();
|
||||
UINT32 fileHeaderSize = sizeof(EFI_FFS_FILE_HEADER);
|
||||
if (volumeHeader->Revision > 1 && (fileHeader->Attributes & FFS_ATTRIB_LARGE_FILE))
|
||||
fileHeaderSize = sizeof(EFI_FFS_FILE_HEADER2);
|
||||
|
||||
// Pad file
|
||||
if (fileHeader->Type == EFI_FV_FILETYPE_PAD) {
|
||||
@ -3357,8 +3393,8 @@ UINT8 FfsEngine::reconstructVolume(const QModelIndex & index, QByteArray & recon
|
||||
UINT8 alignmentPower;
|
||||
UINT32 alignmentBase;
|
||||
alignmentPower = ffsAlignmentTable[(fileHeader->Attributes & FFS_ATTRIB_DATA_ALIGNMENT) >> 3];
|
||||
alignment = (UINT32)pow(2.0, alignmentPower);
|
||||
alignmentBase = header.size() + offset + sizeof(EFI_FFS_FILE_HEADER);
|
||||
alignment = (UINT32)(1UL <<alignmentPower);
|
||||
alignmentBase = header.size() + offset + fileHeaderSize;
|
||||
if (alignmentBase % alignment) {
|
||||
// File will be unaligned if added as is, so we must add pad file before it
|
||||
// Determine pad file size
|
||||
@ -3577,7 +3613,7 @@ UINT8 FfsEngine::reconstructFile(const QModelIndex& index, const UINT8 revision,
|
||||
model->action(index) == Actions::Rebuild) {
|
||||
QByteArray header = model->header(index);
|
||||
EFI_FFS_FILE_HEADER* fileHeader = (EFI_FFS_FILE_HEADER*)header.data();
|
||||
|
||||
|
||||
// Check erase polarity
|
||||
if (erasePolarity == ERASE_POLARITY_UNKNOWN) {
|
||||
msg(tr("reconstructFile: unknown erase polarity"), index);
|
||||
@ -3635,6 +3671,10 @@ UINT8 FfsEngine::reconstructFile(const QModelIndex& index, const UINT8 revision,
|
||||
// File contains sections
|
||||
else {
|
||||
UINT32 offset = 0;
|
||||
UINT32 headerSize = sizeof(EFI_FFS_FILE_HEADER);
|
||||
if (revision > 1 && (fileHeader->Attributes & FFS_ATTRIB_LARGE_FILE)) {
|
||||
headerSize = sizeof(EFI_FFS_FILE_HEADER2);
|
||||
}
|
||||
|
||||
for (int i = 0; i < model->rowCount(index); i++) {
|
||||
// Align to 4 byte boundary
|
||||
@ -3646,7 +3686,7 @@ UINT8 FfsEngine::reconstructFile(const QModelIndex& index, const UINT8 revision,
|
||||
}
|
||||
|
||||
// Calculate section base
|
||||
UINT32 sectionBase = base ? base + sizeof(EFI_FFS_FILE_HEADER) + offset : 0;
|
||||
UINT32 sectionBase = base ? base + headerSize + offset : 0;
|
||||
|
||||
// Reconstruct section
|
||||
QByteArray section;
|
||||
@ -3668,13 +3708,22 @@ UINT8 FfsEngine::reconstructFile(const QModelIndex& index, const UINT8 revision,
|
||||
|
||||
// Correct file size
|
||||
UINT8 tailSize = (revision == 1 && (fileHeader->Attributes & FFS_ATTRIB_TAIL_PRESENT)) ? sizeof(UINT16) : 0;
|
||||
|
||||
uint32ToUint24(sizeof(EFI_FFS_FILE_HEADER) + reconstructed.size() + tailSize, fileHeader->Size);
|
||||
if (revision > 1 && (fileHeader->Attributes & FFS_ATTRIB_LARGE_FILE)) {
|
||||
uint32ToUint24(EFI_SECTION2_IS_USED, fileHeader->Size);
|
||||
EFI_FFS_FILE_HEADER2* fileHeader2 = (EFI_FFS_FILE_HEADER2*) fileHeader;
|
||||
fileHeader2->ExtendedSize = sizeof(EFI_FFS_FILE_HEADER2) + reconstructed.size() + tailSize;
|
||||
} else {
|
||||
if (sizeof(EFI_FFS_FILE_HEADER) + reconstructed.size() + tailSize > 0xFFFFFF) {
|
||||
msg(tr("reconstructFile: resulting file size is too big"), index);
|
||||
return ERR_INVALID_FILE;
|
||||
}
|
||||
uint32ToUint24(sizeof(EFI_FFS_FILE_HEADER) + reconstructed.size() + tailSize, fileHeader->Size);
|
||||
}
|
||||
|
||||
// Recalculate header checksum
|
||||
fileHeader->IntegrityCheck.Checksum.Header = 0;
|
||||
fileHeader->IntegrityCheck.Checksum.File = 0;
|
||||
fileHeader->IntegrityCheck.Checksum.Header = calculateChecksum8((const UINT8*)fileHeader, sizeof(EFI_FFS_FILE_HEADER) - 1);
|
||||
fileHeader->IntegrityCheck.Checksum.Header = 0x100 - (calculateSum8((const UINT8*)header.constData(), header.size()) - fileHeader->State);
|
||||
}
|
||||
// Use current file body
|
||||
else
|
||||
@ -3690,7 +3739,7 @@ UINT8 FfsEngine::reconstructFile(const QModelIndex& index, const UINT8 revision,
|
||||
fileHeader->IntegrityCheck.Checksum.File = FFS_FIXED_CHECKSUM2;
|
||||
|
||||
// Append tail, if needed
|
||||
if (fileHeader->Attributes & FFS_ATTRIB_TAIL_PRESENT) {
|
||||
if (revision == 1 && fileHeader->Attributes & FFS_ATTRIB_TAIL_PRESENT) {
|
||||
UINT8 ht = ~fileHeader->IntegrityCheck.Checksum.Header;
|
||||
UINT8 ft = ~fileHeader->IntegrityCheck.Checksum.File;
|
||||
reconstructed.append(ht).append(ft);
|
||||
@ -3732,6 +3781,10 @@ UINT8 FfsEngine::reconstructSection(const QModelIndex& index, const UINT32 base,
|
||||
model->action(index) == Actions::Rebase) {
|
||||
QByteArray header = model->header(index);
|
||||
EFI_COMMON_SECTION_HEADER* commonHeader = (EFI_COMMON_SECTION_HEADER*)header.data();
|
||||
bool extended = false;
|
||||
if(uint24ToUint32(commonHeader->Size) == 0xFFFFFF) {
|
||||
extended = true;
|
||||
}
|
||||
|
||||
// Reconstruct section with children
|
||||
if (model->rowCount(index)) {
|
||||
@ -3832,7 +3885,13 @@ UINT8 FfsEngine::reconstructSection(const QModelIndex& index, const UINT32 base,
|
||||
}
|
||||
|
||||
// Correct section size
|
||||
uint32ToUint24(header.size() + reconstructed.size(), commonHeader->Size);
|
||||
if (extended) {
|
||||
EFI_COMMON_SECTION_HEADER2 * extHeader = (EFI_COMMON_SECTION_HEADER2*) commonHeader;
|
||||
extHeader->ExtendedSize = header.size() + reconstructed.size();
|
||||
uint32ToUint24(0xFFFFFF, commonHeader->Size);
|
||||
} else {
|
||||
uint32ToUint24(header.size() + reconstructed.size(), commonHeader->Size);
|
||||
}
|
||||
}
|
||||
// Leaf section
|
||||
else
|
||||
|
@ -70,7 +70,7 @@ public:
|
||||
UINT8 parseEcRegion(const QByteArray & ec, QModelIndex & index, const QModelIndex & parent, const UINT8 mode = CREATE_MODE_APPEND);
|
||||
UINT8 parseBios(const QByteArray & bios, const QModelIndex & parent = QModelIndex());
|
||||
UINT8 parseVolume(const QByteArray & volume, QModelIndex & index, const QModelIndex & parent = QModelIndex(), const UINT8 mode = CREATE_MODE_APPEND);
|
||||
UINT8 parseFile(const QByteArray & file, QModelIndex & index, const UINT8 erasePolarity = ERASE_POLARITY_UNKNOWN, const QModelIndex & parent = QModelIndex(), const UINT8 mode = CREATE_MODE_APPEND);
|
||||
UINT8 parseFile(const QByteArray & file, QModelIndex & index, const UINT8 revision = 2, const UINT8 erasePolarity = ERASE_POLARITY_UNKNOWN, const QModelIndex & parent = QModelIndex(), const UINT8 mode = CREATE_MODE_APPEND);
|
||||
UINT8 parseSections(const QByteArray & body, const QModelIndex & parent = QModelIndex());
|
||||
UINT8 parseSection(const QByteArray & section, QModelIndex & index, const QModelIndex & parent = QModelIndex(), const UINT8 mode = CREATE_MODE_APPEND);
|
||||
|
||||
@ -117,7 +117,6 @@ private:
|
||||
UINT8 parseDepexSection(const QByteArray & body, QString & parsed);
|
||||
UINT8 findNextVolume(const QByteArray & bios, const UINT32 volumeOffset, UINT32 & nextVolumeOffset);
|
||||
UINT8 getVolumeSize(const QByteArray & bios, const UINT32 volumeOffset, UINT32 & volumeSize, UINT32 & bmVolumeSize);
|
||||
UINT8 getFileSize(const QByteArray & volume, const UINT32 fileOffset, UINT32 & fileSize);
|
||||
UINT8 getSectionSize(const QByteArray & file, const UINT32 sectionOffset, UINT32 & sectionSize);
|
||||
|
||||
// Reconstruction helpers
|
||||
|
@ -17,7 +17,7 @@
|
||||
UEFITool::UEFITool(QWidget *parent) :
|
||||
QMainWindow(parent),
|
||||
ui(new Ui::UEFITool),
|
||||
version(tr("0.21.5"))
|
||||
version(tr("0.22.0"))
|
||||
{
|
||||
clipboard = QApplication::clipboard();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user