mirror of
https://github.com/WerWolv/ImHex-Patterns.git
synced 2026-03-31 13:25:58 -05:00
Compare commits
9 Commits
ImHex-v1.2
...
ImHex-v1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
575e4d5381 | ||
|
|
6b0fad199e | ||
|
|
ece86f1124 | ||
|
|
ce2b4d60ca | ||
|
|
7c88439681 | ||
|
|
8d3c94be8f | ||
|
|
0b15299980 | ||
|
|
eda13b2518 | ||
|
|
aa6c90fa5b |
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -1,10 +1,2 @@
|
||||
*.pat linguist-language=Rust
|
||||
*.hexpat linguist-language=Rust
|
||||
constants/* text eol=lf
|
||||
encodings/* text eol=lf
|
||||
includes/* text eol=lf
|
||||
magic/* text eol=lf
|
||||
patterns/* text eol=lf
|
||||
structs/* text eol=lf
|
||||
tips/* text eol=lf
|
||||
yara/* text eol=lf
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
- name: 🧪 Perform Unit Tests
|
||||
run: |
|
||||
cd tests/build
|
||||
ctest
|
||||
ctest --output-on-failure
|
||||
- name: 📎 Validate JSON Files
|
||||
run: |
|
||||
cd constants
|
||||
|
||||
@@ -32,6 +32,7 @@ Hex patterns, include patterns and magic files for the use with the ImHex Hex Ed
|
||||
| VDF | | `patterns/vdf.hexpat` | Binary Value Data Format (.vdf) files |
|
||||
| IP | | `patterns/ip.hexpat` | Ethernet II Frames (IP Packets) |
|
||||
| UF2 | | `patterns/uf2.hexpat` | [USB Flashing Format](https://github.com/microsoft/uf2) |
|
||||
| File System | | `patterns/fs.hexpat` | Drive File System |
|
||||
|
||||
### Scripts
|
||||
|
||||
|
||||
33
includes/type/leb128.pat
Normal file
33
includes/type/leb128.pat
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <std/io.pat>
|
||||
#include <std/mem.pat>
|
||||
|
||||
namespace type {
|
||||
|
||||
struct LEB128 {
|
||||
u8 array[while($ == addressof(this) || std::mem::read_unsigned($-1, 1) & 0x80 != 0)] [[hidden]];
|
||||
} [[sealed, format("type::impl::format_leb128"), transform("type::impl::transform_leb128")]];
|
||||
|
||||
namespace impl {
|
||||
|
||||
fn transform_leb128_array(auto array) {
|
||||
u128 res = array[0] & 0x7f;
|
||||
for(u8 i = 1, array[i-1] & 0x80 != 0, i+=1) {
|
||||
res |= u64(array[i] & 0x7f) << 7 * i;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
fn format_leb128(auto leb128) {
|
||||
u128 res = type::impl::transform_leb128_array(leb128.array);
|
||||
return std::format("{} ({:#x})", res, res);
|
||||
};
|
||||
|
||||
fn transform_leb128(auto leb128) {
|
||||
return type::impl::transform_leb128_array(leb128.array);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ namespace program {
|
||||
u64 memory_size;
|
||||
u64 alignment;
|
||||
}
|
||||
} [[static]];
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ namespace section {
|
||||
u64 address_alignment;
|
||||
u64 entry_size;
|
||||
}
|
||||
} [[static]];
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
168
patterns/fs.hexpat
Normal file
168
patterns/fs.hexpat
Normal file
@@ -0,0 +1,168 @@
|
||||
#include <std/io.pat>
|
||||
|
||||
struct DiskTimeStamp {
|
||||
u8 seconds, minutes, hours;
|
||||
};
|
||||
|
||||
enum DiskProtection : u16 {
|
||||
None = 0x0000,
|
||||
CopyProtected = 0x5A5A
|
||||
};
|
||||
|
||||
bitfield CHS {
|
||||
h : 8;
|
||||
s : 6;
|
||||
c : 10;
|
||||
} [[right_to_left, format("chs_formatter")]];
|
||||
|
||||
fn chs_formatter(CHS chs) {
|
||||
return std::format("({:X}, {:X}, {:X}) | 0x{:X}", chs.c, chs.h, chs.s, (chs.c * 16 + chs.h) * 63 + (chs.s - 1));
|
||||
};
|
||||
|
||||
enum PartitionStatus : u8 {
|
||||
None = 0x00,
|
||||
Active = 0x80
|
||||
};
|
||||
|
||||
enum PartitionType : u8 {
|
||||
EmptyPartitionEntry = 0x00,
|
||||
FAT32_CHS = 0x0B,
|
||||
FAT32_LBA = 0x0C
|
||||
};
|
||||
|
||||
namespace fat32 {
|
||||
|
||||
u64 bytesPerCluster;
|
||||
|
||||
struct FSInfo {
|
||||
u32 leadSignature;
|
||||
padding[480];
|
||||
u32 structSignature;
|
||||
u32 freeClusterCount;
|
||||
u32 nextFreeCluster;
|
||||
padding[12];
|
||||
u32 trailSignature;
|
||||
};
|
||||
|
||||
bitfield SequenceNumber {
|
||||
padding : 1;
|
||||
lastLogical : 1;
|
||||
padding : 1;
|
||||
number : 5;
|
||||
} [[left_to_right]];
|
||||
|
||||
enum EntryStatus : u8 {
|
||||
Regular = 0x00,
|
||||
DotEntry = 0x2E,
|
||||
DeletedEntry = 0xE5
|
||||
};
|
||||
|
||||
union EntryStatusOrSequenceNumber {
|
||||
EntryStatus entryStatus;
|
||||
SequenceNumber sequenceNumber;
|
||||
};
|
||||
|
||||
bitfield Attributes {
|
||||
readOnly : 1;
|
||||
hidden : 1;
|
||||
systemFile : 1;
|
||||
volumeLabel : 1;
|
||||
subdirectory : 1;
|
||||
archive : 1;
|
||||
padding : 2;
|
||||
} [[right_to_left]];
|
||||
|
||||
struct DirEntry {
|
||||
char fileName[8];
|
||||
char extension[3];
|
||||
Attributes attributes;
|
||||
u8 reserved[10];
|
||||
u16 time, date;
|
||||
u16 startingCluster;
|
||||
u32 fileSize;
|
||||
|
||||
u8 data[fileSize] @ startingCluster * bytesPerCluster;
|
||||
};
|
||||
|
||||
struct VFATDirEntry {
|
||||
EntryStatusOrSequenceNumber entryStatusOrSequenceNumber;
|
||||
char16 name1[5];
|
||||
Attributes attributes;
|
||||
u8 type;
|
||||
u8 nameChecksum;
|
||||
char16 name2[6];
|
||||
u16 startingCluster;
|
||||
char16 name3[2];
|
||||
|
||||
if (entryStatusOrSequenceNumber.sequenceNumber.number > 1)
|
||||
VFATDirEntry nextLogicalEntry;
|
||||
else
|
||||
DirEntry physicalEntry;
|
||||
};
|
||||
|
||||
struct Partition {
|
||||
u8 jmpCode[3];
|
||||
char oemName[8];
|
||||
u16 bytesPerSector;
|
||||
u8 sectorsPerCluster;
|
||||
u16 reservedAreaSize;
|
||||
u8 numFats;
|
||||
u16 rootEntryCount;
|
||||
u16 numSectors;
|
||||
u8 mediaType;
|
||||
u16 fatSize;
|
||||
u16 sectorsPerTrack;
|
||||
u16 numHeads;
|
||||
u32 numHiddenSectors;
|
||||
u32 numFsSectors;
|
||||
u32 numFatSectors;
|
||||
u16 extFlags;
|
||||
u16 fsVersion;
|
||||
u32 rootCluster;
|
||||
u16 fsInfoSector;
|
||||
u16 backupBootSector;
|
||||
padding[12];
|
||||
u8 driveNumber;
|
||||
padding[1];
|
||||
u8 bootSignature;
|
||||
u32 volumeID;
|
||||
char volumeLabel[11];
|
||||
char fsType[8];
|
||||
u8 bootstrapCode[420];
|
||||
u16 signature;
|
||||
|
||||
bytesPerCluster = (sectorsPerCluster * 1024) * bytesPerSector;
|
||||
|
||||
FSInfo fsInfo @ addressof(this) + fsInfoSector * bytesPerSector;
|
||||
VFATDirEntry rootDirEntry @ addressof(this) + rootCluster * bytesPerCluster;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
struct PartitionEntry {
|
||||
PartitionStatus status;
|
||||
CHS chsFirstSectorAddress;
|
||||
PartitionType type;
|
||||
CHS chsLastSectorAddress;
|
||||
u32 lbaFirstSectorAddress;
|
||||
u32 numSectors;
|
||||
|
||||
if (type == PartitionType::EmptyPartitionEntry)
|
||||
continue;
|
||||
else if (type == PartitionType::FAT32_CHS || type == PartitionType::FAT32_LBA)
|
||||
fat32::Partition partition @ lbaFirstSectorAddress * 512;
|
||||
};
|
||||
|
||||
struct MasterBootRecord {
|
||||
u8 bootstrapCodeArea1[218];
|
||||
padding[2];
|
||||
u8 originalPhysicalDrive;
|
||||
DiskTimeStamp diskTimeStamp;
|
||||
u8 bootstrapCodeArea2[216];
|
||||
u32 diskSignature;
|
||||
DiskProtection diskProtection;
|
||||
PartitionEntry partitionEntries[4];
|
||||
u16 bootSignature;
|
||||
};
|
||||
|
||||
MasterBootRecord mbr @ 0x00;
|
||||
@@ -1,4 +1,10 @@
|
||||
#pragma MIME application/x-dosexec
|
||||
#pragma MIME application/x-msdownload
|
||||
#pragma pattern_limit 100000
|
||||
#pragma array_limit 10000
|
||||
#include <std/mem.pat>
|
||||
#include <std/string.pat>
|
||||
#include <std/ctype.pat>
|
||||
#include <std/io.pat>
|
||||
|
||||
enum MachineType : u16 {
|
||||
Unknown = 0x00,
|
||||
@@ -7,9 +13,13 @@ enum MachineType : u16 {
|
||||
ARM = 0x1C0,
|
||||
ARM64 = 0xAA64,
|
||||
ARMNT = 0x1C4,
|
||||
DECAlphaAXP = 0x183,
|
||||
EBC = 0xEBC,
|
||||
I386 = 0x14C,
|
||||
I860 = 0x14D,
|
||||
IA64 = 0x200,
|
||||
LOONGARCH32 = 0x6232,
|
||||
LOONGARCH64 = 0x6264,
|
||||
M32R = 0x9041,
|
||||
MIPS16 = 0x266,
|
||||
MIPSFPU = 0x366,
|
||||
@@ -28,32 +38,99 @@ enum MachineType : u16 {
|
||||
WCEMIPSV2 = 0x169
|
||||
};
|
||||
|
||||
enum PEFormat : u16 {
|
||||
PE32 = 0x10B,
|
||||
PE32Plus = 0x20B
|
||||
};
|
||||
|
||||
enum SubsystemType : u16 {
|
||||
Unknown = 0x00,
|
||||
Native = 0x01,
|
||||
WindowsGUI = 0x02,
|
||||
WindowsCUI = 0x03,
|
||||
OS2CUI = 0x05,
|
||||
POSIXCUI = 0x07,
|
||||
Windows9xNative = 0x08,
|
||||
WindowsCEGUI = 0x09,
|
||||
EFIApplication = 0x0A,
|
||||
EFIBootServiceDriver = 0x0B,
|
||||
EFIRuntimeDriver = 0x0C,
|
||||
EFIROM = 0x0D,
|
||||
Xbox = 0x0E,
|
||||
WindowsBootApplication = 0x10
|
||||
};
|
||||
|
||||
bitfield Characteristics {
|
||||
stripped : 1;
|
||||
relocationsStripped : 1;
|
||||
executableImage : 1;
|
||||
lineNumsStripped : 1;
|
||||
localSymsStripped : 1;
|
||||
lineNumbersStripped : 1;
|
||||
localSymbolsStripped : 1;
|
||||
aggressiveWsTrim : 1;
|
||||
largeAddressAware : 1;
|
||||
reserved : 1;
|
||||
bytesReversedLo : 1;
|
||||
is32BitMachine : 1;
|
||||
debugStripped : 1;
|
||||
debugInfoStripped : 1;
|
||||
removableRunFromSwap : 1;
|
||||
netRunFromSwap : 1;
|
||||
system : 1;
|
||||
dll : 1;
|
||||
upSystemOnly : 1;
|
||||
uniprocessorMachineOnly : 1;
|
||||
bytesReversedHi : 1;
|
||||
};
|
||||
} [[right_to_left]];
|
||||
|
||||
bitfield DLLCharacteristics {
|
||||
reserved : 4;
|
||||
padding : 1;
|
||||
highEntropyVA : 1;
|
||||
dynamicBase : 1;
|
||||
forceIntegrity : 1;
|
||||
nxCompatible : 1;
|
||||
noIsolation : 1;
|
||||
noSEH : 1;
|
||||
doNotBind : 1;
|
||||
appContainer : 1;
|
||||
wdmDriver : 1;
|
||||
cfGuard : 1;
|
||||
terminalServerAware : 1;
|
||||
} [[right_to_left]];
|
||||
|
||||
bitfield SectionFlags {
|
||||
reserved : 3;
|
||||
doNotPad : 1;
|
||||
reserved : 1;
|
||||
containsCode : 1;
|
||||
containsInitializedData : 1;
|
||||
containsUninitializedData : 1;
|
||||
linkOther : 1;
|
||||
comments : 1;
|
||||
reserved : 1;
|
||||
remove : 1;
|
||||
comdat : 1;
|
||||
padding : 2;
|
||||
globalPointerRelocation : 1;
|
||||
purgeable : 1;
|
||||
is16Bit : 1;
|
||||
locked : 1;
|
||||
preloaded : 1;
|
||||
dataAlignment : 4;
|
||||
extendedRelocations : 1;
|
||||
discardable : 1;
|
||||
notCacheable : 1;
|
||||
notPageable : 1;
|
||||
shared : 1;
|
||||
executed : 1;
|
||||
read : 1;
|
||||
writtenTo : 1;
|
||||
} [[right_to_left]];
|
||||
|
||||
struct DataDirectory {
|
||||
u32 virtualAddress;
|
||||
u32 size;
|
||||
};
|
||||
|
||||
struct OptionalHeader32 {
|
||||
u16 magic;
|
||||
struct OptionalHeader {
|
||||
PEFormat magic;
|
||||
u8 majorLinkerVersion;
|
||||
u8 minorLinkerVersion;
|
||||
u32 sizeOfCode;
|
||||
@@ -61,8 +138,13 @@ struct OptionalHeader32 {
|
||||
u32 sizeOfUninitializedData;
|
||||
u32 addressOfEntryPoint;
|
||||
u32 baseOfCode;
|
||||
u32 baseOfData;
|
||||
u32 imageBase;
|
||||
if (magic == PEFormat::PE32) {
|
||||
u32 baseOfData;
|
||||
u32 imageBase;
|
||||
}
|
||||
else if (magic == PEFormat::PE32Plus) {
|
||||
u64 imageBase;
|
||||
}
|
||||
u32 sectionAlignment;
|
||||
u32 fileAlignment;
|
||||
u16 majorOperatingSystemVersion;
|
||||
@@ -75,87 +157,90 @@ struct OptionalHeader32 {
|
||||
u32 sizeOfImage;
|
||||
u32 sizeOfHeaders;
|
||||
u32 checksum;
|
||||
u16 subsystem;
|
||||
u16 dllCharacteristics;
|
||||
u32 sizeOfStackReserve;
|
||||
u32 sizeOfStackCommit;
|
||||
u32 sizeOfHeapReserve;
|
||||
u32 sizeOfHeapCommit;
|
||||
SubsystemType subsystem;
|
||||
DLLCharacteristics dllCharacteristics;
|
||||
if (magic == PEFormat::PE32) {
|
||||
u32 sizeOfStackReserve;
|
||||
u32 sizeOfStackCommit;
|
||||
u32 sizeOfHeapReserve;
|
||||
u32 sizeOfHeapCommit;
|
||||
}
|
||||
else if (magic == PEFormat::PE32Plus) {
|
||||
u64 sizeOfStackReserve;
|
||||
u64 sizeOfStackCommit;
|
||||
u64 sizeOfHeapReserve;
|
||||
u64 sizeOfHeapCommit;
|
||||
}
|
||||
u32 loaderFlags;
|
||||
u32 numberOfRvaAndSizes;
|
||||
DataDirectory directories[numberOfRvaAndSizes];
|
||||
u32 numberOfRVAsAndSizes;
|
||||
DataDirectory directories[numberOfRVAsAndSizes];
|
||||
};
|
||||
|
||||
struct OptionalHeader64 {
|
||||
u16 magic;
|
||||
u8 majorLinkerVersion;
|
||||
u8 minorLinkerVersion;
|
||||
u32 sizeOfCode;
|
||||
u32 sizeOfInitializedData;
|
||||
u32 sizeOfUninitializedData;
|
||||
u32 addressOfEntryPoint;
|
||||
u32 baseOfCode;
|
||||
u64 imageBase;
|
||||
u32 sectionAlignment;
|
||||
u32 fileAlignment;
|
||||
u16 majorOperatingSystemVersion;
|
||||
u16 minorOperatingSystemVersion;
|
||||
u16 majorImageVersion;
|
||||
u16 minorImageVersion;
|
||||
u16 majorSubsystemVersion;
|
||||
u16 minorSubSystemVersion;
|
||||
u32 win32VersionValue;
|
||||
u32 sizeOfImage;
|
||||
u32 sizeOfHeaders;
|
||||
u32 checksum;
|
||||
u16 subsystem;
|
||||
u16 dllCharacteristics;
|
||||
u64 sizeOfStackReserve;
|
||||
u64 sizeOfStackCommit;
|
||||
u64 sizeOfHeapReserve;
|
||||
u64 sizeOfHeapCommit;
|
||||
u32 loaderFlags;
|
||||
u32 numberOfRvaAndSizes;
|
||||
DataDirectory directories[numberOfRvaAndSizes];
|
||||
};
|
||||
|
||||
struct COFFHeader {
|
||||
u32 signature;
|
||||
struct COFFHeader {
|
||||
char signature[4];
|
||||
MachineType machine;
|
||||
u16 numberOfSections;
|
||||
u32 timeDateStamp;
|
||||
u32 pointerToSymbolTable;
|
||||
u32 numberOfSymbolTable;
|
||||
u32 numberOfSymbols;
|
||||
u16 sizeOfOptionalHeader;
|
||||
Characteristics characteristics;
|
||||
|
||||
if (machine == MachineType::AMD64) {
|
||||
OptionalHeader64 optionalHeader;
|
||||
} else {
|
||||
OptionalHeader32 optionalHeader;
|
||||
|
||||
if (sizeOfOptionalHeader > 0x00) {
|
||||
OptionalHeader optionalHeader;
|
||||
}
|
||||
};
|
||||
|
||||
struct DOSHeader {
|
||||
u16 signature;
|
||||
u8 header[0x3A];
|
||||
COFFHeader *coffHeaderPointer : u32;
|
||||
char signature[2];
|
||||
u16 lastPageSize;
|
||||
u16 numberOfPages;
|
||||
u16 relocations;
|
||||
u16 headerSizeInParagraphs;
|
||||
u16 minimumAllocatedParagraphs;
|
||||
u16 maximumAllocatedParagraphs;
|
||||
u16 initialSSValue;
|
||||
u16 initialRelativeSPValue;
|
||||
u16 checksum;
|
||||
u16 initialRelativeIPValue;
|
||||
u16 initialCSValue;
|
||||
u16 relocationsTablePointer;
|
||||
u16 overlayNumber;
|
||||
u16 reservedWords[4];
|
||||
u16 oemIdentifier;
|
||||
u16 oemInformation;
|
||||
u16 reservedWords[10];
|
||||
u32 coffHeaderPointer;
|
||||
};
|
||||
|
||||
fn isdosmessage(char c) {
|
||||
return std::ctype::isalnum(c) || c == '.' || c == ' ';
|
||||
};
|
||||
|
||||
struct DOSStub {
|
||||
u8 code[14];
|
||||
s8 message[0x27];
|
||||
u8 data[11];
|
||||
u8 code[while(std::mem::read_string($, 4) != "This")];
|
||||
char message[while(isdosmessage(std::mem::read_unsigned($, 1)))];
|
||||
char data[while(std::mem::read_unsigned($, 1) != 0x00)];
|
||||
};
|
||||
|
||||
union SectionMisc {
|
||||
u32 physicalAddress;
|
||||
u32 virtualSize;
|
||||
struct PEHeader {
|
||||
DOSHeader dosHeader;
|
||||
if (dosHeader.headerSizeInParagraphs * 16 < std::mem::size()) {
|
||||
DOSStub dosStub @ dosHeader.headerSizeInParagraphs * 16;
|
||||
}
|
||||
};
|
||||
|
||||
struct Section {
|
||||
PEHeader peHeader @ 0x00;
|
||||
|
||||
COFFHeader coffHeader @ peHeader.dosHeader.coffHeaderPointer;
|
||||
|
||||
struct SectionHeader {
|
||||
char name[8];
|
||||
SectionMisc misc;
|
||||
if (coffHeader.characteristics.executableImage) {
|
||||
u32 virtualSize;
|
||||
} else {
|
||||
u32 physicalAddress;
|
||||
}
|
||||
u32 virtualAddress;
|
||||
u32 sizeOfRawData;
|
||||
u32 ptrRawData;
|
||||
@@ -163,15 +248,436 @@ struct Section {
|
||||
u32 ptrLineNumbers;
|
||||
u16 numberOfRelocations;
|
||||
u16 numberOfLineNumbers;
|
||||
SectionFlags characteristics;
|
||||
};
|
||||
|
||||
SectionHeader sectionTable[coffHeader.numberOfSections] @ (addressof(coffHeader.characteristics) + 2) + coffHeader.sizeOfOptionalHeader;
|
||||
|
||||
u16 currentSectionIndex;
|
||||
fn updateSectionIndex() {
|
||||
currentSectionIndex = currentSectionIndex + 1;
|
||||
};
|
||||
|
||||
struct NullTerminatedString {
|
||||
char string[while(std::mem::read_unsigned($, 1) != 0x00)];
|
||||
padding[1];
|
||||
} [[inline]];
|
||||
|
||||
// Declarations of sections and variables used by it
|
||||
// Imports + Readonly Data Section
|
||||
struct DirectoryTable {
|
||||
u32 lookupTableRVA;
|
||||
u32 timeDateStamp;
|
||||
u32 forwarderChain;
|
||||
u32 dllNameRVA;
|
||||
u32 addressTableRVA;
|
||||
};
|
||||
|
||||
struct ImportsSection {
|
||||
DirectoryTable directoryTables[while(std::mem::read_unsigned($, 16) != 0x00)];
|
||||
DirectoryTable nullDirectoryTable;
|
||||
};
|
||||
|
||||
struct ReadonlyDataSection {
|
||||
u8 readonlyDataStart[while($ < coffHeader.optionalHeader.directories[1].virtualAddress - (sectionTable[currentSectionIndex].virtualAddress - sectionTable[currentSectionIndex].ptrRawData))];
|
||||
ImportsSection importsSection;
|
||||
u8 readonlyDataEnd[while($ < sectionTable[currentSectionIndex+1].ptrRawData)];
|
||||
};
|
||||
|
||||
// Resource Section
|
||||
struct ResourceDirectoryTable {
|
||||
u32 characteristics;
|
||||
u32 timeDateStamp;
|
||||
u16 majorVersion;
|
||||
u16 minorVersion;
|
||||
u16 nameEntriesAmount;
|
||||
u16 idEntriesAmount;
|
||||
};
|
||||
|
||||
enum ResourceID : u32 {
|
||||
Bitmap = 0x02,
|
||||
Icon = 0x03,
|
||||
Menu = 0x04,
|
||||
Dialog = 0x05,
|
||||
String = 0x06,
|
||||
GroupIcon = 0x0D,
|
||||
Version = 0x10,
|
||||
Manifest = 0x18
|
||||
};
|
||||
|
||||
struct IdDirectoryEntry {
|
||||
ResourceID id;
|
||||
u32 relativeOffsetToData;
|
||||
};
|
||||
|
||||
struct ResourceDirectory {
|
||||
ResourceDirectoryTable resourceDirectoryTable;
|
||||
IdDirectoryEntry idEntries[resourceDirectoryTable.idEntriesAmount];
|
||||
};
|
||||
|
||||
struct ResourceSection {
|
||||
ResourceDirectory rootDirectory;
|
||||
};
|
||||
|
||||
// Exports Section
|
||||
struct ExportDirectoryTable {
|
||||
u32 exportFlags;
|
||||
u32 timeDateStamp;
|
||||
u16 majorVersion;
|
||||
u16 minorVersion;
|
||||
u32 nameRVA;
|
||||
u32 ordinalBase;
|
||||
u32 addressTableEntries;
|
||||
u32 numberOfNamePointers;
|
||||
u32 exportAddressTableRVA;
|
||||
u32 namePointerRVA;
|
||||
u32 ordinalTableRVA;
|
||||
};
|
||||
|
||||
struct ExportAddress {
|
||||
if (std::mem::read_unsigned($, 4) > sectionTable[currentSectionIndex].sizeOfRawData && std::mem::read_unsigned($, 4) < sectionTable[currentSectionIndex].ptrRawData) {
|
||||
u32 exportRVA;
|
||||
}
|
||||
else {
|
||||
u32 forwarderRVA;
|
||||
}
|
||||
};
|
||||
|
||||
struct ExportsSection {
|
||||
ExportDirectoryTable exportDirectoryTable;
|
||||
ExportAddress exportAddressTable[exportDirectoryTable.addressTableEntries];
|
||||
u32 exportNamePointerTable[exportDirectoryTable.numberOfNamePointers];
|
||||
};
|
||||
|
||||
// Exception Section
|
||||
bitfield ExceptionSectionBits {
|
||||
functionLength : 22;
|
||||
instructions32Bit : 1;
|
||||
exceptionHandler : 1;
|
||||
} [[right_to_left]];
|
||||
|
||||
struct FunctionTableEntry {
|
||||
if (coffHeader.machine == MachineType::MIPSFPU) {
|
||||
u32 beginVA;
|
||||
u32 endVA;
|
||||
u32 exceptionHandlerPointer;
|
||||
u32 handlerDataPointer;
|
||||
u32 prologEndVA;
|
||||
} else if (coffHeader.machine == MachineType::ARM || MachineType::ARM64 || MachineType::ARMNT || MachineType::POWERPC || MachineType::POWERPCFP || MachineType::SH3 || MachineType::SH4) {
|
||||
u32 beginVA;
|
||||
u8 prologLength;
|
||||
ExceptionSectionBits miscellaneousBits;
|
||||
} else if (coffHeader.machine == MachineType::AMD64 || MachineType::IA64) {
|
||||
u32 beginRVA;
|
||||
u32 endRVA;
|
||||
u32 unwindInformationRVA;
|
||||
}
|
||||
};
|
||||
|
||||
struct ExceptionSection {
|
||||
FunctionTableEntry functionTableEntries[while($ < sectionTable[currentSectionIndex].ptrRawData + sectionTable[currentSectionIndex].sizeOfRawData)];
|
||||
};
|
||||
|
||||
// TLS Section
|
||||
struct TLSSection {
|
||||
if (coffHeader.optionalHeader.magic == PEFormat::PE32) {
|
||||
u32 rawDataStartVA;
|
||||
u32 rawDataEndVA;
|
||||
u32 indexAddress;
|
||||
u32 callbacksAddress;
|
||||
}
|
||||
else if (coffHeader.optionalHeader.magic == PEFormat::PE32Plus) {
|
||||
u64 rawDataStartVA;
|
||||
u64 rawDataEndVA;
|
||||
u64 indexAddress;
|
||||
u64 callbacksAddress;
|
||||
}
|
||||
u32 zeroFillSize;
|
||||
u32 characteristics;
|
||||
};
|
||||
|
||||
struct PEHeader {
|
||||
DOSHeader dosHeader;
|
||||
DOSStub dosStub;
|
||||
// Relocations Section
|
||||
bitfield RelocationWord {
|
||||
type : 4;
|
||||
offset : 12;
|
||||
};
|
||||
|
||||
PEHeader peHeader @ 0x00;
|
||||
struct BaseRelocationBlock {
|
||||
u32 pageRVA;
|
||||
u32 blockSize;
|
||||
RelocationWord word;
|
||||
};
|
||||
|
||||
Section sectionsTable[peHeader.dosHeader.coffHeaderPointer.numberOfSections]
|
||||
@ addressof(peHeader.dosHeader.coffHeaderPointer) + sizeof(peHeader.dosHeader.coffHeaderPointer);
|
||||
struct BaseRelocationTable {
|
||||
BaseRelocationBlock baseRelocationBlocks[while(std::mem::read_unsigned($, 8) != 0x00)];
|
||||
};
|
||||
|
||||
// General Section things
|
||||
enum I386Relocations : u16 {
|
||||
Absolute = 0x00,
|
||||
Dir16 = 0x01,
|
||||
Rel16 = 0x02,
|
||||
Dir32 = 0x06
|
||||
};
|
||||
|
||||
struct Relocation {
|
||||
u32 virtualAddress;
|
||||
u32 symbolTableIndex;
|
||||
if (coffHeader.machine == MachineType::I386) {
|
||||
I386Relocations type;
|
||||
}
|
||||
else {
|
||||
u16 type;
|
||||
}
|
||||
};
|
||||
|
||||
struct LineNumber {
|
||||
u32 lineNumber @ $ + 4;
|
||||
if (lineNumber > 0x00) {
|
||||
u32 virtualAddress @ $ - 8;
|
||||
} else {
|
||||
u32 symbolTableIndex @ $ - 8;
|
||||
}
|
||||
$ += 8;
|
||||
};
|
||||
|
||||
fn importsSectionExists() {
|
||||
bool returnedValue = false;
|
||||
std::print("Checking which section is .idata for read-only data section");
|
||||
for (u16 i = 0, i < coffHeader.numberOfSections, i = i + 1) {
|
||||
std::print("Checking section " + std::string::to_string(i));
|
||||
if (sectionTable[i].name == ".idata") {
|
||||
std::print("Check successful. Section " + std::string::to_string(i) + " is .idata, so the read-only data section won't have an imports section in it");
|
||||
returnedValue = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!returnedValue) {
|
||||
std::print("Check failed! This means there is no separate imports section");
|
||||
}
|
||||
return returnedValue;
|
||||
};
|
||||
|
||||
struct Section {
|
||||
if (std::string::starts_with(sectionTable[currentSectionIndex].name, ".pdata")) { // Exception section
|
||||
ExceptionSection exceptionSection;
|
||||
}
|
||||
else if (std::string::starts_with(sectionTable[currentSectionIndex].name, ".rdata")) {// Read-only data section
|
||||
if (importsSectionExists() || coffHeader.optionalHeader.directories[1].size == 0) {
|
||||
u8 readonlyDataSection[sectionTable[currentSectionIndex].sizeOfRawData];
|
||||
}
|
||||
else {
|
||||
ReadonlyDataSection readonlyDataSection;
|
||||
}
|
||||
}
|
||||
else if (std::string::starts_with(sectionTable[currentSectionIndex].name, ".idata")) {// Imports section
|
||||
ImportsSection importsSection;
|
||||
}
|
||||
else if (std::string::starts_with(sectionTable[currentSectionIndex].name, ".edata")) {// Exports section
|
||||
ExportsSection exportsSection;
|
||||
}
|
||||
else if (std::string::starts_with(sectionTable[currentSectionIndex].name, ".rsrc")) { // Resource section
|
||||
ResourceSection resourceSection;
|
||||
}
|
||||
else if (std::string::starts_with(sectionTable[currentSectionIndex].name, ".tls")) { // Thread-local storage section
|
||||
TLSSection tlsSection;
|
||||
}
|
||||
else if (std::string::starts_with(sectionTable[currentSectionIndex].name, ".reloc")) {// Thread-local storage section
|
||||
BaseRelocationTable relocationsSection;
|
||||
}
|
||||
else {
|
||||
u8 freeformSection[sectionTable[currentSectionIndex].sizeOfRawData]; // Freeform data section
|
||||
}
|
||||
|
||||
// Additional things
|
||||
//Relocation relocations[sectionTable[currentSectionIndex].numberOfRelocations] @ sectionTable[currentSectionIndex].ptrRelocations;
|
||||
LineNumber lineNumbers[sectionTable[currentSectionIndex].numberOfLineNumbers] @ sectionTable[currentSectionIndex].ptrLineNumbers;
|
||||
|
||||
// Next section
|
||||
if (currentSectionIndex < coffHeader.numberOfSections-1) { // If it's not the last section (to avoid problems this code would have with it)
|
||||
updateSectionIndex(); // Make the current section index the next section's index
|
||||
if (sectionTable[currentSectionIndex].sizeOfRawData > 0) { // If the size of this section is bigger than 0
|
||||
$ = sectionTable[currentSectionIndex].ptrRawData; // Put the current offset at the start of the next section
|
||||
}
|
||||
}
|
||||
} [[inline]];
|
||||
|
||||
Section sections[coffHeader.numberOfSections] @ sectionTable[0].ptrRawData;
|
||||
|
||||
// Symbol & String Tables
|
||||
enum SectionNumberType : s16 {
|
||||
Undefined = 0,
|
||||
Absolute = -1,
|
||||
Debug = -2
|
||||
};
|
||||
|
||||
enum SymbolTypeMSB : u8 {
|
||||
Null = 0x00,
|
||||
Pointer = 0x10,
|
||||
Function = 0x20,
|
||||
Array = 0x30
|
||||
};
|
||||
|
||||
enum SymbolTypeLSB : u8 {
|
||||
Null = 0x00,
|
||||
Void = 0x01,
|
||||
Char = 0x02,
|
||||
Short = 0x03,
|
||||
Integer = 0x04,
|
||||
Long = 0x05,
|
||||
Float = 0x06,
|
||||
Double = 0x07,
|
||||
Struct = 0x08,
|
||||
Union = 0x09,
|
||||
Enum = 0x0A,
|
||||
MemberOfEnum = 0x0B,
|
||||
Byte = 0x0C,
|
||||
Word = 0x0D,
|
||||
UInt = 0x0E,
|
||||
DWord = 0x0F
|
||||
};
|
||||
|
||||
enum StorageClassType : s8 {
|
||||
EndOfFunction = -1,
|
||||
Null = 0,
|
||||
Automatic = 1,
|
||||
External = 2,
|
||||
Static = 3,
|
||||
Register = 4,
|
||||
DefinedExternally = 5,
|
||||
Label = 6,
|
||||
UndefinedLabel = 7,
|
||||
MemberOfStruct = 8,
|
||||
Argument = 9,
|
||||
StructTag = 10,
|
||||
MemberOfUnion = 11,
|
||||
UnionTag = 12,
|
||||
TypeDefinition = 13,
|
||||
UndefinedStatic = 14,
|
||||
EnumTag = 15,
|
||||
MemberOfEnum = 16,
|
||||
RegisterParameter = 17,
|
||||
Bitfield = 18,
|
||||
Block = 100,
|
||||
BlockFunction = 101,
|
||||
EndOfStruct = 102,
|
||||
File = 103,
|
||||
Section = 104,
|
||||
WeakExternal = 105,
|
||||
CLRToken = 107
|
||||
};
|
||||
|
||||
struct SymbolNameAddress {
|
||||
u32 zeroes;
|
||||
u32 offset;
|
||||
};
|
||||
|
||||
struct SymbolName {
|
||||
if (std::mem::read_unsigned($, 4) == 0) {
|
||||
SymbolNameAddress nameAddress;
|
||||
}
|
||||
else {
|
||||
char shortName[8];
|
||||
}
|
||||
};
|
||||
|
||||
struct SymbolType {
|
||||
SymbolTypeMSB msb;
|
||||
SymbolTypeLSB lsb;
|
||||
};
|
||||
|
||||
struct Symbol {
|
||||
SymbolName name;
|
||||
u32 value;
|
||||
SectionNumberType sectionNumber;
|
||||
SymbolType type;
|
||||
StorageClassType storageClass;
|
||||
u8 numberOfAuxSymbols;
|
||||
};
|
||||
|
||||
Symbol symbolTable[coffHeader.numberOfSymbols] @ coffHeader.pointerToSymbolTable;
|
||||
|
||||
struct StringTable {
|
||||
u32 size;
|
||||
NullTerminatedString strings[while($ < addressof(this) + size)];
|
||||
} [[inline]];
|
||||
|
||||
StringTable stringTable[coffHeader.numberOfSymbols > 0] @ addressof(symbolTable) + sizeof(symbolTable);
|
||||
|
||||
// Rich Header
|
||||
enum ProductType : u16 {
|
||||
Unmarked = 0x00 ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
Imports = 0x01 ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
STDLIBDLL = 0x04 ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
VC6CVTRes = 0x06 ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
VC6CCompiler = 0x0A ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
VC6CPPCompiler = 0x0B ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
OldNames = 0x0C ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
MASM613 = 0x0E ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
VC2003Assembler = 0x0F ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
VC2002Linker = 0x19 ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
VC2002CCompiler = 0x1C ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
VC2002CPPCompiler = 0x1D ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
VC2003SDKIMP = 0x5D ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
VC2003CPPCompiler = 0x60 ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
VC2008SDKIMP = 0x93 ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
Linker12 = 0x9D ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
MASM10 = 0x9E ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
VC2010CCompiler = 0xAA ^ richHeaderEnd[0].mask.maskArray[1],
|
||||
VC2010CPPCompiler = 0xAB ^ richHeaderEnd[0].mask.maskArray[1]
|
||||
};
|
||||
|
||||
union RichHeaderMask {
|
||||
u32 maskVariable;
|
||||
u16 maskArray[2];
|
||||
};
|
||||
|
||||
struct RichHeaderEnd {
|
||||
char signature[4];
|
||||
RichHeaderMask mask;
|
||||
} [[inline]];
|
||||
|
||||
u8 richHeaderAmount;
|
||||
u8 richHeaderEndPosition;
|
||||
u8 richHeaderCorpusPosition;
|
||||
fn initializeRichHeader() {
|
||||
$ = sizeof(peHeader);
|
||||
while ($ < peHeader.dosHeader.coffHeaderPointer) {
|
||||
if (std::mem::read_string($, 4) == "Rich") {
|
||||
richHeaderAmount = 1;
|
||||
richHeaderEndPosition = $;
|
||||
break;
|
||||
}
|
||||
$ += 1;
|
||||
}
|
||||
};
|
||||
initializeRichHeader();
|
||||
|
||||
RichHeaderEnd richHeaderEnd[richHeaderAmount] @ richHeaderEndPosition;
|
||||
|
||||
struct Product {
|
||||
u16 buildNumber;
|
||||
ProductType productID;
|
||||
u32 objectCount;
|
||||
};
|
||||
|
||||
struct RichHeaderCorpus {
|
||||
char maskedSignature[4];
|
||||
u32 nullPadding[3];
|
||||
Product products[while($ != richHeaderEndPosition)];
|
||||
} [[inline]];
|
||||
|
||||
fn setupRichHeader() {
|
||||
if (richHeaderAmount > 0) {
|
||||
//0x20 is the size of a Rich Header with one product
|
||||
for (u8 richCursor = $ - 0x20, richCursor > sizeof(peHeader), richCursor = richCursor - 0x01) {
|
||||
if (str(std::mem::read_unsigned(richCursor, 4) ^ richHeaderEnd[0].mask.maskVariable) == "DanS") {
|
||||
richHeaderCorpusPosition = richCursor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
setupRichHeader();
|
||||
|
||||
RichHeaderCorpus richHeaderCorpus[richHeaderAmount] @ richHeaderCorpusPosition;
|
||||
|
||||
35
scripts/extract_legacy_hexproj.py
Normal file
35
scripts/extract_legacy_hexproj.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def extractData(projectName, jsonData, objectName, extension):
|
||||
if objectName in jsonData:
|
||||
with open(f"./{projectName}.{extension}", "w") as output:
|
||||
output.write(jsonData[objectName])
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2 or not str(sys.argv[1]).endswith(".hexproj"):
|
||||
print(f"Usage: {sys.argv[0]} <filename.hexproj>")
|
||||
exit(1)
|
||||
|
||||
projectPath = sys.argv[1]
|
||||
with open(projectPath, "r") as file:
|
||||
jsonData = json.loads(file.read())
|
||||
|
||||
projectName = Path(projectPath).stem
|
||||
|
||||
extractData(projectName, jsonData, "dataProcessor", "hexnode")
|
||||
extractData(projectName, jsonData, "pattern", "hexpat")
|
||||
|
||||
if "bookmarks" in jsonData:
|
||||
with open(f"./{projectName}.hexbm", "w") as output:
|
||||
jsonOutput = {}
|
||||
jsonOutput["bookmarks"] = jsonData["bookmarks"]
|
||||
|
||||
output.write(json.dumps(jsonOutput, indent=4))
|
||||
|
||||
if "filePath" in jsonData:
|
||||
print(f"Project file used file {jsonData['filePath']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,5 +1,8 @@
|
||||
#include <pl.hpp>
|
||||
#include <helpers/file.hpp>
|
||||
#include <pl/helpers/file.hpp>
|
||||
|
||||
#include <pl/core/errors/preprocessor_errors.hpp>
|
||||
#include <pl/core/errors/evaluator_errors.hpp>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <cstdlib>
|
||||
@@ -19,17 +22,20 @@ int main(int argc, char **argv) {
|
||||
fmt::print("Running test {} on test file {}\n", includeName, includeFilePath.filename().string());
|
||||
|
||||
// Open pattern file
|
||||
pl::fs::File patternFile(includeFilePath, pl::fs::File::Mode::Read);
|
||||
pl::hlp::fs::File patternFile(includeFilePath, pl::hlp::fs::File::Mode::Read);
|
||||
if (!patternFile.isValid())
|
||||
return EXIT_FAILURE;
|
||||
|
||||
// Setup Pattern Language Runtime
|
||||
pl::PatternLanguage runtime;
|
||||
{
|
||||
constexpr auto DummyPragmaHandler = [](const auto&, const auto&){ pl::LogConsole::abortEvaluation("Include files should never use this pragma!"); return true; };
|
||||
constexpr auto DummyPragmaHandler = [](const auto&, const auto&){
|
||||
pl::core::err::M0006.throwError("Include files should never use this pragma!");
|
||||
return false;
|
||||
};
|
||||
|
||||
runtime.setDataSource([&](pl::u64 address, pl::u8 *data, size_t size) {
|
||||
pl::LogConsole::abortEvaluation("Include files should never read from memory directly!");
|
||||
pl::core::err::E0011.throwError("Include files should never read from memory directly!");
|
||||
}, 0x00, 0x100000);
|
||||
runtime.setDangerousFunctionCallHandler([]{ return true; });
|
||||
runtime.setIncludePaths({ includePath });
|
||||
@@ -47,14 +53,15 @@ int main(int argc, char **argv) {
|
||||
fmt::print("Error during execution!\n");
|
||||
|
||||
if (const auto &hardError = runtime.getError(); hardError.has_value())
|
||||
fmt::print("Hard error: {}\n\n", hardError.value().what());
|
||||
fmt::print("Hard error: {}:{} - {}\n\n", hardError->line, hardError->column, hardError->message);
|
||||
|
||||
for (const auto &[level, message] : runtime.getConsoleLog()) {
|
||||
switch (level) {
|
||||
case pl::LogConsole::Level::Debug: fmt::print(" [DEBUG] "); break;
|
||||
case pl::LogConsole::Level::Info: fmt::print(" [INFO] "); break;
|
||||
case pl::LogConsole::Level::Warning: fmt::print(" [WARN] "); break;
|
||||
case pl::LogConsole::Level::Error: fmt::print(" [ERROR] "); break;
|
||||
using enum pl::core::LogConsole::Level;
|
||||
case Debug: fmt::print(" [DEBUG] "); break;
|
||||
case Info: fmt::print(" [INFO] "); break;
|
||||
case Warning: fmt::print(" [WARN] "); break;
|
||||
case Error: fmt::print(" [ERROR] "); break;
|
||||
}
|
||||
|
||||
fmt::print("{}\n", message);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include <helpers/file.hpp>
|
||||
#include <helpers/guards.hpp>
|
||||
#include <pl/helpers/file.hpp>
|
||||
#include <pl/helpers/guards.hpp>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <cstdlib>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include <pl.hpp>
|
||||
#include <helpers/file.hpp>
|
||||
#include <pl/helpers/file.hpp>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <cstdlib>
|
||||
@@ -27,12 +27,12 @@ int main(int argc, char **argv) {
|
||||
fmt::print("Running test {} on test file {}\n", patternName, testFilePath.stem().string());
|
||||
|
||||
// Open pattern file
|
||||
pl::fs::File patternFile(patternFilePath, pl::fs::File::Mode::Read);
|
||||
pl::hlp::fs::File patternFile(patternFilePath, pl::hlp::fs::File::Mode::Read);
|
||||
if (!patternFile.isValid())
|
||||
return EXIT_FAILURE;
|
||||
|
||||
// Open test file
|
||||
pl::fs::File testFile(testFilePath, pl::fs::File::Mode::Read);
|
||||
pl::hlp::fs::File testFile(testFilePath, pl::hlp::fs::File::Mode::Read);
|
||||
if (!testFile.isValid())
|
||||
return EXIT_FAILURE;
|
||||
|
||||
@@ -55,14 +55,15 @@ int main(int argc, char **argv) {
|
||||
fmt::print("Error during execution!\n");
|
||||
|
||||
if (const auto &hardError = runtime.getError(); hardError.has_value())
|
||||
fmt::print("Hard error: {}\n\n", hardError.value().what());
|
||||
fmt::print("Hard error: {}:{} - {}\n\n", hardError->line, hardError->column, hardError->message);
|
||||
|
||||
for (const auto &[level, message] : runtime.getConsoleLog()) {
|
||||
switch (level) {
|
||||
case pl::LogConsole::Level::Debug: fmt::print(" [DEBUG] "); break;
|
||||
case pl::LogConsole::Level::Info: fmt::print(" [INFO] "); break;
|
||||
case pl::LogConsole::Level::Warning: fmt::print(" [WARN] "); break;
|
||||
case pl::LogConsole::Level::Error: fmt::print(" [ERROR] "); break;
|
||||
using enum pl::core::LogConsole::Level;
|
||||
case Debug: fmt::print(" [DEBUG] "); break;
|
||||
case Info: fmt::print(" [INFO] "); break;
|
||||
case Warning: fmt::print(" [WARN] "); break;
|
||||
case Error: fmt::print(" [ERROR] "); break;
|
||||
}
|
||||
|
||||
fmt::print("{}\n", message);
|
||||
|
||||
Reference in New Issue
Block a user