From 1771c1f077e16656736422da0112583ff8f1c352 Mon Sep 17 00:00:00 2001 From: Scrivener07 Date: Sun, 23 Mar 2025 06:30:43 -0400 Subject: [PATCH] patterns: Added support for PEX Papyrus executables (#370) Added support for PEX, Bethesda's Papyrus executable for compiled script files. - Skyrim - Fallout 4 - Fallout 76 - Starfield Co-authored-by: Jonathan Ostrus <12855515+jbostrus@users.noreply.github.com> --- README.md | 1 + patterns/pex.hexpat | 466 ++++++++++++++++++++++++ tests/patterns/test_data/pex.hexpat.pex | Bin 0 -> 6072 bytes 3 files changed, 467 insertions(+) create mode 100644 patterns/pex.hexpat create mode 100644 tests/patterns/test_data/pex.hexpat.pex diff --git a/README.md b/README.md index 3c3e48c..5f69996 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | PCK | | [`patterns/pck.hexpat`](patterns/pck.hexpat) | Minecraft Legacy Console Edition .pck file | | PCX | `application/x-pcx` | [`patterns/pcx.hexpat`](patterns/pcx.hexpat) | PCX Image format | | PE | `application/x-dosexec` `application/x-msdownload` | [`patterns/pe.hexpat`](patterns/pe.hexpat) | PE header, COFF header, Standard COFF fields and Windows Specific fields | +| PEX | | [`patterns/pex.hexpat`](patterns/pex.hexpat) | Bethesda Papyrus executable compiled script file | | PP | | [`patterns/selinuxpp.hexpat`](patterns/selinuxpp.pat) | SE Linux package | | PFS0 | | [`patterns/pfs0.hexpat`](patterns/pfs0.hexpat) | Nintendo Switch PFS0 archive (NSP files) | | PF | | [`patterns/pf.hexpat`](patterns/pf.hexpat) | Microsoft uncompressed prefetch files (.pf) | diff --git a/patterns/pex.hexpat b/patterns/pex.hexpat new file mode 100644 index 0000000..5dc3f70 --- /dev/null +++ b/patterns/pex.hexpat @@ -0,0 +1,466 @@ +#pragma author Scrivener07 and Jonathan Ostrus +#pragma description Bethesda Papyrus executable compiled script file + +#pragma magic [FA 57 C0 DE] @ 0x00 +#pragma magic [DE C0 57 FA] @ 0x00 + +import std.sys; +import std.core; +import std.string; +import std.time; + +enum GAMEID : u16 { + GAME_Skyrim = 1, + GAME_Fallout4 = 2, + GAME_Fallout76 = 3, + GAME_Starfield = 4 +}; + +GAMEID g_GameId; + +namespace formatter { + + fn pexversion(ref auto data) { + return std::format("{}.{}", data.MajorVersion, data.MinorVersion); + }; + + fn structname(ref auto data) { + return StringLookup(data.name); + }; + + fn typedstructname(ref auto data) { + return std::format("{0} {1}", StringLookup(data.typeName), StringLookup(data.name)); + }; + + fn time(ref auto data) { + // We will only support dates back to the epoch + std::time::Time time64 = std::time::to_utc(data.Time); + return std::time::format(time64, "%c"); + }; + + fn vartype(u8 data) { + match (data) + { + (0x0): return "object"; + (0x1): return "identifier"; + (0x2): return "string"; + (0x3): return "integer"; + (0x4): return "float"; + (0x5): return "bool"; + (_): return "Unknown type"; + } + }; + + fn arraycount(ref auto data) { + return std::format("[{}]", data.count); + }; + + fn functiontype(u8 data) { + match(data) { + (0): return "regular"; + (1): return "property getter"; + (2): return "property setter"; + (3): std::assert(true, "Unknown function type"); + } + }; +} + +fn StringLookup(u16 idx) { + return pex.stringsTable.strings[idx]; +}; + +struct PexVersion { + u8 MajorVersion; + u8 MinorVersion; +}[[sealed, format("formatter::pexversion")]]; + +struct StringReference { + u16 name; +} [[sealed, format("formatter::structname")]]; + +// Dynamic array of strings +struct StringsTable { + u16 count; + if (count > 0) + std::string::SizedString strings[count]; +} [[format("formatter::arraycount")]]; + +struct Time { + u64 Time; +}[[sealed, format("formatter::time")]]; + +struct VariableData { + u8 varType [[format("formatter::vartype")]]; + match (varType) + { + (0x0): {} // object pointer + (0x1): u16 stringVal [[format("StringLookup")]]; // identifier + (0x2): u16 stringVal [[format("StringLookup")]]; // string + (0x3): s32 intVal; // integer + (0x4): float floatVal; + (0x5): bool boolVal; + (_): std::assert(false, "Unknown type for variable data"); + } +}; + +struct VariableType { + u16 name [[format("StringLookup")]]; + u16 varType [[format("StringLookup")]]; +}; + +struct VariableTypes { + u16 count; + if (count > 0) + VariableType parameter[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct Instruction { + u8 op; + match (op) + { + ( + 0x00 // nop + ): {} + ( + 0x01 | // iadd + 0x02 | // fadd + 0x03 | // isub + 0x04 | // fsub + 0x05 | // imul + 0x06 | // fmul + 0x07 | // idiv + 0x08 | // fdiv + 0x09 | // imod + 0x0F | // cmp_eq + 0x10 | // cmp_lt + 0x11 | // cmp_lte + 0x12 | // cmp_gt + 0x13 | // comp_gte + 0x1B | // strcat + 0x1C | // propget + 0x1D | // propset + 0x20 | // array_getlement + 0x21 | // array_setelement + 0x24 | // is + 0x26 | // struct_get + 0x27 | // struct_set + 0x2A | // array_add + 0x2B | // array_insert + 0x2D // array_remove + ): VariableData argument[3]; + ( + 0x22 | // array_findelement + 0x23 // array_rfindelement + ): VariableData argument[4]; + ( + 0x14 | // jmp + 0x1A | // return + 0x25 | // struct_create + 0x2C | // array_removelast + 0x2E // array_clear + ): VariableData argument[1]; + ( + 0x0A | // not + 0x0B | // ineg + 0x0C | // fneg + 0x0D | // assign + 0x0E | // cast + 0x15 | // jmpt + 0x16 | // jmpf + 0x1E | // array_create + 0x1F // array_length + ): VariableData argument[2]; + ( + 0x28 | // array_findstruct + 0x29 // array_rfindstruct + ): VariableData argument[5]; + ( + 0x2F // array_getallmatchingstructs + ): VariableData argument[6]; + ( + 0x17 | // callmethod + 0x19 // callstatic + ): { + VariableData argument[4]; + std::assert(argument[3].varType == 0x3, "VarArgs not integer"); + if (argument[3].intVal > 0) + VariableData varArgument[argument[3].intVal]; + } + ( + 0x18 // callparent + ): { + VariableData argument[3]; + std::assert(argument[2].varType == 0x3, "VarArgs not integer"); + if (argument[2].intVal > 0) + VariableData varArgument[argument[2].intVal]; + } + ( + 0x30 | // lock_guards + 0x31 // unlock_guards + ): { + VariableData argument[1]; + std::assert(argument[0].varType == 0x3, "VarArgs not integer"); + if (argument[0].intVal > 0) + VariableData varArgument[argument[0].intVal]; + } + ( + 0x32 // try_lock_guards + ): { + VariableData argument[2]; + std::assert(argument[1].varType == 0x3, "VarArgs not integer"); + if (argument[1].intVal > 0) + VariableData varArgument[argument[1].intVal]; + } + (_): { + Printf("Invalid opcode: %Xh\n", op); + std::assert(false, "Invalid opcode"); + } + } + +}; + +struct Instructions { + u16 count; + if (count > 0) + Instruction instruction[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct Function { + u16 returnType [[format("StringLookup")]]; + u16 docString [[format("StringLookup")]]; + u32 userFlags; + u8 flags; + VariableTypes arguments; + VariableTypes locals; + Instructions instructions; +}; + +struct NamedFunction { + u16 name [[format("StringLookup")]]; + Function function [[inline]]; +} [[format("formatter::structname")]]; + +struct NamedFunctions { + u16 count; + if (count > 0) + NamedFunction function[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct State { + u16 name [[format("StringLookup")]]; + NamedFunctions functions; +} [[format("formatter::structname")]]; + +struct States { + u16 count; + if (count > 0) + State state[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct Property { + u16 name [[format("StringLookup")]]; + u16 typeName [[format("StringLookup")]]; + u16 docString [[format("StringLookup")]]; + u32 userFlags; + u8 flags; + if ((flags & 0x4) != 0) + u16 autoVarName [[format("StringLookup")]]; + else { + if ((flags & 0x1) != 0) + Function readHandler; + if ((flags & 0x2) != 0) + Function writeHandler; + } +} [[format("formatter::structname")]]; + +struct Properties { + u16 count; + if (count > 0) + Property property[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct Variable { + u16 name [[format("StringLookup")]]; + u16 typeName [[format("StringLookup")]]; + u32 userFlags; + VariableData data; + if (g_GameId >= GAMEID::GAME_Fallout4) + u8 constFlag; +} [[format("formatter::structname")]];; + +struct Variables { + u16 count; + if (count > 0) + Variable variable[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct ObjectStructMember { + u16 name [[format("StringLookup")]]; + u16 typeName [[format("StringLookup")]]; + u32 userFlags; + VariableData data; + u8 constFlag; + u16 docString [[format("StringLookup")]]; +} [[format("formatter::typedstructname")]]; + +struct ObjectStructMembers { + u16 count; + if (count > 0) + ObjectStructMember members[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct ObjectStruct { + u16 name [[format("StringLookup")]]; + ObjectStructMembers members; +} [[format("formatter::structname")]]; + +struct ObjectStructs { + u16 count; + if (count > 0) + ObjectStruct structs[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct Guards { + u16 count; + if (count > 0) + StringReference names[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct ScriptObjectData { + u16 parentClassName [[format("StringLookup")]]; + u16 docString [[format("StringLookup")]]; + if (g_GameId >= GAMEID::GAME_Fallout4) + u8 constFlag; + u32 userFlags; + u16 autoStateName [[format("StringLookup")]]; + if (g_GameId >= GAMEID::GAME_Fallout4) + ObjectStructs structs; + Variables variables; + if (g_GameId >= GAMEID::GAME_Starfield) + Guards guards; + Properties properties; + States states; + if (g_GameId == GAMEID::GAME_Fallout76) + u16 unknown; +}; + +struct ScriptObject { + u16 name [[format("StringLookup")]]; + u32 length; + ScriptObjectData data; + + // The BGS Compiler sets length to sizeof(data) + sizeof(length) + // The Caprica compiler sets the length to just sizeof(data) + // Since we have no way to identify which compiler is used we'll check + // both and just hope it isn't short exactly 4 bytes + std::assert(sizeof(data) == length - 4 || sizeof(data) == length, "Length of object data in state missmatch."); +}; + +struct ScriptObjects { + u16 count; + if (count > 0) + ScriptObject scriptObjects[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct UserFlag { + u16 name [[format("StringLookup")]]; + u8 flagIndex; +} [[format("formatter::structname")]]; + +struct UserFlags { + u16 count; + if (count > 0) + UserFlag userFlags[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct StringMembers { + u16 count; + if (count > 0) + StringReference members[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct DebugPropertyGroup { + u16 objectName [[format("StringLookup")]]; + u16 name [[format("StringLookup")]]; + u16 docString [[format("StringLookup")]]; + u32 userFlags; + StringMembers members; +} [[format("formatter::structname")]]; + +struct DebugInstructions { + u16 count; + if (count > 0) + u16 lineNumbers[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct DebugFunction { + u16 objectName [[format("StringLookup")]]; + u16 stateName [[format("StringLookup")]]; + u16 name [[format("StringLookup")]]; + u8 functionType [[format("formatter::functiontype")]]; + DebugInstructions instructions; +} [[format("formatter::structname")]]; + +struct DebugStruct { + u16 name [[format("StringLookup")]]; + u16 orderName [[format("StringLookup")]]; + StringMembers members; +}; + +struct DebugFunctions { + u16 count; + if (count > 0) + DebugFunction functions[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct DebugPropertyGroups { + u16 count; + if (count > 0) + DebugPropertyGroup groups[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct DebugStructs { + u16 count; + if (count > 0) + DebugStruct structs[count] [[inline]]; +} [[format("formatter::arraycount")]]; + +struct DebugInfo { + bool hasDebugInfo; + if (hasDebugInfo) + { + Time modificationTime; + DebugFunctions functions; + if (g_GameId >= GAMEID::GAME_Fallout4) + { + DebugPropertyGroups groups; + DebugStructs structs; + } + } +}; + +// Header +struct Header { + u32 Magic; + PexVersion pexVersion; + GAMEID GameID; g_GameId = GameID; + Time CompilationTime; + std::string::SizedString SourceFileName; + std::string::SizedString UserName; + std::string::SizedString MachineName; +}; + +struct PEX { + if (std::mem::read_unsigned(0, 4) == le u32(0xDEC057FA)) + std::core::set_endian(std::mem::Endian::Big); + else + std::core::set_endian(std::mem::Endian::Little); + + Header header; + StringsTable stringsTable; + DebugInfo debugInfo; + UserFlags userFlags; + ScriptObjects scriptObjects; +}; + +PEX pex @ 0x00; diff --git a/tests/patterns/test_data/pex.hexpat.pex b/tests/patterns/test_data/pex.hexpat.pex new file mode 100644 index 0000000000000000000000000000000000000000..1a83c58271959141e81fbf991d0761e9930096e2 GIT binary patch literal 6072 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