From 91e9222fcfcd5e10291da5cf770e79a0f5f11484 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 2 Jul 2025 14:40:13 +0300 Subject: [PATCH 01/93] Add Capcom's Devil May Cry 3 HD .mod hexpat Hex Pattern file for Capcom's Devil May Cry 3 HD Collection's .mod (3D Models) files --- patterns/DMC3 HD Mod.hexpat | 160 ++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 patterns/DMC3 HD Mod.hexpat diff --git a/patterns/DMC3 HD Mod.hexpat b/patterns/DMC3 HD Mod.hexpat new file mode 100644 index 00000000..a782d6b3 --- /dev/null +++ b/patterns/DMC3 HD Mod.hexpat @@ -0,0 +1,160 @@ +// author = haru233, many thanks to AxCut +// ImHex Hex Pattern File for Capcom's Devil May Cry 3 HD .mod files + +import std.core; + + +struct ModelHeader { + char ID[4]; + float Version; + u8 padding0[8]; + u8 objectCount; + u8 boneCount; + u8 numberTextures; + u8 unknown1; + u32 unknown2; + u64 unknown3; + u64 skeletonOffset; + u8 padding1[24]; +}; + +struct Object { + u8 meshCount; + u8 unknown; + u16 numberVertices; + u8 padding0[4]; + u64 meshOffset; + u32 flags; + u8 padding1[28]; + float X, Y, Z; + float radius; +}; + +struct Positions { + float positions[3]; +}; + + +struct Normals { + float normal[3]; +}; + + +struct UVs { + float uv[2]; +}; + +struct BoneIndices { + u8 boneindex[4]; +}; + +struct Weights { + u16 weight[1]; +}; + +struct MeshSCM { + u16 numberVertices; + u16 textureIndex; + u8 padding0[12]; + u64 VerticesPositionsOffset; + u64 NormalsPositionsOffset; + u64 UVsPositionsOffset; + + u8 padding2[16]; + u64 unknownOffset; + + u64 unknown; + u8 padding3[8]; + + Positions positions[numberVertices] @VerticesPositionsOffset; + Normals normals[numberVertices] @NormalsPositionsOffset; + UVs uvs[numberVertices] @UVsPositionsOffset; + + +}; + +struct Mesh { + u16 numberVertices; + u16 textureIndex; + u8 padding0[12]; + u64 VerticesPositionsOffset; + u64 NormalsPositionsOffset; + u64 UVsPositionsOffset; + + u64 BoneIndicesOffset; + u64 WeightsOffset; + u8 padding1[8]; + + u64 unknown; + u8 padding3[8]; + + Positions positions[numberVertices] @VerticesPositionsOffset; + Normals normals[numberVertices] @NormalsPositionsOffset; + UVs uvs[numberVertices] @UVsPositionsOffset; + + BoneIndices b_index[numberVertices] @BoneIndicesOffset; + Weights weights[numberVertices] @WeightsOffset; + + +}; + + +struct Hierarchy { + u8 hierarchy; +}; + +struct HierarchyOrder { + u8 hierarchyorder; +}; + +struct Unknown { + u8 unknown; +}; + +struct Transform { + float x; + float y; + float z; + float length; // sqrt(x*x + y*y + z*z) +}; + +struct Skeleton{ + u32 hierarchyOffset; + u32 hierarchyOrderOffset; + u32 unknownOffset; + u32 transformsOffset; +}; + + + + + +ModelHeader modelheader @ 0x00; +Object objects[modelheader.objectCount] @ 0x40; + +u32 objectOffset; + +struct IthMesh { + u64 i = std::core::array_index(); + if (modelheader.ID == "SCM ") { + objectOffset = objects[0].meshOffset; + MeshSCM meshscm[objects[i].meshCount] @ objects[i].meshOffset; + + + } else { + objectOffset = objects[0].meshOffset; + Mesh mesh[objects[i].meshCount] @ objects[i].meshOffset; + } +}; + +IthMesh meshes[modelheader.objectCount] @objectOffset; + +Skeleton skeleton @modelheader.skeletonOffset; + +Hierarchy hierarchy[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.hierarchyOffset); + +HierarchyOrder hierarchyorder[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.hierarchyOrderOffset); + +Unknown unknown[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.unknownOffset); + +Transform transform[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.transformsOffset); From 76b24fc7272d258e37dfed66fedd33ac071528a5 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 2 Jul 2025 17:37:10 +0300 Subject: [PATCH 02/93] Update DMC3 HD Mod.hexpat --- patterns/DMC3 HD Mod.hexpat | 1 + 1 file changed, 1 insertion(+) diff --git a/patterns/DMC3 HD Mod.hexpat b/patterns/DMC3 HD Mod.hexpat index a782d6b3..f5b14b53 100644 --- a/patterns/DMC3 HD Mod.hexpat +++ b/patterns/DMC3 HD Mod.hexpat @@ -1,3 +1,4 @@ +#pragma description Devil May Cry 3 HD .mod 3D model file // author = haru233, many thanks to AxCut // ImHex Hex Pattern File for Capcom's Devil May Cry 3 HD .mod files From 7481fe5d7356b1a3175f4b36e116d9b5c752302c Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 2 Jul 2025 17:44:30 +0300 Subject: [PATCH 03/93] Update DMC3 HD Mod.hexpat --- patterns/DMC3 HD Mod.hexpat | 2 ++ 1 file changed, 2 insertions(+) diff --git a/patterns/DMC3 HD Mod.hexpat b/patterns/DMC3 HD Mod.hexpat index f5b14b53..fd103717 100644 --- a/patterns/DMC3 HD Mod.hexpat +++ b/patterns/DMC3 HD Mod.hexpat @@ -1,4 +1,6 @@ #pragma description Devil May Cry 3 HD .mod 3D model file +#pragma MIME 3d-model/capcom.dmc3-hd-mod + // author = haru233, many thanks to AxCut // ImHex Hex Pattern File for Capcom's Devil May Cry 3 HD .mod files From 7b08f082d856c4ac51c0c887f44db95c60b00038 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 2 Jul 2025 18:06:35 +0300 Subject: [PATCH 04/93] Update DMC3 HD Mod.hexpat --- patterns/DMC3 HD Mod.hexpat | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/patterns/DMC3 HD Mod.hexpat b/patterns/DMC3 HD Mod.hexpat index fd103717..b08b8dcd 100644 --- a/patterns/DMC3 HD Mod.hexpat +++ b/patterns/DMC3 HD Mod.hexpat @@ -4,6 +4,7 @@ // author = haru233, many thanks to AxCut // ImHex Hex Pattern File for Capcom's Devil May Cry 3 HD .mod files + import std.core; @@ -44,7 +45,7 @@ struct Normals { struct UVs { - float uv[2]; + s16 uv[2]; }; struct BoneIndices { @@ -119,6 +120,7 @@ struct Transform { float y; float z; float length; // sqrt(x*x + y*y + z*z) + u8 unknown[16]; }; struct Skeleton{ From e182d178504ad1140bb886152d0b0b8dce70afa2 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 2 Jul 2025 19:47:34 +0300 Subject: [PATCH 05/93] Add files via upload --- .../patterns/test_data/dmc3_hd_mod.hexpat.mod | Bin 0 -> 192784 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/patterns/test_data/dmc3_hd_mod.hexpat.mod diff --git a/tests/patterns/test_data/dmc3_hd_mod.hexpat.mod b/tests/patterns/test_data/dmc3_hd_mod.hexpat.mod new file mode 100644 index 0000000000000000000000000000000000000000..a0a149cd2e98f80c221ff4d4866471cf2ddf82c7 GIT binary patch literal 192784 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%0D4e$$ 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 literal 0 HcmV?d00001 From d2dfe0acc1fbd9437aa2839bf77ff85e08ec0727 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Thu, 3 Jul 2025 19:44:26 +0300 Subject: [PATCH 06/93] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 55e004aa..1f352a42 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ZIP | `application/zip` | [`patterns/zip.hexpat`](patterns/zip.hexpat) | End of Central Directory Header, Central Directory File Headers | | ZLIB | `application/zlib` | [`patterns/zlib.hexpat`](patterns/zlib.hexpat) | ZLIB compressed data format | | ZSTD | `application/zstd` | [`patterns/zstd.hexpat`](patterns/zstd.hexpat) | Zstandard compressed data format | +| MOD | `3d-model/mod` | [`patterns/DMC3HD-Mod.hexpat`](patterns/dmc3_hd_mod.hexpat) | 3D Model files used in Devil May Cry 3 HD Collection | ### Scripts From 7e8f6035e198474f0b54a9430f26b785ea52e355 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Thu, 3 Jul 2025 19:57:54 +0300 Subject: [PATCH 07/93] Rename DMC3 HD Mod.hexpat to dmc3_hd_mod.hexpat --- patterns/{DMC3 HD Mod.hexpat => dmc3_hd_mod.hexpat} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename patterns/{DMC3 HD Mod.hexpat => dmc3_hd_mod.hexpat} (100%) diff --git a/patterns/DMC3 HD Mod.hexpat b/patterns/dmc3_hd_mod.hexpat similarity index 100% rename from patterns/DMC3 HD Mod.hexpat rename to patterns/dmc3_hd_mod.hexpat From a67765deb455844e0503aaf5328edeeb5a560e3c Mon Sep 17 00:00:00 2001 From: haruse23 Date: Thu, 3 Jul 2025 20:16:32 +0300 Subject: [PATCH 08/93] Delete patterns/dmc3_hd_mod.hexpat --- patterns/dmc3_hd_mod.hexpat | 165 ------------------------------------ 1 file changed, 165 deletions(-) delete mode 100644 patterns/dmc3_hd_mod.hexpat diff --git a/patterns/dmc3_hd_mod.hexpat b/patterns/dmc3_hd_mod.hexpat deleted file mode 100644 index b08b8dcd..00000000 --- a/patterns/dmc3_hd_mod.hexpat +++ /dev/null @@ -1,165 +0,0 @@ -#pragma description Devil May Cry 3 HD .mod 3D model file -#pragma MIME 3d-model/capcom.dmc3-hd-mod - -// author = haru233, many thanks to AxCut -// ImHex Hex Pattern File for Capcom's Devil May Cry 3 HD .mod files - - -import std.core; - - -struct ModelHeader { - char ID[4]; - float Version; - u8 padding0[8]; - u8 objectCount; - u8 boneCount; - u8 numberTextures; - u8 unknown1; - u32 unknown2; - u64 unknown3; - u64 skeletonOffset; - u8 padding1[24]; -}; - -struct Object { - u8 meshCount; - u8 unknown; - u16 numberVertices; - u8 padding0[4]; - u64 meshOffset; - u32 flags; - u8 padding1[28]; - float X, Y, Z; - float radius; -}; - -struct Positions { - float positions[3]; -}; - - -struct Normals { - float normal[3]; -}; - - -struct UVs { - s16 uv[2]; -}; - -struct BoneIndices { - u8 boneindex[4]; -}; - -struct Weights { - u16 weight[1]; -}; - -struct MeshSCM { - u16 numberVertices; - u16 textureIndex; - u8 padding0[12]; - u64 VerticesPositionsOffset; - u64 NormalsPositionsOffset; - u64 UVsPositionsOffset; - - u8 padding2[16]; - u64 unknownOffset; - - u64 unknown; - u8 padding3[8]; - - Positions positions[numberVertices] @VerticesPositionsOffset; - Normals normals[numberVertices] @NormalsPositionsOffset; - UVs uvs[numberVertices] @UVsPositionsOffset; - - -}; - -struct Mesh { - u16 numberVertices; - u16 textureIndex; - u8 padding0[12]; - u64 VerticesPositionsOffset; - u64 NormalsPositionsOffset; - u64 UVsPositionsOffset; - - u64 BoneIndicesOffset; - u64 WeightsOffset; - u8 padding1[8]; - - u64 unknown; - u8 padding3[8]; - - Positions positions[numberVertices] @VerticesPositionsOffset; - Normals normals[numberVertices] @NormalsPositionsOffset; - UVs uvs[numberVertices] @UVsPositionsOffset; - - BoneIndices b_index[numberVertices] @BoneIndicesOffset; - Weights weights[numberVertices] @WeightsOffset; - - -}; - - -struct Hierarchy { - u8 hierarchy; -}; - -struct HierarchyOrder { - u8 hierarchyorder; -}; - -struct Unknown { - u8 unknown; -}; - -struct Transform { - float x; - float y; - float z; - float length; // sqrt(x*x + y*y + z*z) - u8 unknown[16]; -}; - -struct Skeleton{ - u32 hierarchyOffset; - u32 hierarchyOrderOffset; - u32 unknownOffset; - u32 transformsOffset; -}; - - - - - -ModelHeader modelheader @ 0x00; -Object objects[modelheader.objectCount] @ 0x40; - -u32 objectOffset; - -struct IthMesh { - u64 i = std::core::array_index(); - if (modelheader.ID == "SCM ") { - objectOffset = objects[0].meshOffset; - MeshSCM meshscm[objects[i].meshCount] @ objects[i].meshOffset; - - - } else { - objectOffset = objects[0].meshOffset; - Mesh mesh[objects[i].meshCount] @ objects[i].meshOffset; - } -}; - -IthMesh meshes[modelheader.objectCount] @objectOffset; - -Skeleton skeleton @modelheader.skeletonOffset; - -Hierarchy hierarchy[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.hierarchyOffset); - -HierarchyOrder hierarchyorder[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.hierarchyOrderOffset); - -Unknown unknown[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.unknownOffset); - -Transform transform[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.transformsOffset); From 6de6f373ba0dbba6570f8ce8e68052abd060b753 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Thu, 3 Jul 2025 20:34:32 +0300 Subject: [PATCH 09/93] Delete tests/patterns/test_data/dmc3_hd_mod.hexpat.mod --- .../patterns/test_data/dmc3_hd_mod.hexpat.mod | Bin 192784 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/patterns/test_data/dmc3_hd_mod.hexpat.mod diff --git a/tests/patterns/test_data/dmc3_hd_mod.hexpat.mod b/tests/patterns/test_data/dmc3_hd_mod.hexpat.mod deleted file mode 100644 index a0a149cd2e98f80c221ff4d4866471cf2ddf82c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 192784 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%0D4e$$ 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 From 646057bf61ff15ea59385320a1f7407fdc6c4e39 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Thu, 3 Jul 2025 20:35:06 +0300 Subject: [PATCH 10/93] Add files via upload --- patterns/dmc3_hd_mod.hexpat | 165 ++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 patterns/dmc3_hd_mod.hexpat diff --git a/patterns/dmc3_hd_mod.hexpat b/patterns/dmc3_hd_mod.hexpat new file mode 100644 index 00000000..d691fe9f --- /dev/null +++ b/patterns/dmc3_hd_mod.hexpat @@ -0,0 +1,165 @@ +#pragma description Devil May Cry 3 HD .mod 3D model file +#pragma MIME 3d-model/capcom.dmc3-hd-mod + +// author = haru233, many thanks to AxCut +// ImHex Hex Pattern File for Capcom's Devil May Cry 3 HD .mod files + + +import std.core; + + +struct ModelHeader { + char ID[4]; + float Version; + u8 padding0[8]; + u8 objectCount; + u8 boneCount; + u8 numberTextures; + u8 unknown1; + u32 unknown2; + u64 unknown3; + u64 skeletonOffset; + u8 padding1[24]; +}; + +struct Object { + u8 meshCount; + u8 unknown; + u16 numberVertices; + u8 padding0[4]; + u64 meshOffset; + u32 flags; + u8 padding1[28]; + float X, Y, Z; + float radius; +}; + +struct Positions { + float positions[3]; +}; + + +struct Normals { + float normal[3]; +}; + + +struct UVs { + s16 uv[2]; +}; + +struct BoneIndices { + u8 boneindex[4]; +}; + +struct Weights { + u16 weight[1]; +}; + +struct MeshSCM { + u16 numberVertices; + u16 textureIndex; + u8 padding0[12]; + u64 VerticesPositionsOffset; + u64 NormalsPositionsOffset; + u64 UVsPositionsOffset; + + u8 padding2[16]; + u64 unknownOffset; + + u64 unknown; + u8 padding3[8]; + + Positions positions[numberVertices] @VerticesPositionsOffset; + Normals normals[numberVertices] @NormalsPositionsOffset; + UVs uvs[numberVertices] @UVsPositionsOffset; + + +}; + +struct Mesh { + u16 numberVertices; + u16 textureIndex; + u8 padding0[12]; + u64 VerticesPositionsOffset; + u64 NormalsPositionsOffset; + u64 UVsPositionsOffset; + + u64 BoneIndicesOffset; + u64 WeightsOffset; + u8 padding1[8]; + + u64 unknown; + u8 padding3[8]; + + Positions positions[numberVertices] @VerticesPositionsOffset; + Normals normals[numberVertices] @NormalsPositionsOffset; + UVs uvs[numberVertices] @UVsPositionsOffset; + + BoneIndices b_index[numberVertices] @BoneIndicesOffset; + Weights weights[numberVertices] @WeightsOffset; + + +}; + + +struct Hierarchy { + u8 hierarchy; +}; + +struct HierarchyOrder { + u8 hierarchyorder; +}; + +struct Unknown { + u8 unknown; +}; + +struct Transform { + float x; + float y; + float z; + float length; // sqrt(x*x + y*y + z*z) + u8 unknown[16]; +}; + +struct Skeleton{ + u32 hierarchyOffset; + u32 hierarchyOrderOffset; + u32 unknownOffset; + u32 transformsOffset; +}; + + + + + +ModelHeader modelheader @ 0x00; +Object objects[modelheader.objectCount] @ 0x40; + +u32 objectOffset; + +struct IthMesh { + u64 i = std::core::array_index(); + if (modelheader.ID == "SCM ") { + objectOffset = objects[0].meshOffset; + MeshSCM meshscm[objects[i].meshCount] @ objects[i].meshOffset; + + + } else { + objectOffset = objects[0].meshOffset; + Mesh mesh[objects[i].meshCount] @ objects[i].meshOffset; + } +}; + +IthMesh meshes[modelheader.objectCount] @objectOffset; + +Skeleton skeleton @modelheader.skeletonOffset; + +Hierarchy hierarchy[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.hierarchyOffset); + +HierarchyOrder hierarchyorder[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.hierarchyOrderOffset); + +Unknown unknown[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.unknownOffset); + +Transform transform[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.transformsOffset); \ No newline at end of file From 34341ebc5f4b7b9f8b4692be57165467006faeff Mon Sep 17 00:00:00 2001 From: haruse23 Date: Tue, 8 Jul 2025 08:45:42 +0300 Subject: [PATCH 11/93] Update dmc3_hd_mod.hexpat --- patterns/dmc3_hd_mod.hexpat | 54 ++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/patterns/dmc3_hd_mod.hexpat b/patterns/dmc3_hd_mod.hexpat index d691fe9f..9872b781 100644 --- a/patterns/dmc3_hd_mod.hexpat +++ b/patterns/dmc3_hd_mod.hexpat @@ -11,25 +11,25 @@ import std.core; struct ModelHeader { char ID[4]; float Version; - u8 padding0[8]; + padding[8]; u8 objectCount; u8 boneCount; u8 numberTextures; - u8 unknown1; - u32 unknown2; - u64 unknown3; + u8; + u32; + u64; u64 skeletonOffset; - u8 padding1[24]; + padding[24]; }; -struct Object { +struct ObjectInfo { u8 meshCount; - u8 unknown; + u8; u16 numberVertices; - u8 padding0[4]; + padding[4]; u64 meshOffset; u32 flags; - u8 padding1[28]; + padding[28]; float X, Y, Z; float radius; }; @@ -59,16 +59,16 @@ struct Weights { struct MeshSCM { u16 numberVertices; u16 textureIndex; - u8 padding0[12]; + padding[12]; u64 VerticesPositionsOffset; u64 NormalsPositionsOffset; u64 UVsPositionsOffset; - u8 padding2[16]; + padding[16]; u64 unknownOffset; - u64 unknown; - u8 padding3[8]; + u64; + padding[8]; Positions positions[numberVertices] @VerticesPositionsOffset; Normals normals[numberVertices] @NormalsPositionsOffset; @@ -80,17 +80,17 @@ struct MeshSCM { struct Mesh { u16 numberVertices; u16 textureIndex; - u8 padding0[12]; + padding[12]; u64 VerticesPositionsOffset; u64 NormalsPositionsOffset; u64 UVsPositionsOffset; u64 BoneIndicesOffset; u64 WeightsOffset; - u8 padding1[8]; + padding[8]; - u64 unknown; - u8 padding3[8]; + u64; + padding[8]; Positions positions[numberVertices] @VerticesPositionsOffset; Normals normals[numberVertices] @NormalsPositionsOffset; @@ -112,7 +112,7 @@ struct HierarchyOrder { }; struct Unknown { - u8 unknown; + u8; }; struct Transform { @@ -120,7 +120,7 @@ struct Transform { float y; float z; float length; // sqrt(x*x + y*y + z*z) - u8 unknown[16]; + padding[16]; }; struct Skeleton{ @@ -135,24 +135,24 @@ struct Skeleton{ ModelHeader modelheader @ 0x00; -Object objects[modelheader.objectCount] @ 0x40; +ObjectInfo objects_info[modelheader.objectCount] @ 0x40; u32 objectOffset; -struct IthMesh { +struct Object { u64 i = std::core::array_index(); if (modelheader.ID == "SCM ") { - objectOffset = objects[0].meshOffset; - MeshSCM meshscm[objects[i].meshCount] @ objects[i].meshOffset; + objectOffset = objects_info[0].meshOffset; + MeshSCM meshscm[objects_info[i].meshCount] @ objects_info[i].meshOffset; } else { - objectOffset = objects[0].meshOffset; - Mesh mesh[objects[i].meshCount] @ objects[i].meshOffset; + objectOffset = objects_info[0].meshOffset; + Mesh mesh[objects_info[i].meshCount] @ objects_info[i].meshOffset; } }; -IthMesh meshes[modelheader.objectCount] @objectOffset; +Object objects[modelheader.objectCount] @objectOffset; Skeleton skeleton @modelheader.skeletonOffset; @@ -162,4 +162,4 @@ HierarchyOrder hierarchyorder[modelheader.boneCount] @(modelheader.skeletonOffse Unknown unknown[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.unknownOffset); -Transform transform[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.transformsOffset); \ No newline at end of file +Transform transform[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.transformsOffset); From fed5db41091173ec2fba61db14350644b4b12539 Mon Sep 17 00:00:00 2001 From: Nik Date: Tue, 8 Jul 2025 23:51:54 +0200 Subject: [PATCH 12/93] Add Capcom's Devil May Cry 3 HD .mod hexpat (#415) * Add Capcom's Devil May Cry 3 HD .mod hexpat Hex Pattern file for Capcom's Devil May Cry 3 HD Collection's .mod (3D Models) files * Update DMC3 HD Mod.hexpat * Update DMC3 HD Mod.hexpat * Update DMC3 HD Mod.hexpat * Add files via upload * Update README.md * Rename DMC3 HD Mod.hexpat to dmc3_hd_mod.hexpat * Delete patterns/dmc3_hd_mod.hexpat * Delete tests/patterns/test_data/dmc3_hd_mod.hexpat.mod * Add files via upload * Update dmc3_hd_mod.hexpat --------- Co-authored-by: haruse23 --- README.md | 1 + patterns/dmc3_hd_mod.hexpat | 165 ++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 patterns/dmc3_hd_mod.hexpat diff --git a/README.md b/README.md index 55e004aa..1f352a42 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ZIP | `application/zip` | [`patterns/zip.hexpat`](patterns/zip.hexpat) | End of Central Directory Header, Central Directory File Headers | | ZLIB | `application/zlib` | [`patterns/zlib.hexpat`](patterns/zlib.hexpat) | ZLIB compressed data format | | ZSTD | `application/zstd` | [`patterns/zstd.hexpat`](patterns/zstd.hexpat) | Zstandard compressed data format | +| MOD | `3d-model/mod` | [`patterns/DMC3HD-Mod.hexpat`](patterns/dmc3_hd_mod.hexpat) | 3D Model files used in Devil May Cry 3 HD Collection | ### Scripts diff --git a/patterns/dmc3_hd_mod.hexpat b/patterns/dmc3_hd_mod.hexpat new file mode 100644 index 00000000..9872b781 --- /dev/null +++ b/patterns/dmc3_hd_mod.hexpat @@ -0,0 +1,165 @@ +#pragma description Devil May Cry 3 HD .mod 3D model file +#pragma MIME 3d-model/capcom.dmc3-hd-mod + +// author = haru233, many thanks to AxCut +// ImHex Hex Pattern File for Capcom's Devil May Cry 3 HD .mod files + + +import std.core; + + +struct ModelHeader { + char ID[4]; + float Version; + padding[8]; + u8 objectCount; + u8 boneCount; + u8 numberTextures; + u8; + u32; + u64; + u64 skeletonOffset; + padding[24]; +}; + +struct ObjectInfo { + u8 meshCount; + u8; + u16 numberVertices; + padding[4]; + u64 meshOffset; + u32 flags; + padding[28]; + float X, Y, Z; + float radius; +}; + +struct Positions { + float positions[3]; +}; + + +struct Normals { + float normal[3]; +}; + + +struct UVs { + s16 uv[2]; +}; + +struct BoneIndices { + u8 boneindex[4]; +}; + +struct Weights { + u16 weight[1]; +}; + +struct MeshSCM { + u16 numberVertices; + u16 textureIndex; + padding[12]; + u64 VerticesPositionsOffset; + u64 NormalsPositionsOffset; + u64 UVsPositionsOffset; + + padding[16]; + u64 unknownOffset; + + u64; + padding[8]; + + Positions positions[numberVertices] @VerticesPositionsOffset; + Normals normals[numberVertices] @NormalsPositionsOffset; + UVs uvs[numberVertices] @UVsPositionsOffset; + + +}; + +struct Mesh { + u16 numberVertices; + u16 textureIndex; + padding[12]; + u64 VerticesPositionsOffset; + u64 NormalsPositionsOffset; + u64 UVsPositionsOffset; + + u64 BoneIndicesOffset; + u64 WeightsOffset; + padding[8]; + + u64; + padding[8]; + + Positions positions[numberVertices] @VerticesPositionsOffset; + Normals normals[numberVertices] @NormalsPositionsOffset; + UVs uvs[numberVertices] @UVsPositionsOffset; + + BoneIndices b_index[numberVertices] @BoneIndicesOffset; + Weights weights[numberVertices] @WeightsOffset; + + +}; + + +struct Hierarchy { + u8 hierarchy; +}; + +struct HierarchyOrder { + u8 hierarchyorder; +}; + +struct Unknown { + u8; +}; + +struct Transform { + float x; + float y; + float z; + float length; // sqrt(x*x + y*y + z*z) + padding[16]; +}; + +struct Skeleton{ + u32 hierarchyOffset; + u32 hierarchyOrderOffset; + u32 unknownOffset; + u32 transformsOffset; +}; + + + + + +ModelHeader modelheader @ 0x00; +ObjectInfo objects_info[modelheader.objectCount] @ 0x40; + +u32 objectOffset; + +struct Object { + u64 i = std::core::array_index(); + if (modelheader.ID == "SCM ") { + objectOffset = objects_info[0].meshOffset; + MeshSCM meshscm[objects_info[i].meshCount] @ objects_info[i].meshOffset; + + + } else { + objectOffset = objects_info[0].meshOffset; + Mesh mesh[objects_info[i].meshCount] @ objects_info[i].meshOffset; + } +}; + +Object objects[modelheader.objectCount] @objectOffset; + +Skeleton skeleton @modelheader.skeletonOffset; + +Hierarchy hierarchy[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.hierarchyOffset); + +HierarchyOrder hierarchyorder[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.hierarchyOrderOffset); + +Unknown unknown[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.unknownOffset); + +Transform transform[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.transformsOffset); From bc35349e0fd13a9eea0dddad8cf5d9957844c76e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Tamio=C5=82=C5=82o?= Date: Wed, 9 Jul 2025 00:21:07 +0200 Subject: [PATCH 13/93] PEF - fix export count calculation (#418) * Initial version of PEF * add pef test file * Fixed export hash slot count calculation --------- Co-authored-by: paxcut <53811119+paxcut@users.noreply.github.com> --- patterns/pef.hexpat | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/patterns/pef.hexpat b/patterns/pef.hexpat index 6d2b27d9..73e96143 100644 --- a/patterns/pef.hexpat +++ b/patterns/pef.hexpat @@ -1,11 +1,11 @@ -#pragma author Dominik Tamiollo +#pragma author domin568 #pragma description Preffered Executable Format executable (for Mac OS 7.1.2 - Mac OS 10.4 / BeOS) #pragma endian big #pragma pattern_limit 1000000 -#pragma magic [4A 6F 79 21] @ 0x00 import std.core; import std.sys; +import std.math; struct PEFContainerHeader { @@ -435,7 +435,7 @@ u32 relocBegin = loaderOffset + loaderHeader.relocInstrOffset; u32 relocSize = loaderOffset + loaderHeader.loaderStringsOffset - relocBegin; RelocInstruction relocationInstructions[while(($-relocBegin) < relocSize)] @ relocBegin; -u32 exportSlotNum = 2 * loaderHeader.exportHashTablePower; // 2 ^ loaderHeader.exportHashTablePower +u32 exportSlotNum = std::math::pow(2, loaderHeader.exportHashTablePower); u32 exportDataOffset = loaderOffset + loaderHeader.exportHashOffset; u32 exportSymbolKeysOffset = exportDataOffset + sizeof(PEFExportedSymbolHashSlot) * exportSlotNum; u32 exportedSymbolsOffset = exportSymbolKeysOffset + sizeof(PEFExportedSymbolKey) * loaderHeader.exportedSymbolCount; From 44717e9b195777d933796d93944b8cdc9396faf6 Mon Sep 17 00:00:00 2001 From: paxcut <53811119+paxcut@users.noreply.github.com> Date: Fri, 11 Jul 2025 02:25:02 -0700 Subject: [PATCH 14/93] fix: incorrect names and values for some colormap entries of the text editor. (#419) fix: incorrect names and values for some colormap entries of the test editor. Some names were changed from the original that need to be changed back to make the old colormaps compatible with the new system. Also, the colors of the console text were incorrectly set to be equal to each other. This Pr brings back the old names and hopefully fixes problems of incorrect color being used --- themes/catppuccin-frappe.json | 14 +++++++------- themes/catppuccin-latte.json | 14 +++++++------- themes/catppuccin-macchiato.json | 14 +++++++------- themes/catppuccin-mocha.json | 14 +++++++------- themes/solarized_dark.json | 16 ++++++++-------- themes/vs_dark.json | 16 ++++++++-------- 6 files changed, 44 insertions(+), 44 deletions(-) diff --git a/themes/catppuccin-frappe.json b/themes/catppuccin-frappe.json index 7ff19966..46c8bbdc 100644 --- a/themes/catppuccin-frappe.json +++ b/themes/catppuccin-frappe.json @@ -143,8 +143,8 @@ "error-marker": "#E782847F", "unknown-identifier": "#E782847F", "error-text": "#E782847F", - "debug-text": "#E782847F", - "warning-text": "#E782847F", + "debug-text": "#737994FF", + "warning-text": "#A5ADCEFF", "pattern-variable": "#949CBBFF", "function-parameter": "#949CBBFF", "function-variable": "#949CBBFF", @@ -160,13 +160,13 @@ "typedef": "#949CBBFF", "user-defined-type": "#949CBBFF", "keyword": "#CA9EE6FF", - "builtin-type": "#E78284FF", + "known-identifier": "#E78284FF", "line-number": "#838BA7FF", - "block-comment": "#737994FF", + "multi-line--comment": "#737994FF", "number": "#EF9F76FF", - "preprocessor-identifier": "#BABBF1FF", - "directive": "#A5ADCEFF", - "operator": "#C6D0F5FF", + "preproc-identifier": "#BABBF1FF", + "preprocessor": "#A5ADCEFF", + "punctuation": "#C6D0F5FF", "separator": "#C6D0F5FF", "selection": "#6268807F", "string": "#A6D189FF" diff --git a/themes/catppuccin-latte.json b/themes/catppuccin-latte.json index 29e66922..af4f42f5 100644 --- a/themes/catppuccin-latte.json +++ b/themes/catppuccin-latte.json @@ -143,8 +143,8 @@ "error-marker": "#D20F397F", "unknown-identifier": "#D20F397F", "error-text": "#D20F397F", - "debug-text": "#D20F397F", - "warning-text": "#D20F397F", + "debug-text": "#9CA0B0FF", + "warning-text": "#6C6F85FF", "pattern-variable": "#7C7F93FF", "function-parameter": "#7C7F93FF", "function-variable": "#7C7F93FF", @@ -160,13 +160,13 @@ "typedef": "#7C7F93FF", "user-defined-type": "#7C7F93FF", "keyword": "#8839EFFF", - "builtin-type": "#D20F39FF", + "known=identifier": "#D20F39FF", "line-number": "#8C8FA1FF", - "block-comment": "#9CA0B0FF", + "multi-line-comment": "#9CA0B0FF", "number": "#FE640BFF", - "preprocessor-identifier": "#7287FDFF", - "directive": "#6C6F85FF", - "operator": "#4C4F69FF", + "preproc-identifier": "#7287FDFF", + "preprocessor": "#6C6F85FF", + "punctuation": "#4C4F69FF", "separator": "#4C4F69FF", "selection": "#ACB0BE7F", "string": "#40A02BFF" diff --git a/themes/catppuccin-macchiato.json b/themes/catppuccin-macchiato.json index e056cc22..f2886b05 100644 --- a/themes/catppuccin-macchiato.json +++ b/themes/catppuccin-macchiato.json @@ -143,8 +143,8 @@ "error-marker": "#ED87967F", "unknown-identifier": "#ED87967F", "error-text": "#ED87967F", - "debug-text": "#ED87967F", - "warning-text": "#ED87967F", + "debug-text": "#6E738DFF", + "warning-text": "#A5ADCBFF", "pattern-variable": "#939AB7FF", "function-parameter": "#939AB7FF", "function-variable": "#939AB7FF", @@ -160,13 +160,13 @@ "typedef": "#939AB7FF", "user-defined-type": "#939AB7FF", "keyword": "#C6A0F6FF", - "builtin-type": "#ED8796FF", + "known-identifier": "#ED8796FF", "line-number": "#8087A2FF", - "block-comment": "#6E738DFF", + "multi-line-comment": "#6E738DFF", "number": "#F5A97FFF", - "preprocessor-identifier": "#B7BDF8FF", - "directive": "#A5ADCBFF", - "operator": "#CAD3F5FF", + "preproc-identifier": "#B7BDF8FF", + "preprocessor": "#A5ADCBFF", + "punctuation": "#CAD3F5FF", "separator": "#CAD3F5FF", "selection": "#5B60787F", "string": "#A6DA95FF" diff --git a/themes/catppuccin-mocha.json b/themes/catppuccin-mocha.json index 67652505..4bddd267 100644 --- a/themes/catppuccin-mocha.json +++ b/themes/catppuccin-mocha.json @@ -143,8 +143,8 @@ "error-marker": "#F38BA87F", "unknown-identifier": "#F38BA87F", "error-text": "#F38BA87F", - "debug-text": "#F38BA87F", - "warning-text": "#F38BA87F", + "debug-text": "#6C7086FF", + "warning-text": "#A6ADC8FF", "pattern-variable": "#9399B2FF", "function-parameter": "#9399B2FF", "function-variable": "#9399B2FF", @@ -160,13 +160,13 @@ "typedef": "#9399B2FF", "user-defined-type": "#9399B2FF", "keyword": "#CBA6F7FF", - "builtin-type": "#F38BA8FF", + "known-identifier": "#F38BA8FF", "line-number": "#7F849CFF", - "block-comment": "#6C7086FF", + "multi-line-comment": "#6C7086FF", "number": "#FAB387FF", - "preprocessor-identifier": "#B4BEFEFF", - "directive": "#A6ADC8FF", - "operator": "#CDD6F4FF", + "preproc-identifier": "#B4BEFEFF", + "preprocessor": "#A6ADC8FF", + "punctuation": "#CDD6F4FF", "separator": "#CDD6F4FF", "selection": "#585B707F", "string": "#A6E3A1FF" diff --git a/themes/solarized_dark.json b/themes/solarized_dark.json index 9b075646..9c5ae3a5 100644 --- a/themes/solarized_dark.json +++ b/themes/solarized_dark.json @@ -143,8 +143,8 @@ "error-marker": "#FF200080", "unknown-identifier": "#FF200080", "error-text": "#FF200080", - "debug-text": "#FF200080", - "warning-text": "#FF200080", + "debug-text": "#206020FF", + "warning-text": "#808040FF", "identifier": "#AAAAAAFF", "pattern-variable": "#AAAAAAFF", "function-parameter": "#AAAAAAFF", @@ -161,14 +161,14 @@ "typedef": "#AAAAAAFF", "user-defined-type": "#AAAAAAFF", "keyword": "#569CD6FF", - "builtin-type": "#4DC69BFF", + "known-identifier": "#4DC69BFF", "line-number": "#007070FF", - "block-comment": "#206040FF", + "multi-line-comment": "#206040FF", "number": "#00FF00FF", - "preprocessor-identifier": "#A040C0FF", - "directive": "#808040FF", - "operator": "#FFFFFFFF", - "separator": "#FFFFFFFF", + "preproc-identifier": "#A040C0FF", + "preprocessor": "#808040FF", + "punctuation": "#7F7F7FFF", + "separator": "#7F7F7FFF", "selection": "#2060A080", "string": "#E07070FF" } diff --git a/themes/vs_dark.json b/themes/vs_dark.json index 92b41bf0..bf028663 100644 --- a/themes/vs_dark.json +++ b/themes/vs_dark.json @@ -145,8 +145,8 @@ "error-marker": "#FF200080", "unknown-identifier": "#FF200080", "error-text": "#FF200080", - "debug-text": "#FF200080", - "warning-text": "#FF200080", + "debug-text": "#206020FF", + "warning-text": "#808040FF", "pattern-variable": "#AAAAAAFF", "function-parameter": "#AAAAAAFF", "function-variable": "#AAAAAAFF", @@ -162,14 +162,14 @@ "typedef": "#AAAAAAFF", "user-defined-type": "#AAAAAAFF", "keyword": "#569CD6FF", - "builtin-type": "#4DC69BFF", + "known-identifier": "#4DC69BFF", "line-number": "#007070FF", - "block-comment": "#206040FF", + "multi-line-comment": "#206040FF", "number": "#00FF00FF", - "preprocessor-identifier": "#A040C0FF", - "directive": "#808040FF", - "operator": "#FFFFFFFF", - "separator": "#FFFFFFFF", + "preproc-identifier": "#A040C0FF", + "preprocessor": "#808040FF", + "punctuation": "#7F7F7FFF", + "separator": "#7F7F7FFF", "selection": "#2060A080", "string": "#E07070FF" } From 5c6cb9dcccd778a59615bb966864aa49713a0638 Mon Sep 17 00:00:00 2001 From: paxcut <53811119+paxcut@users.noreply.github.com> Date: Fri, 11 Jul 2025 03:42:45 -0700 Subject: [PATCH 15/93] fix: issues with two remaining themes. (#420) Both one-dark and nocturne used global-doc-comment instead of the correctly named doc-global-comment. Typos in Nocturned didn't prevent the theme from loading, but left annoying messages in the log. As explained by the author, the base color map must be one of the three that imhex loads by default, dark,light and/or classic. Even if that is not the case, the name given in base must be that of a valid color map that has been loaded prior to the ones that use it as a base. --- themes/Nocturne.json | 6 +++--- themes/one_dark.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/themes/Nocturne.json b/themes/Nocturne.json index 253ea5fe..9db4d5ba 100644 --- a/themes/Nocturne.json +++ b/themes/Nocturne.json @@ -1,6 +1,6 @@ { - "base": "Nocturne (Dark)", - "name": "Nocture", + "base": "Dark", + "name": "Nocturne", "image_theme": "dark", "authors": "JeanxPereira", "description": "minimal dark theme based on Dracula but more darker", @@ -162,7 +162,7 @@ "default": "#7F7F7FFF", "doc-comment": "#206020FF", "error-marker": "#D83545FF", - "global-doc-comment": "#2B9C89FF", + "doc-global-comment": "#2B9C89FF", "identifier": "#AFAFAFFF", "keyword": "#F87AC4FF", "known-identifier": "#6CF982FF", diff --git a/themes/one_dark.json b/themes/one_dark.json index 545c904c..11c16f67 100644 --- a/themes/one_dark.json +++ b/themes/one_dark.json @@ -158,7 +158,7 @@ "default": "#DADADAFF", "doc-comment": "#206858FF", "error-marker": "#C01A2BFF", - "global-doc-comment": "#208070FF", + "doc-global-comment": "#208070FF", "identifier": "#E4E4E4FB", "keyword": "#C46DE6FF", "known-identifier": "#C46DE6FF", From f4f004f0eba509cf4eb557b89e8579c26541000e Mon Sep 17 00:00:00 2001 From: paxcut <53811119+paxcut@users.noreply.github.com> Date: Wed, 16 Jul 2025 07:01:43 -0700 Subject: [PATCH 16/93] improv: Added new text editor semantic highlighting themes. (#422) * improv: Added new text editor semantic highlighting themes. Current themes use the same colors for all identifier types. These two new themes are just examples of what the new feature makes possible. One aims to simulate CLion Darcula theme and the other is an extension of the original dark theme. * fix: corrected entries to readme file * fix: corrected base for the theme Made sure readme looks correctly on fork and links go to files. --- README.md | 2 + themes/retina_dark.json | 364 ++++++++++++++++++++++++++++++++++++++++ themes/theme_lion.json | 364 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 730 insertions(+) create mode 100644 themes/retina_dark.json create mode 100644 themes/theme_lion.json diff --git a/README.md b/README.md index 1f352a42..6b6b8210 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,8 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | Catppuccin Frappe | [`themes/catppuccin-frappe.json`](themes/catppuccin-frappe.json) | Catppuccin Frappe Flavor (Dark Theme) | | Catppuccin Macchiato | [`themes/catppuccin-macchiato.json`](themes/catppuccin-macchiato.json) | Catppuccin Macchiato Flavor (Dark Theme) | | Catppuccin Mocha | [`themes/catppuccin-mocha.json`](themes/catppuccin-mocha.json) | Catppuccin Mocha Flavor (Dark Theme) | +| Theme Lion | [`themes/theme_lion.json`](themes/theme_lion.json) | Semantic CLion inspired theme (Dark Theme) | +| Retina Dark | [`themes/retina_dark.json`](themes/retina_dark.json) | Semantic theme based on Dark Theme | ### Disassemblers diff --git a/themes/retina_dark.json b/themes/retina_dark.json new file mode 100644 index 00000000..5aa8ad49 --- /dev/null +++ b/themes/retina_dark.json @@ -0,0 +1,364 @@ +{ + "base": "Dark", + "colors": { + "imgui": { + "border": "#6D6D7F7F", + "border-shadow": "#00000000", + "button": "#4296F966", + "button-active": "#0F87F9FF", + "button-hovered": "#4296F9FF", + "check-mark": "#4296F9FF", + "child-background": "#00000000", + "docking-empty-background": "#0F0F0FEF", + "docking-preview": "#4296F9B2", + "drag-drop-target": "#FFFF00E5", + "frame-background": "#28497A89", + "frame-background-active": "#4296F9AA", + "frame-background-hovered": "#4296F966", + "header": "#4296F94F", + "header-active": "#4296F9FF", + "header-hovered": "#4296F9CC", + "menu-bar-background": "#232323FF", + "modal-window-dim-background": "#CCCCCC59", + "nav-highlight": "#4296F9FF", + "nav-windowing-background": "#CCCCCC33", + "nav-windowing-highlight": "#FFFFFFB2", + "plot-histogram": "#E5B200FF", + "plot-histogram-hovered": "#FF9900FF", + "plot-lines": "#9B9B9BFF", + "plot-lines-hovered": "#FF6D59FF", + "popup-background": "#141414FF", + "resize-grip": "#4296F933", + "resize-grip-active": "#4296F9F2", + "resize-grip-hovered": "#4296F9AA", + "scrollbar-background": "#05050587", + "scrollbar-grab": "#4F4F4FFF", + "scrollbar-grab-active": "#828282FF", + "scrollbar-grab-hovered": "#686868FF", + "separator": "#6D6D7F7F", + "separator-active": "#1966BFFF", + "separator-hovered": "#1966BFC6", + "slider-grab": "#3D84E0FF", + "slider-grab-active": "#4296F9FF", + "tab": "#2D5993DB", + "tab-active": "#3268ADFF", + "tab-hovered": "#4296F9CC", + "tab-unfocused": "#111A25F7", + "tab-unfocused-active": "#22426CFF", + "table-border-light": "#3A3A3FFF", + "table-border-strong": "#4F4F59FF", + "table-header-background": "#303033FF", + "table-row-background": "#00000000", + "table-row-background-alt": "#FFFFFF0F", + "text": "#FFFFFFFF", + "text-disabled": "#7F7F7FFF", + "text-selected-background": "#4296F959", + "title-background": "#232323FF", + "title-background-active": "#232323FF", + "title-background-collapse": "#232323FF", + "window-background": "#0F0F0FFF", + "window-shadow": "#000000FF" + }, + "imhex": { + "IEEE-tool-exp": "#5D7F5DFF", + "IEEE-tool-mantissa": "#7F5D5DFF", + "IEEE-tool-sign": "#5D5D7FFF", + "achievement-unlocked": "#F1C40FFF", + "advanced-encoding-ascii": "#06539BFF", + "advanced-encoding-multi": "#F1C40FFF", + "advanced-encoding-single": "#E74C3CFF", + "advanced-encoding-unknown": "#E74C3CFF", + "blur-background": "#00000000", + "desc-button": "#141414FF", + "desc-button-active": "#3C3C3CFF", + "desc-button-hovered": "#282828FF", + "diff-added": "#388B42FF", + "diff-changed": "#F1C40FFF", + "diff-removed": "#E74C3CFF", + "find-highlight": "#672A78FF", + "highlight": "#4DC69BFF", + "logger-debug": "#388B42FF", + "logger-error": "#E74C3CFF", + "logger-fatal": "#672A78FF", + "logger-info": "#06539BFF", + "logger-warning": "#F1C40FFF", + "patches": "#E74C3CFF", + "pattern-selected": "#3683CBFF", + "toolbar-blue": "#06539BFF", + "toolbar-brown": "#DBB377FF", + "toolbar-gray": "#E6E6E6FF", + "toolbar-green": "#388B42FF", + "toolbar-purple": "#672A78FF", + "toolbar-red": "#E74C3CFF", + "toolbar-yellow": "#F1C40FFF" + }, + "imnodes": { + "box-selector": "#3D85E01E", + "box-selector-outline": "#3D85E096", + "grid-background": "#282832C8", + "grid-line": "#C8C8C828", + "grid-line-primary": "#F0F0F03C", + "link": "#3D85E0C8", + "link-hovered": "#4296FAFF", + "link-selected": "#4296FAFF", + "mini-map-background": "#19191996", + "mini-map-background-hovered": "#191919C8", + "mini-map-canvas": "#C8C8C819", + "mini-map-canvas-outline": "#C8C8C8C8", + "mini-map-link": "#3D85E0C8", + "mini-map-link-selected": "#4296FAFF", + "mini-map-node-background": "#C8C8C864", + "mini-map-node-background-hovered": "#C8C8C8FF", + "mini-map-node-background-selected": "#C8C8C8FF", + "mini-map-node-outline": "#C8C8C864", + "mini-map-outline": "#96969664", + "mini-map-outline-hovered": "#969696C8", + "node-background": "#323232FF", + "node-background-hovered": "#4B4B4BFF", + "node-background-selected": "#4B4B4BFF", + "node-outline": "#646464FF", + "pin": "#F5CB25FF", + "pin-hovered": "#FA8335FF", + "title-bar": "#294A7AFF", + "title-bar-hovered": "#4296FAFF", + "title-bar-selected": "#4296FAFF" + }, + "implot": { + "axis-bg": "#00000000", + "axis-bg-active": "#00000000", + "axis-bg-hovered": "#00000000", + "axis-grid": "#FFFFFF3F", + "axis-text": "#FFFFFFFF", + "axis-tick": "#00000000", + "crosshairs": "#FFFFFF7F", + "error-bar": "#00000000", + "fill": "#00000000", + "frame-bg": "#FFFFFF11", + "inlay-text": "#FFFFFFFF", + "legend-bg": "#141414EF", + "legend-border": "#6D6D7F7F", + "legend-text": "#FFFFFFFF", + "line": "#00000000", + "marker-fill": "#00000000", + "marker-outline": "#00000000", + "plot-bg": "#0000007F", + "plot-border": "#6D6D7F7F", + "selection": "#FF9900FF", + "title-text": "#FFFFFFFF" + }, + "text-editor": { + "attribute": "#C09494FF", + "background": "#101010FF", + "breakpoint": "#89140340", + "calculated-pointer": "#AAAAAAFF", + "char-literal": "#936848FF", + "comment": "#206020FF", + "current-line-edge": "#A0A0A040", + "current-line-fill": "#00000040", + "current-line-fill-inactive": "#80808040", + "cursor": "#E0E0E0FF", + "debug-text": "#8A8A8AFF", + "default": "#7F7F7FFF", + "default-text": "#7F7F7FFF", + "doc-block-comment": "#73B473FF", + "doc-comment": "#406020FF", + "doc-global-comment": "#506050FF", + "error-marker": "#90140280", + "error-text": "#96130080", + "function": "#C3C283FF", + "function-parameter": "#DADADAFF", + "function-variable": "#B7B7B7FF", + "global-variable": "#CECECEFF", + "identifier": "#AAAAAAFF", + "keyword": "#4880AFFF", + "known-identifier": "#45B28CFF", + "line-number": "#02A7A7FF", + "local-variable": "#C1C1C1FF", + "multi-line-comment": "#206040FF", + "namespace": "#A96226FF", + "number": "#287928FF", + "pattern-variable": "#D3D3D3FF", + "placed-variable": "#C8C6C6FF", + "preproc-identifier": "#BCB999FF", + "preprocessor": "#C0C04CFF", + "preprocessor-deactivated": "#4F4F4F45", + "punctuation": "#B6B6B6FF", + "selection": "#2060A080", + "separator": "#C4C4C4FF", + "string": "#7E3D3DFF", + "template-variable": "#7C7C7CFF", + "typedef": "#75C34CFF", + "unknown-identifier": "#BC2323FE", + "user-defined-type": "#73BCB2FF", + "view": "#7D7D7DFF", + "warning-text": "#A9A906FF" + } + }, + "image_theme": "dark", + "name": "Retina Dark", + "styles": { + "imgui": { + "alpha": 1.0, + "button-text-align": [ + 0.5, + 0.5 + ], + "cell-padding": [ + 4.0, + 2.0 + ], + "child-border-size": 1.0, + "child-rounding": 0.0, + "disabled-alpha": 0.6000000238418579, + "frame-border-size": 0.0, + "frame-padding": [ + 4.0, + 3.0 + ], + "frame-rounding": 0.0, + "grab-min-size": 12.0, + "grab-rounding": 0.0, + "indent-spacing": 10.0, + "item-inner-spacing": [ + 4.0, + 4.0 + ], + "item-spacing": [ + 8.0, + 4.0 + ], + "popup-border-size": 1.0, + "popup-rounding": 0.0, + "scrollbar-rounding": 9.0, + "scrollbar-size": 14.0, + "selectable-text-align": [ + 0.0, + 0.0 + ], + "tab-rounding": 4.0, + "window-border-size": 1.0, + "window-min-size": [ + 32.0, + 32.0 + ], + "window-padding": [ + 8.0, + 8.0 + ], + "window-rounding": 0.0, + "window-shadow-angle": 0.0, + "window-shadow-offset": 0.0, + "window-shadow-size": 64.0, + "window-title-align": [ + 0.0, + 0.5 + ] + }, + "imhex": { + "popup-alpha": 0.6499999761581421, + "window-blur": 0.0 + }, + "imnodes": { + "grid-spacing": 24.0, + "link-hover-distance": 10.0, + "link-line-segments-per-length": 0.10000000149011612, + "link-thickness": 3.0, + "mini-map-offset": [ + 4.0, + 4.0 + ], + "mini-map-padding": [ + 8.0, + 8.0 + ], + "node-border-thickness": 1.0, + "node-corner-rounding": 4.0, + "node-padding": [ + 8.0, + 8.0 + ], + "pin-circle-radius": 4.0, + "pin-hover-radius": 10.0, + "pin-line-thickness": 1.0, + "pin-offset": 0.0, + "pin-quad-side-length": 7.0, + "pin-triangle-side-length": 9.5 + }, + "implot": { + "annotation-padding": [ + 2.0, + 2.0 + ], + "digital-bit-gap": 4.0, + "digital-bit-height": 8.0, + "error-bar-size": 5.0, + "error-bar-weight": 1.5, + "fill-alpha": 1.0, + "fit-padding": [ + 0.0, + 0.0 + ], + "label-padding": [ + 5.0, + 5.0 + ], + "legend-inner-padding": [ + 5.0, + 5.0 + ], + "legend-padding": [ + 10.0, + 10.0 + ], + "legend-spacing": [ + 5.0, + 0.0 + ], + "line-weight": 1.0, + "major-grid-size": [ + 1.0, + 1.0 + ], + "major-tick-len": [ + 10.0, + 10.0 + ], + "major-tick-size": [ + 1.0, + 1.0 + ], + "marker-size": 4.0, + "marker-weight": 1.0, + "minor-alpha": 0.25, + "minor-grid-size": [ + 1.0, + 1.0 + ], + "minor-tick-len": [ + 5.0, + 5.0 + ], + "minor-tick-size": [ + 1.0, + 1.0 + ], + "mouse-pos-padding": [ + 10.0, + 10.0 + ], + "plot-border-size": 1.0, + "plot-default-size": [ + 400.0, + 300.0 + ], + "plot-min-size": [ + 200.0, + 150.0 + ], + "plot-padding": [ + 10.0, + 10.0 + ] + } + } +} \ No newline at end of file diff --git a/themes/theme_lion.json b/themes/theme_lion.json new file mode 100644 index 00000000..875cd1a2 --- /dev/null +++ b/themes/theme_lion.json @@ -0,0 +1,364 @@ +{ + "base": "Dark", + "colors": { + "imgui": { + "border": "#6D6D7F7F", + "border-shadow": "#2B2B2B00", + "button": "#21BEA866", + "button-active": "#0FF9E8FF", + "button-hovered": "#25BAB2FF", + "check-mark": "#1AC3BAFF", + "child-background": "#2B2B2B00", + "docking-empty-background": "#2B2B2BEF", + "docking-preview": "#108691B2", + "drag-drop-target": "#FFFF00E5", + "frame-background": "#28717A89", + "frame-background-active": "#24AAB6AA", + "frame-background-hovered": "#10B3B566", + "header": "#20999F4F", + "header-active": "#269DA5FF", + "header-hovered": "#33A2AECC", + "menu-bar-background": "#313537FF", + "modal-window-dim-background": "#CCCCCC59", + "nav-highlight": "#42F9E7FF", + "nav-windowing-background": "#CCCCCC33", + "nav-windowing-highlight": "#FFFFFFB2", + "plot-histogram": "#E5B200FF", + "plot-histogram-hovered": "#FF9900FF", + "plot-lines": "#9B9B9BFF", + "plot-lines-hovered": "#FF6D59FF", + "popup-background": "#3B3F41FF", + "resize-grip": "#42F9D433", + "resize-grip-active": "#42F9E4F2", + "resize-grip-hovered": "#42F9E7AA", + "scrollbar-background": "#2B2B2B87", + "scrollbar-grab": "#4F4F4FFF", + "scrollbar-grab-active": "#828282FF", + "scrollbar-grab-hovered": "#686868FF", + "separator": "#6D747F7F", + "separator-active": "#19BFA4FF", + "separator-hovered": "#19BFACC6", + "slider-grab": "#2C8990FF", + "slider-grab-active": "#267C83FF", + "tab": "#2D8F93DB", + "tab-active": "#158891FF", + "tab-hovered": "#36A3C1CC", + "tab-unfocused": "#1E2D31F7", + "tab-unfocused-active": "#226C6CFF", + "table-border-light": "#3A3A3FFF", + "table-border-strong": "#4F4F59FF", + "table-header-background": "#2B2B2BFF", + "table-row-background": "#2B2B2B6A", + "table-row-background-alt": "#FFFFFF0F", + "text": "#A9B7C6FF", + "text-disabled": "#7F7F7FFF", + "text-selected-background": "#19B8A259", + "title-background": "#3C3F41FF", + "title-background-active": "#232323FF", + "title-background-collapse": "#232323FF", + "window-background": "#2B2B2BFF", + "window-shadow": "#1B1B1BFF" + }, + "imhex": { + "IEEE-tool-exp": "#5D7F5DFF", + "IEEE-tool-mantissa": "#7F5D5DFF", + "IEEE-tool-sign": "#5D5D7FFF", + "achievement-unlocked": "#F1C40FFF", + "advanced-encoding-ascii": "#06539BFF", + "advanced-encoding-multi": "#F1C40FFF", + "advanced-encoding-single": "#E74C3CFF", + "advanced-encoding-unknown": "#E74C3CFF", + "blur-background": "#22B2B2B0", + "desc-button": "#2B2B2BFF", + "desc-button-active": "#3C3C3CFF", + "desc-button-hovered": "#4B4B4BFF", + "diff-added": "#388B42FF", + "diff-changed": "#F1C40FFF", + "diff-removed": "#E74C3CFF", + "find-highlight": "#672A78FF", + "highlight": "#4DC69BFF", + "logger-debug": "#388B42FF", + "logger-error": "#E74C3CFF", + "logger-fatal": "#672A78FF", + "logger-info": "#06539BFF", + "logger-warning": "#F1C40FFF", + "patches": "#E74C3CFF", + "pattern-selected": "#06539BFF", + "toolbar-blue": "#06539BFF", + "toolbar-brown": "#DBB377FF", + "toolbar-gray": "#E6E6E6FF", + "toolbar-green": "#388B42FF", + "toolbar-purple": "#672A78FF", + "toolbar-red": "#E74C3CFF", + "toolbar-yellow": "#F1C40FFF" + }, + "imnodes": { + "box-selector": "#3D78E01E", + "box-selector-outline": "#3D78E096", + "grid-background": "#282832C8", + "grid-line": "#C8C8C828", + "grid-line-primary": "#F0F0F03C", + "link": "#3D85E0C8", + "link-hovered": "#4285FAFF", + "link-selected": "#4285FAFF", + "mini-map-background": "#19191996", + "mini-map-background-hovered": "#191919C8", + "mini-map-canvas": "#C8C8C819", + "mini-map-canvas-outline": "#C8C8C8C8", + "mini-map-link": "#3D85E0C8", + "mini-map-link-selected": "#4285FAFF", + "mini-map-node-background": "#C8C8C864", + "mini-map-node-background-hovered": "#C8C8C8FF", + "mini-map-node-background-selected": "#C8C8C8FF", + "mini-map-node-outline": "#C8C8C864", + "mini-map-outline": "#96969664", + "mini-map-outline-hovered": "#969696C8", + "node-background": "#323232FF", + "node-background-hovered": "#4B4B4BFF", + "node-background-selected": "#4B4B4BFF", + "node-outline": "#646464FF", + "pin": "#F5CB25FF", + "pin-hovered": "#FA8335FF", + "title-bar": "#29467AFF", + "title-bar-hovered": "#4285FAFF", + "title-bar-selected": "#4285FAFF" + }, + "implot": { + "axis-bg": "#00000000", + "axis-bg-active": "#00000000", + "axis-bg-hovered": "#00000000", + "axis-grid": "#FFFFFF3F", + "axis-text": "#FFFFFFFF", + "axis-tick": "#00000000", + "crosshairs": "#FFFFFF7F", + "error-bar": "#00000000", + "fill": "#00000000", + "frame-bg": "#FFFFFF11", + "inlay-text": "#FFFFFFFF", + "legend-bg": "#141414EF", + "legend-border": "#6D6D7F7F", + "legend-text": "#FFFFFFFF", + "line": "#00000000", + "marker-fill": "#00000000", + "marker-outline": "#00000000", + "plot-bg": "#0000007F", + "plot-border": "#6D6D7F7F", + "selection": "#FF9900FF", + "title-text": "#FFFFFFFF" + }, + "text-editor": { + "attribute": "#30A090FF", + "background": "#2B2B2BFF", + "multi-line-comment": "#B0B0B0FF", + "breakpoint": "#3A2323FF", + "know-identifier": "#F09A3BFF", + "calculated-pointer": "#F2C7FEFF", + "char-literal": "#38E517FF", + "comment": "#808080FF", + "current-line-edge": "#A0A0A040", + "current-line-fill": "#00000040", + "current-line-fill-inactive": "#80808040", + "cursor": "#E0E0E0FF", + "debug-text": "#8A8A8AFF", + "default": "#7F7F7FFF", + "default-text": "#7F7F7FFF", + "preprocessor": "#BBB529FF", + "doc-block-comment": "#416438FF", + "doc-comment": "#629755FF", + "doc-global-comment": "#74B265FF", + "error-marker": "#FF0C0080", + "error-text": "#FF200080", + "function": "#FFC66DFF", + "function-parameter": "#A9B7C6FF", + "function-variable": "#798CADFF", + "global-variable": "#B8C5BFFF", + "identifier": "#AAAAAAFF", + "keyword": "#CC7832FF", + "line-number": "#676767FF", + "local-variable": "#FF7FCAFF", + "namespace": "#B5B6E3FF", + "number": "#6897BBFF", + "punctutation": "#ABD374FF", + "pattern-variable": "#FCADDCFF", + "placed-variable": "#A9B7C6FF", + "preprocessor-deactivated": "#686A4BB8", + "preproc-identifier": "#908B25FF", + "selection": "#209CA080", + "separator": "#AA9C89FF", + "string": "#6A8759FF", + "template-variable": "#9876AAFF", + "typedef": "#B9BCD1FF", + "unknown-identifier": "#BC3F3CFF", + "user-defined-type": "#B1B2FFFF", + "view": "#C7CAFFFF", + "warning-text": "#FFFF00FF" + } + }, + "image_postfix": "dark", + "name": "Theme Lion", + "styles": { + "imgui": { + "alpha": 1.0, + "button-text-align": [ + 0.5, + 0.5 + ], + "cell-padding": [ + 4.0, + 2.0 + ], + "child-border-size": 1.0, + "child-rounding": 0.0, + "disabled-alpha": 0.6000000238418579, + "frame-border-size": 0.0, + "frame-padding": [ + 4.0, + 3.0 + ], + "frame-rounding": 0.0, + "grab-min-size": 12.0, + "grab-rounding": 0.0, + "indent-spacing": 10.0, + "item-inner-spacing": [ + 4.0, + 4.0 + ], + "item-spacing": [ + 8.0, + 4.0 + ], + "popup-border-size": 1.0, + "popup-rounding": 0.0, + "scrollbar-rounding": 9.0, + "scrollbar-size": 14.0, + "selectable-text-align": [ + 0.0, + 0.0 + ], + "tab-rounding": 4.0, + "window-border-size": 1.0, + "window-min-size": [ + 32.0, + 32.0 + ], + "window-padding": [ + 8.0, + 8.0 + ], + "window-rounding": 0.0, + "window-shadow-angle": 0.0, + "window-shadow-offset": 0.0, + "window-shadow-size": 64.0, + "window-title-align": [ + 0.0, + 0.5 + ] + }, + "imhex": { + "popup-alpha": 0.6499999761581421, + "window-blur": 0.0 + }, + "imnodes": { + "grid-spacing": 24.0, + "link-hover-distance": 10.0, + "link-line-segments-per-length": 0.10000000149011612, + "link-thickness": 3.0, + "mini-map-offset": [ + 4.0, + 4.0 + ], + "mini-map-padding": [ + 8.0, + 8.0 + ], + "node-border-thickness": 1.0, + "node-corner-rounding": 4.0, + "node-padding": [ + 8.0, + 8.0 + ], + "pin-circle-radius": 4.0, + "pin-hover-radius": 10.0, + "pin-line-thickness": 1.0, + "pin-offset": 0.0, + "pin-quad-side-length": 7.0, + "pin-triangle-side-length": 9.5 + }, + "implot": { + "annotation-padding": [ + 2.0, + 2.0 + ], + "digital-bit-gap": 4.0, + "digital-bit-height": 8.0, + "error-bar-size": 5.0, + "error-bar-weight": 1.5, + "fill-alpha": 1.0, + "fit-padding": [ + 0.0, + 0.0 + ], + "label-padding": [ + 5.0, + 5.0 + ], + "legend-inner-padding": [ + 5.0, + 5.0 + ], + "legend-padding": [ + 10.0, + 10.0 + ], + "legend-spacing": [ + 5.0, + 0.0 + ], + "line-weight": 1.0, + "major-grid-size": [ + 1.0, + 1.0 + ], + "major-tick-len": [ + 10.0, + 10.0 + ], + "major-tick-size": [ + 1.0, + 1.0 + ], + "marker-size": 4.0, + "marker-weight": 1.0, + "minor-alpha": 0.25, + "minor-grid-size": [ + 1.0, + 1.0 + ], + "minor-tick-len": [ + 5.0, + 5.0 + ], + "minor-tick-size": [ + 1.0, + 1.0 + ], + "mouse-pos-padding": [ + 10.0, + 10.0 + ], + "plot-border-size": 1.0, + "plot-default-size": [ + 400.0, + 300.0 + ], + "plot-min-size": [ + 200.0, + 150.0 + ], + "plot-padding": [ + 10.0, + 10.0 + ] + } + } +} \ No newline at end of file From 3b2f098b098fc30689b69f12851893c23386c4e0 Mon Sep 17 00:00:00 2001 From: paxcut <53811119+paxcut@users.noreply.github.com> Date: Fri, 18 Jul 2025 08:39:37 -0700 Subject: [PATCH 17/93] fix: fixed two typos in this theme (#423) --- themes/theme_lion.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/themes/theme_lion.json b/themes/theme_lion.json index 875cd1a2..33d2796f 100644 --- a/themes/theme_lion.json +++ b/themes/theme_lion.json @@ -151,7 +151,7 @@ "background": "#2B2B2BFF", "multi-line-comment": "#B0B0B0FF", "breakpoint": "#3A2323FF", - "know-identifier": "#F09A3BFF", + "known-identifier": "#F09A3BFF", "calculated-pointer": "#F2C7FEFF", "char-literal": "#38E517FF", "comment": "#808080FF", @@ -178,7 +178,7 @@ "local-variable": "#FF7FCAFF", "namespace": "#B5B6E3FF", "number": "#6897BBFF", - "punctutation": "#ABD374FF", + "punctuation": "#ABD374FF", "pattern-variable": "#FCADDCFF", "placed-variable": "#A9B7C6FF", "preprocessor-deactivated": "#686A4BB8", From d3b05fd7537d7c0e7d1cc75f311c6c38b9de2a00 Mon Sep 17 00:00:00 2001 From: WerWolv Date: Tue, 29 Jul 2025 21:38:20 +0200 Subject: [PATCH 18/93] git: Allow test action to be imported by other projects --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 62bc2f23..0dc54d5b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,6 +7,7 @@ on: branches: [ '*' ] repository_dispatch: types: [run_tests] + workflow_call: jobs: tests: From 60c8d934499f958721658475c8c30202e4faa629 Mon Sep 17 00:00:00 2001 From: WerWolv Date: Tue, 29 Jul 2025 21:57:54 +0200 Subject: [PATCH 19/93] git: Make sure other repos check out the right repo --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0dc54d5b..9931f239 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,7 @@ jobs: - name: 🧰 Checkout uses: actions/checkout@v4 with: + repository: WerWolv/ImHex-Patterns submodules: recursive - name: ⬇️ Install dependencies From a75a7a5b980a19733383a15789105480dad01911 Mon Sep 17 00:00:00 2001 From: WerWolv Date: Tue, 29 Jul 2025 22:26:27 +0200 Subject: [PATCH 20/93] git: Let the CI still work correctly in PRs --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9931f239..c9a61d28 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: - name: 🧰 Checkout uses: actions/checkout@v4 with: - repository: WerWolv/ImHex-Patterns + repository: ${{ github.repository_owner }}/ImHex-Patterns submodules: recursive - name: ⬇️ Install dependencies From 5ed64f9f08e6af0b9028851d4868a147adf76bb0 Mon Sep 17 00:00:00 2001 From: WerWolv Date: Sat, 2 Aug 2025 19:29:16 +0200 Subject: [PATCH 21/93] tests: Include patterns folder as include path to support `import * from X as Y` #426 --- tests/patterns/source/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/patterns/source/main.cpp b/tests/patterns/source/main.cpp index 7d12f034..0d6abeb7 100644 --- a/tests/patterns/source/main.cpp +++ b/tests/patterns/source/main.cpp @@ -72,7 +72,7 @@ int main(int argc, char **argv) { } runtime.setDangerousFunctionCallHandler([]{ return true; }); - runtime.setIncludePaths({ includePath }); + runtime.setIncludePaths({ includePath, patternFilePath.parent_path() }); runtime.addPragma("MIME", DummyPragmaHandler); runtime.addPragma("description", DescPragmaHandler); runtime.addDefine("__PL_UNIT_TESTS__"); From 9207282bcff35e31dd57bbfecf0acccdc2448d8c Mon Sep 17 00:00:00 2001 From: Stephen Hewitt Date: Sun, 3 Aug 2025 04:13:35 +1000 Subject: [PATCH 22/93] patterns: Added Commodore BASIC (#428) * Commodore BASIC * Update desc * I made it * Implemented suggestion * Implemented suggestion * Test file * Rename file --------- Co-authored-by: Nik --- README.md | 1 + patterns/commodore_basic.hexpat | 158 ++++++++++++++++++ .../test_data/commodore_basic.hexpat.prg | Bin 0 -> 6702 bytes 3 files changed, 159 insertions(+) create mode 100644 patterns/commodore_basic.hexpat create mode 100644 tests/patterns/test_data/commodore_basic.hexpat.prg diff --git a/README.md b/README.md index 6b6b8210..695e3125 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ZLIB | `application/zlib` | [`patterns/zlib.hexpat`](patterns/zlib.hexpat) | ZLIB compressed data format | | ZSTD | `application/zstd` | [`patterns/zstd.hexpat`](patterns/zstd.hexpat) | Zstandard compressed data format | | MOD | `3d-model/mod` | [`patterns/DMC3HD-Mod.hexpat`](patterns/dmc3_hd_mod.hexpat) | 3D Model files used in Devil May Cry 3 HD Collection | +| CBM BASIC | | [`commodore_basic.hexpat`](patterns/commodore_basic.hexpat) | Commodore BASIC | ### Scripts diff --git a/patterns/commodore_basic.hexpat b/patterns/commodore_basic.hexpat new file mode 100644 index 00000000..5f4cf029 --- /dev/null +++ b/patterns/commodore_basic.hexpat @@ -0,0 +1,158 @@ +#pragma description Commodore BASIC +#pragma author Stephen Hewitt + +import std.io; +import std.mem; + +bool in_quotes = false; + +le u16 LoadAddress @0 [[color("ff0000")]]; + +fn formatll(u16 offset) { + if (offset&0xff00 == 0) + return "No next line"; + u16 fo = offset-LoadAddress+2; + return std::format("Next line: ${:04X} (offset ${:04X})", offset, fo); +}; + +enum Token : u8 { + END = 0x80, + FOR = 0x81, + NEXT = 0x82, + DATA = 0x83, + INPUT_ = 0x84, + INPUT = 0x85, + DIM = 0x86, + READ = 0x87, + LET = 0x88, + GOTO = 0x89, + RUN = 0x8a, + IF = 0x8b, + RESTORE = 0x8c, + GOSUB = 0x8d, + RETURN = 0x8e, + REM = 0x8f, + STOP = 0x90, + ON = 0x91, + WAIT = 0x92, + LOAD = 0x93, + SAVE = 0x94, + VERIFY = 0x95, + DEF = 0x96, + POKE = 0x97, + PRINT_ = 0x98, + PRINT = 0x99, + CONT = 0x9a, + LIST = 0x9b, + CLR = 0x9c, + CMD = 0x9d, + SYS = 0x9e, + OPEN = 0x9f, + CLOSE = 0xa0, + GET = 0xa1, + NEW = 0xa2, + TAB_ = 0xa3, + TO = 0xa4, + FN = 0xa5, + SPC_ = 0xa6, + THEN = 0xa7, + NOT = 0xa8, + STEP = 0xa9, + PLUS_ = 0xaa, + MINUS_ = 0xab, + TIMES_ = 0xac, + DIVIDE_ = 0xad, + POW_ = 0xae, + AND = 0xaf, + OR = 0xb0, + GT_ = 0xb1, + EQ_ = 0xb2, + LT_ = 0xb3, + SGN = 0xb4, + INT = 0xb5, + ABS = 0xb6, + USR = 0xb7, + FRE = 0xb7, + POS = 0xb9, + SQR = 0xba, + RND = 0xbb, + LOG = 0xbc, + EXP = 0xbd, + COS = 0xbe, + SIN = 0xbf, + TAN = 0xc0, + ATN = 0xc1, + PEEK = 0xc2, + LEN = 0xc3, + STR_ = 0xc4, + VAL = 0xc5, + ASC = 0xc6, + CHR_ = 0xc7, + LEFT_ = 0xc8, + RIGHT_ = 0xc9, + MID_ = 0xca, + PI_ = 0xff +} [[format("formate")]]; + +// Can't seem to put attributes on enum members. Hack around it. +fn formate(Token t) { + match (t) { + (Token::INPUT_): return "INPUT#"; // $84 + (Token::PRINT_): return "PRINT#"; // $98 + (Token::TAB_): return "TAB("; // $a3 + (Token::SPC_): return "SPC("; // $a6 + (Token::PLUS_): return "+"; // $aa + (Token::MINUS_): return "-"; // $ab + (Token::TIMES_): return "*"; // $ac + (Token::DIVIDE_): return "/"; // $ad + //(Token::POW_): return "↑"; // $ae + (Token::GT_): return ">"; // $b1 + (Token::EQ_): return "="; // $b2 + (Token::LT_): return "<"; // $b3 + (Token::STR_): return "STR$"; // $c4 + (Token::CHR_): return "CHR$"; // $c7 + (Token::LEFT_): return "LEFT$"; // $c8 + (Token::RIGHT_): return "RIGHT$"; // $c9 + (Token::MID_): return "MID$"; // $ca + //(Token::PI_): return "π"; // $ff + } + + return t; +}; + +fn NotZero() { + u8 b = std::mem::read_unsigned($, 1); + return b!=0; +}; + +fn IsToken() { + u8 b = std::mem::read_unsigned($, 1); + return b&0x80!=0; +}; + +fn IsPETSCII() { + u8 b = std::mem::read_unsigned($, 1); + if (b == '"') + in_quotes = !in_quotes; + return (b!=0) && (in_quotes || (b&0x80)==0); +}; + +struct LineSegment +{ + Token tokens[while(IsToken())] [[color("a040a0")]]; + char petscii[while(IsPETSCII())] [[color("a0a0a0")]]; +}; + +struct Line +{ + in_quotes = false; + + u16 next [[color("8080ff"), format("formatll")]]; + if (next&0xff00 == 0) + break; + le u16 line_number [[color("00FF00")]]; + LineSegment contents[while(NotZero())]; + u8 eol [[color("00ffff")]]; +}; + +Line Lines[while(!std::mem::eof())] @ 2; \ No newline at end of file diff --git a/tests/patterns/test_data/commodore_basic.hexpat.prg b/tests/patterns/test_data/commodore_basic.hexpat.prg new file mode 100644 index 0000000000000000000000000000000000000000..9ad2826a250274c935eaef197f30c30dbdb4395d GIT binary patch literal 6702 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 literal 0 HcmV?d00001 From 6b9f39cc2175c573fec2ae703f2c9ee3821f7335 Mon Sep 17 00:00:00 2001 From: Mark Jansen Date: Fri, 15 Aug 2025 17:16:50 +0200 Subject: [PATCH 23/93] patterns: Add SDB pattern (#424) Co-authored-by: Nik --- README.md | 1 + patterns/sdb.hexpat | 87 ++++++++++++++++++++++++ tests/patterns/test_data/sdb.hexpat.sdb | Bin 0 -> 260 bytes 3 files changed, 88 insertions(+) create mode 100644 patterns/sdb.hexpat create mode 100644 tests/patterns/test_data/sdb.hexpat.sdb diff --git a/README.md b/README.md index 695e3125..6f0cc418 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ReFS | | [`patterns/refs.hexpat`](patterns/fs/refs.hexpat) | Microsoft Resilient File System | | RGBDS | | [`patterns/rgbds.hexpat`](patterns/rgbds.hexpat) | [RGBDS](https://rgbds.gbdev.io) object file format | | RPM | | [`patterns/rpm.hexpat`](patterns/rpm.hexpat) | [RPM](http://ftp.rpm.org/max-rpm/s1-rpm-file-format-rpm-file-format.html) package file format | +| SDB | | [`patterns/sdb.hexpat`](patterns/sdb.hexpat) | [Shim DataBase](https://learn.microsoft.com/en-us/windows/win32/devnotes/application-compatibility-database) file format | | Shell Link | `application/x-ms-shortcut` | [`patterns/lnk.hexpat`](patterns/lnk.hexpat) | Windows Shell Link file format | | shp | | [`patterns/shp.hexpat`](patterns/shp.hexpat) | ESRI shape file | | shx | | [`patterns/shx.hexpat`](patterns/shx.hexpat) | ESRI index file | diff --git a/patterns/sdb.hexpat b/patterns/sdb.hexpat new file mode 100644 index 00000000..e097c3cf --- /dev/null +++ b/patterns/sdb.hexpat @@ -0,0 +1,87 @@ +#pragma description SDB File (Shim DataBase file) +#pragma author learn_more +#pragma magic [ 73 64 62 66 ] @ 0x08 +#pragma endian little + +import std.mem; +import std.io; +import std.string; +import type.base; + +enum TagType : u8 { + Null = 0x1, + Byte = 0x2, + Word = 0x3, + DWord = 0x4, + QWord = 0x5, + StringRef = 0x6, + List = 0x7, + String = 0x8, + Binary = 0x9, +}; + +bitfield TypeId { + id : 12; + type : 4; +}; + +fn get_tag(TypeId tag) { + return std::format("{:x}{:03x}", tag.type, tag.id); +}; + +fn get_type(TypeId tag) { + TagType tmp = tag.type; + str name = std::format("{}", tmp); + u32 len = std::string::length(name); + return std::string::substr(name, 9, len-9); +}; + +struct Tag { + TypeId tag[[format_read("get_tag")]]; + match (tag.type) { + (TagType::Null) : { + // All set + } + (TagType::Byte) : { + u8 value; + u8 pad[[hidden]]; + } + (TagType::Word) : { + u16 value; + } + (TagType::DWord) : { + u32 value; + } + (TagType::QWord) : { + u64 value; + } + (TagType::StringRef) : { + u32 str_offset; + char value @ str_offset; + } + (TagType::List) : { + u32 size ; + Tag tags[while($ < ((addressof(size) + size + 4) ))]; + } + (TagType::String) : { + u32 size; + char16 value[size/2]; + } + (TagType::Binary) : { + u32 size; + u8 value[size]; + if (size & 1 ) { + u8 pad[[hidden]]; + } + } + } +}[[name(get_tag(tag)), comment(get_type(tag))]]; + +struct Header { + u32 Major; + u32 Minor; + char Magic[4]; + Tag tags[while(!std::mem::eof())]; +}; + +Header file @ 0x0; \ No newline at end of file diff --git a/tests/patterns/test_data/sdb.hexpat.sdb b/tests/patterns/test_data/sdb.hexpat.sdb new file mode 100644 index 0000000000000000000000000000000000000000..e98a40e1be84ac8f94a9307bc4b94c882a2c5919 GIT binary patch literal 260 zcmYL?uMWa65XSGi@kg!*1PNpmO|oQW@&tk)$P9iEs6sR$VRnwmy&H0hSKtN8T_^aO z{CeNrcWnfN9@mRS_Z8zpCxO)Wka-W}Sx^c^&`mio)a)7+;!w$jVHXojp~s9pNmrFZ z5r*BNX4l9|PdLL}W|j1nTL=qPM11BHvD=VluE{V*?#LXwJ#*q_%q1~kMaay_XaxNK dSH2E*`? Date: Wed, 20 Aug 2025 13:32:11 -0400 Subject: [PATCH 24/93] patterns: Added AppleSingle, AppleDouble, CHD, TARC patterns (#431) * Commit patterns I've collected - AppleSingle/AppleDouble pattern, used for macOS resource forks. - MAME CHD file format, currently only supports v5. - KEX Engine proprietary TARC format, used by various Nightdive games. * Add to README --- README.md | 4 ++ patterns/apple_single_double.hexpat | 50 +++++++++++++ patterns/chd.hexpat | 105 ++++++++++++++++++++++++++++ patterns/tarc.hexpat | 50 +++++++++++++ 4 files changed, 209 insertions(+) create mode 100644 patterns/apple_single_double.hexpat create mode 100644 patterns/chd.hexpat create mode 100644 patterns/tarc.hexpat diff --git a/README.md b/README.md index 6f0cc418..c5459fc8 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ADTS | `audio/x-hx-aac-adts` | [`patterns/adts.hexpat`](patterns/adts.hexpat) | ADTS/AAC audio files | | AFE2 | | [`patterns/afe2.hexpat`](patterns/afe2.hexpat) | Nintendo Switch Atmosphère CFW Fatal Error log | | ANI | `application/x-navi-animation` | [`patterns/ani.hexpat`](patterns/ani.hexpat) | Windows Animated Cursor file | +| AppleSingle | `application/applefile` | [`patterns/apple_single_double.hexpat`](patterns/apple_single_double.hexpat) | AppleSingle Dual Fork file | +| AppleDouble | `multipart/appledouble` | [`patterns/apple_single_double.hexpat`](patterns/apple_single_double.hexpat) | AppleDouble Resource Fork/Finder Metadata file | | AR | `application/x-archive` | [`patterns/ar.hexpat`](patterns/ar.hexpat) | Static library archive files | | ARC | | [`patterns/arc.hexpat`](patterns/arc.hexpat) | Minecraft Legacy Console Edition ARC files | | ARIA2 | | [`patterns/aria2.hexpat`](patterns/aria2.hexpat) | ARIA2 Download Manager Control files | @@ -50,6 +52,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | CCVXL | | [`patterns/ccvxl.hexpat`](patterns/ccvxl.hexpat) | Command and Conquer Voxel Model | | CCPAL | | [`patterns/ccpal.hexpat`](patterns/ccpal.hexpat) | Command and Conquer Voxel Palette | | CDA | | [`patterns/cda.hexpat`](patterns/cda.hexpat) | Compact Disc Audio track | +| CHD | | [`patterns/chd.hexpat`](patterns/chd.hexpat) | MAME Compressed Hunks of Data file | | CHM | `application/vnd.ms-htmlhelp` | [`patterns/chm.hexpat`](patterns/chm.hexpat) | Windows HtmlHelp Data (ITSF / CHM) | | COFF | `application/x-coff` | [`patterns/coff.hexpat`](patterns/coff.hexpat) | Common Object File Format (COFF) executable | | CPIO | `application/x-cpio` | [`patterns/cpio.hexpat`](patterns/cpio.hexpat) | Old Binary CPIO Format | @@ -168,6 +171,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | SWF | `application/x-shockwave-flash` |[`patterns/swf.hexpat`](patterns/swf.hexpat) | Shockwave Flash file format | | TA | | [`patterns/optee_ta.hexpat`](patterns/optee_ta.hexpat) | OPTEE Trusted Application Executable | | TAR | `application/x-tar` | [`patterns/tar.hexpat`](patterns/tar.hexpat) | Tar file format | +| TARC | | [`patterns/tarc.hexpat`](patterns/tarc.hexpat) | KEX Engine TARC file format | | TES | | [`patterns/wintec_tes.hexpat`](patterns/wintec_tes.hexpat) | Wintec TES GPS log | | Thumbcache | | [`patterns/thumbcache.hexpat`](patterns/thumbcache.hexpat) | Windows thumbcache_*.db | | TIFF | `image/tiff` | [`patterns/tiff.hexpat`](patterns/tiff.hexpat) | Tag Image File Format | diff --git a/patterns/apple_single_double.hexpat b/patterns/apple_single_double.hexpat new file mode 100644 index 00000000..e1b38b47 --- /dev/null +++ b/patterns/apple_single_double.hexpat @@ -0,0 +1,50 @@ +#pragma author Lexi Mayfield +#pragma description AppleSingle/AppleDouble file format +#pragma endian big +#pragma magic [00 05 16 0?] @ 0x00 + +import type.magic; + +enum EntryID : u32 { + DataFork = 1, + ResourceFork = 2, + RealName = 3, + Comment = 4, + IconBW = 5, + IconColor = 6, + FileDatesInfo = 8, + FinderInfo = 9, + MacFileInfo = 10, + ProDOSFileInfo = 11, + MSDOSFileInfo = 12, + ShortName = 13, + AFPFileInfo = 14, + DirectoryID = 15, +}; + +struct Entry { + EntryID entryID; + u32 offset; + u32 length; + u8 data[length] @ offset [[sealed]]; +}; + +enum FileType : u32 { + AppleSingle = 0x00051600, + AppleDouble = 0x00051607, +}; + +enum Version : u32 { + V1 = 0x00010000, + V2 = 0x00020000, +}; + +struct AppleSingleDouble { + FileType fileType; + Version version; + char homeFileSystem[16]; + u16 numEntries; + Entry entryDescs[numEntries]; +}; + +AppleSingleDouble appleSingleDouble @ 0x00; \ No newline at end of file diff --git a/patterns/chd.hexpat b/patterns/chd.hexpat new file mode 100644 index 00000000..d10ced8f --- /dev/null +++ b/patterns/chd.hexpat @@ -0,0 +1,105 @@ +#pragma author Lexi Mayfield +#pragma description MAME Compressed Hunks of Data +#pragma endian big + +fn CHD_MAKE_TAG(char a, char b, char c, char d) { + return (u32(u8(a)) << 24) | + u32(u8((b)) << 16) | + u32(u8((c)) << 8) | + u32(u8(d)); +}; + +enum CHDv5_CODEC : u32 { + NONE = 0, + ZLIB = CHD_MAKE_TAG('z','l','i','b'), + ZSTD = CHD_MAKE_TAG('z','s','t','d'), + LZMA = CHD_MAKE_TAG('l','z','m','a'), + HUFFMAN = CHD_MAKE_TAG('h','u','f','f'), + FLAC = CHD_MAKE_TAG('f','l','a','c'), + CD_ZLIB = CHD_MAKE_TAG('c','d','z','l'), + CD_ZSTD = CHD_MAKE_TAG('c','d','z','s'), + CD_LZMA = CHD_MAKE_TAG('c','d','l','z'), + CD_FLAC = CHD_MAKE_TAG('c','d','f','l'), + AVHUFF = CHD_MAKE_TAG('a','v','h','u'), +}; + +enum CHDv5_METADATA_TAG : u32 { + CHDMETATAG_WILDCARD = 0, + HARD_DISK_METADATA_TAG = CHD_MAKE_TAG('G','D','D','D'), + HARD_DISK_IDENT_METADATA_TAG = CHD_MAKE_TAG('I','D','N','T'), + HARD_DISK_KEY_METADATA_TAG = CHD_MAKE_TAG('K','E','Y',' '), + PCMCIA_CIS_METADATA_TAG = CHD_MAKE_TAG('C','I','S',' '), + CDROM_OLD_METADATA_TAG = CHD_MAKE_TAG('C','H','C','D'), + CDROM_TRACK_METADATA_TAG = CHD_MAKE_TAG('C','H','T','R'), + CDROM_TRACK_METADATA2_TAG = CHD_MAKE_TAG('C','H','T','2'), + GDROM_OLD_METADATA_TAG = CHD_MAKE_TAG('C','H','G','T'), + GDROM_TRACK_METADATA_TAG = CHD_MAKE_TAG('C','H','G','D'), + DVD_METADATA_TAG = CHD_MAKE_TAG('D','V','D',' '), + AV_METADATA_TAG = CHD_MAKE_TAG('A','V','A','V'), + AV_LD_METADATA_TAG = CHD_MAKE_TAG('A','V','L','D'), +}; + +struct CHDv5UncompressedMap { + u32 offset; +}; + +struct CHDv5CompressedMapEntry { + u8 compression; + u24 complength; + u48 offset; + u16 crc; +}; + +struct CHDv5CompressedMap { + u32 length; + u48 datastart; + u16 crc; + u8 lengthbits; + u8 hunkbits; + u8 parentunitbits; + u8 reserved; +}; + +struct CHDv5MetadataEntry { + CHDv5_METADATA_TAG metatag; + u8 flags; + u24 length; + u64 next; + char entry[length]; + + if (next != 0) { + CHDv5MetadataEntry nextMeta @ next; + } +}; + +struct CHDv5 { + CHDv5_CODEC compressors[4]; + u64 logicalbytes; + u64 mapoffset; + u64 metaoffset; + u32 hunkbytes; + u32 unitbytes; + u8 rawsha1[20]; + u8 sha1[20]; + u8 parentsha1[20]; + + if (compressors[0] == CHDv5_CODEC::NONE) { + CHDv5UncompressedMap map @ mapoffset; + } else { + CHDv5CompressedMap map @ mapoffset; + } + + CHDv5MetadataEntry meta @ metaoffset; +}; + +struct CHD { + char tag[8]; + u32 length; + u32 version; + + if (version == 5) { + CHDv5 chd; + } +}; + +CHD chd @ 0x00; \ No newline at end of file diff --git a/patterns/tarc.hexpat b/patterns/tarc.hexpat new file mode 100644 index 00000000..93ce6ad3 --- /dev/null +++ b/patterns/tarc.hexpat @@ -0,0 +1,50 @@ +#pragma author Lexi Mayfield +#pragma description KEX Engine TARC format +#pragma magic [0x54 0x41 0x52 0x43] @ 0x00 + +enum PixelFormat : u16 { + DXT1 = 14, + DXT5 = 16, + BC7 = 33, +}; + +struct TARCEntry1 { + char fileName[64]; + u64 offset; + u32 size; + u16 width; + u16 height; + u16 numMipMaps; + PixelFormat pixelFormat; +}; + +struct TARCEntry2 { + char fileName[64]; + u64 offset; + u32 size; + u16 x; + u16 y; + u16 width; + u16 height; + u16 numMipMaps; + PixelFormat pixelFormat; + u16 origWidth; + u16 origHeight; + s16 offsetX; + s16 offsetY; +}; + +struct TARC { + char id[4]; + u32 version; + u32 numEntries; + u64 pixelDataOffset; + + if (version == 1) { + TARCEntry1 entries[numEntries]; + } else if (version == 2) { + TARCEntry2 entries[numEntries]; + } +}; + +TARC tarc @ 0x00; \ No newline at end of file From afffd7eceddaae3ee14194d364daa953ecd2a760 Mon Sep 17 00:00:00 2001 From: Nik Date: Sun, 24 Aug 2025 12:00:05 +0200 Subject: [PATCH 25/93] includes/std: Added section parameters to a few std::mem functions --- includes/std/mem.pat | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/includes/std/mem.pat b/includes/std/mem.pat index 2c223033..96264e66 100644 --- a/includes/std/mem.pat +++ b/includes/std/mem.pat @@ -27,6 +27,8 @@ namespace auto std::mem { A Handle for a custom Section */ using Section = u128; + const u64 SectionMain = 0; + const u64 SectionCurrent = 0xFFFF'FFFF'FFFF'FFFF; /** The endianness of a value @@ -72,16 +74,16 @@ namespace auto std::mem { Gets the base address of the data @return The base address of the memory */ - fn base_address() { - return builtin::std::mem::base_address(); + fn base_address(u64 section = SectionCurrent) { + return builtin::std::mem::base_address(section); }; /** Gets the size of the data @return The size of the memory */ - fn size() { - return builtin::std::mem::size(); + fn size(u64 section = SectionCurrent) { + return builtin::std::mem::size(section); }; /** @@ -138,8 +140,8 @@ namespace auto std::mem { @param [endian] The endianness of the value to read. Defaults to native @return The value read */ - fn read_unsigned(u128 address, u8 size, Endian endian = Endian::Native) { - return builtin::std::mem::read_unsigned(address, size, u32(endian)); + fn read_unsigned(u128 address, u8 size, Endian endian = Endian::Native, Section section = SectionCurrent) { + return builtin::std::mem::read_unsigned(address, size, u32(endian), section); }; /** @@ -149,8 +151,8 @@ namespace auto std::mem { @param [endian] The endianness of the value to read. Defaults to native @return The value read */ - fn read_signed(u128 address, u8 size, Endian endian = Endian::Native) { - return builtin::std::mem::read_signed(address, size, u32(endian)); + fn read_signed(u128 address, u8 size, Endian endian = Endian::Native, Section section = SectionCurrent) { + return builtin::std::mem::read_signed(address, size, u32(endian), section); }; /** @@ -159,8 +161,8 @@ namespace auto std::mem { @param size The size of the value to read @return The value read */ - fn read_string(u128 address, u128 size) { - return builtin::std::mem::read_string(address, size); + fn read_string(u128 address, u128 size, Section section = SectionCurrent) { + return builtin::std::mem::read_string(address, size, section); }; /** From 91fd36097c98e5632b9f89d955fbc9a197bd20f9 Mon Sep 17 00:00:00 2001 From: Nik Date: Sun, 24 Aug 2025 12:04:33 +0200 Subject: [PATCH 26/93] includes/std: Fix use of std::mem::size and std::mem::base_address in library --- includes/std/mem.pat | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/includes/std/mem.pat b/includes/std/mem.pat index 96264e66..9406ba6c 100644 --- a/includes/std/mem.pat +++ b/includes/std/mem.pat @@ -6,23 +6,6 @@ namespace auto std::mem { - namespace impl { - - struct MagicSearchImpl { - s128 address = builtin::std::mem::find_string_in_range(0, $, builtin::std::mem::size(), Magic); - if (address < 0) - break; - - $ = address; - try { - T data [[inline]]; - } catch { - T data; - } - }; - - } - /** A Handle for a custom Section */ @@ -93,8 +76,8 @@ namespace auto std::mem { @return The address of the sequence */ fn find_sequence(u128 occurrence_index, auto ... bytes) { - const u128 address = builtin::std::mem::base_address(); - return builtin::std::mem::find_sequence_in_range(occurrence_index, address, address + builtin::std::mem::size(), bytes); + const u128 address = std::mem::base_address(); + return builtin::std::mem::find_sequence_in_range(occurrence_index, address, address + std::mem::size(), bytes); }; /** @@ -117,8 +100,8 @@ namespace auto std::mem { @return The address of the sequence */ fn find_string(u128 occurrence_index, str string) { - const u128 address = builtin::std::mem::base_address(); - return builtin::std::mem::find_string_in_range(occurrence_index, address, address + builtin::std::mem::size(), string); + const u128 address = std::mem::base_address(); + return builtin::std::mem::find_string_in_range(occurrence_index, address, address + std::mem::size(), string); }; /** @@ -287,6 +270,19 @@ namespace auto std::mem { return ""; }; + struct MagicSearchImpl { + s128 address = builtin::std::mem::find_string_in_range(0, $, std::mem::size(), Magic); + if (address < 0) + break; + + $ = address; + try { + T data [[inline]]; + } catch { + T data; + } + }; + } } From 7278a22eb28ac11787685003835ab0a6e49261ee Mon Sep 17 00:00:00 2001 From: Nik Date: Sun, 24 Aug 2025 13:34:47 +0200 Subject: [PATCH 27/93] includes/std: Fix MagicSearch implementation not being found --- includes/std/mem.pat | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/includes/std/mem.pat b/includes/std/mem.pat index 9406ba6c..b56e2b74 100644 --- a/includes/std/mem.pat +++ b/includes/std/mem.pat @@ -227,16 +227,6 @@ namespace auto std::mem { return builtin::std::mem::current_bit_offset(); }; - - /** - Searches for a sequence of bytes and places the given type at that address - @tparam Magic The magic sequence to search for - @tparam T The type to place at the address - */ - struct MagicSearch { - std::mem::impl::MagicSearchImpl impl[while(!std::mem::eof())] [[inline]]; - }; - /** Reinterprets a value as a different one @tparam From The type to reinterpret from @@ -285,4 +275,13 @@ namespace auto std::mem { } + /** + Searches for a sequence of bytes and places the given type at that address + @tparam Magic The magic sequence to search for + @tparam T The type to place at the address + */ + struct MagicSearch { + std::mem::impl::MagicSearchImpl impl[while(!std::mem::eof())] [[inline]]; + }; + } From c4c75a9ab2a7b6963e1d91426d157c84e929f20e Mon Sep 17 00:00:00 2001 From: mjarduk <66081585+mjarduk@users.noreply.github.com> Date: Mon, 25 Aug 2025 23:22:32 +0300 Subject: [PATCH 28/93] patterns: Add a pattern for Roblox .pack files (#435) * Added the .pack format * Clarified some fields by changing the naming --- README.md | 1 + patterns/roblox_pack.hexpat | 33 ++++++++++++++++++ .../test_data/roblox_pack.hexpat.pack | Bin 0 -> 116 bytes 3 files changed, 34 insertions(+) create mode 100644 patterns/roblox_pack.hexpat create mode 100644 tests/patterns/test_data/roblox_pack.hexpat.pack diff --git a/README.md b/README.md index c5459fc8..03cefbd0 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | NTFS | | [`patterns/fs/ntfs.hexpat`](patterns/fs/ntfs.hexpat) | NTFS (NT File System) | | OGG | `audio/ogg` | [`patterns/ogg.hexpat`](patterns/ogg.hexpat) | OGG Audio format | | ORP / ORS | | [`patterns/orp.hexpat`](patterns/orp.hexpat) | OpenRGB profile format | +| PACK | | [`patterns/roblox_pack.hexpat`](patterns/roblox_pack.hexpat) | Roblox shader archive format | | PAK | | [`patterns/xgspak.hexpat`](patterns/xgspak.hexpat) | Exient XGS Engine Pak files | | PCAP | `application/vnd.tcpdump.pcap` | [`patterns/pcap.hexpat`](patterns/pcap.hexpat) | pcap header and packets | | PcapNG | `application/vnd.tcpdump.pcap` | [`patterns/pcapng.hexpat`](patterns/pcapng.hexpat) | pcapng header and packets | diff --git a/patterns/roblox_pack.hexpat b/patterns/roblox_pack.hexpat new file mode 100644 index 00000000..2c63493e --- /dev/null +++ b/patterns/roblox_pack.hexpat @@ -0,0 +1,33 @@ +#pragma author marduk.ru +#pragma description Roblox .pack shader archive format +#pragma magic [ 52 42 58 53 ] @ 0x00 + +import std.string; +import hex.core; +import type.magic; + +struct Header { + type::Magic<"RBXS"> magic; + u32 fileCount; +}; + +struct FileIndex { + char name[0x40]; + u128 md5Hash; + u32 offset; + u32 size; + u64 reserved; + + u8 file[size] @ offset; + + #ifdef __IMHEX__ + hex::core::add_virtual_file(name, file); + #endif +} [[name(name)]]; + +struct RBXPack { + Header header; + FileIndex files[header.fileCount]; +}; + +RBXPack pack @ 0x0; \ No newline at end of file diff --git a/tests/patterns/test_data/roblox_pack.hexpat.pack b/tests/patterns/test_data/roblox_pack.hexpat.pack new file mode 100644 index 0000000000000000000000000000000000000000..99be9630cafaa6020a499159d310e24ec4cd1ab3 GIT binary patch literal 116 tcmWG?iU?+8U| Date: Sun, 31 Aug 2025 05:23:09 -0400 Subject: [PATCH 29/93] patterns/upk-ue3: Add magic pragma (#436) --- patterns/upk-ue3.hexpat | 1 + 1 file changed, 1 insertion(+) diff --git a/patterns/upk-ue3.hexpat b/patterns/upk-ue3.hexpat index b0c2a559..6f8674a4 100644 --- a/patterns/upk-ue3.hexpat +++ b/patterns/upk-ue3.hexpat @@ -1,5 +1,6 @@ #pragma description Unreal Engine 3 UPK #pragma endian little +#pragma magic [ c1 83 2a 9e ] @ 0x00 import std.io; From 74c06b74f7c74c6edeec81c3c657736766614873 Mon Sep 17 00:00:00 2001 From: MicroBlock <66859419+std-microblock@users.noreply.github.com> Date: Sun, 31 Aug 2025 17:27:59 +0800 Subject: [PATCH 30/93] patterns/lua53: Fix lua53 long string (#427) Update lua53.hexpat --- patterns/lua53.hexpat | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/patterns/lua53.hexpat b/patterns/lua53.hexpat index 12ef6851..dd0fbde6 100644 --- a/patterns/lua53.hexpat +++ b/patterns/lua53.hexpat @@ -44,8 +44,15 @@ struct LuaBinaryHeader { struct LuaString { u8 size; if (size > 0) { - char data[size-1]; + if (size == 0xff) { + u64 sizeReal; + char data[sizeReal-1]; + } else { + char data[size-1]; + } } + + }[[format("impl::format_LuaString")]]; struct LuaConstant { From 76f850c543997ab1a33f549e75e4f608f48806d6 Mon Sep 17 00:00:00 2001 From: DmitriLeon2000 Date: Sun, 31 Aug 2025 11:28:45 +0200 Subject: [PATCH 31/93] patterns/fas/was: Update FAS, WAS/WA3 pattern files and README.md (#425) * Add .fas and .was pattern files (Oska DeskMates) * Update .was pattern file * Update .was/.wa3 pattern file * Update README.md * Update README.md * Update .fas & .was pattern files * Update README.md * Update fas_oskasoftware_old.hexpat * Added WAS test file * Update WAS test file * Update was_oskasoftware.hexpat * Update was_oskasoftware.hexpat * Update fas_oskasoftware_old.hexpat * Update fas_oskasoftware.hexpat * Update README.md Replacing backward slashes with forward ones in the `WAS` row. * Update fas_oskasoftware_old.hexpat * Update was_oskasoftware.hexpat --- README.md | 2 +- patterns/fas_oskasoftware.hexpat | 34 +++++++++++----------- patterns/fas_oskasoftware_old.hexpat | 43 ++++++++++++++-------------- patterns/was_oskasoftware.hexpat | 6 ++-- 4 files changed, 42 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 03cefbd0..c6704725 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | VHDX | | [`patterns/vhdx.hexpat`](patterns/vhdx.hexpat) | Microsoft Hyper-V Virtual Hard Disk format | | VOX | | [`patterns/vox.hexpat`](patterns/vox.hexpat) | MagicaVoxel scene description format | | WAV | `audio/x-wav` | [`patterns/wav.hexpat`](patterns/wav.hexpat) | RIFF header, WAVE header, PCM header | -| WAS | | [`patterns\was_oskasoftware.hexpat`](patterns\was_oskasoftware.hexpat) | Oska Software DeskMates WAS/WA3 (WAVE/MP3 Set) file +| WAS | | [`patterns/was_oskasoftware.hexpat`](patterns/was_oskasoftware.hexpat) | Oska Software DeskMates WAS/WA3 (WAVE/MP3 Set) file | WAD | | [`patterns/wad.hexpat`](patterns/wad.hexpat) | DOOM WAD Archive | | WebP | `image/webp` | [`patterns/webp.hexpat`](patterns/webp.hexpat) | Google WebP image | | XBEH | `audio/x-xbox-executable` | [`patterns/xbeh.hexpat`](patterns/xbeh.hexpat) | Xbox executable | diff --git a/patterns/fas_oskasoftware.hexpat b/patterns/fas_oskasoftware.hexpat index 617ba4bc..f72b8559 100644 --- a/patterns/fas_oskasoftware.hexpat +++ b/patterns/fas_oskasoftware.hexpat @@ -1,25 +1,25 @@ #pragma author DmitriLeon2000 -#pragma description Oska Software DeskMates FAS (Frames and Sequences) file +#pragma description Oska Software DeskMates FAS (Frames and Sequences) #pragma endian little enum Compression : u32 { - BI_RGB, - BI_RLE8, - BI_RLE4, - BI_BITFIELDS, - BI_JPEG, - BI_PNG, - BI_ALPHABITFIELDS, - BI_CMYK, - BI_CMYKRLE8, - BI_CMYKRLE4, + BI_RGB, + BI_RLE8, + BI_RLE4, + BI_BITFIELDS, + BI_JPEG, + BI_PNG, + BI_ALPHABITFIELDS, + BI_CMYK, + BI_CMYKRLE8, + BI_CMYKRLE4, }; struct Colors { - u8 blue; - u8 green; - u8 red; - u8 reserved; + u8 blue; + u8 green; + u8 red; + u8 reserved; }; struct FASHeader { @@ -69,7 +69,7 @@ struct SeqHeader { }; struct Bitmap { - u8 byte[fas.fasHeader.version >= 3 ? fas.fasHeader.frameSize + (fas.frameSizeHigh << 16) : fas.fasHeader.frameSize]; + u8 byte[parent.fasHeader.version >= 3 ? parent.fasHeader.frameSize + (parent.frameSizeHigh << 16) : parent.fasHeader.frameSize] [[sealed]]; }; struct FramesHeader { @@ -105,7 +105,7 @@ struct FAS { u8 filenameChecksum; // a checksum for a filename in ASCII AnimSequence sequences[seqHeader.count]; FramesHeader framesHeader; + Bitmap framesBitmap[framesHeader.count]; }; FAS fas @ 0x00; -Bitmap framesBitmap[fas.framesHeader.count] @ $; \ No newline at end of file diff --git a/patterns/fas_oskasoftware_old.hexpat b/patterns/fas_oskasoftware_old.hexpat index 9c7e7902..fc04ec43 100644 --- a/patterns/fas_oskasoftware_old.hexpat +++ b/patterns/fas_oskasoftware_old.hexpat @@ -1,25 +1,25 @@ #pragma author DmitriLeon2000 -#pragma description Oska Software DeskMates FAS (Frames and Sequences) file (Oska DeskMate versions 1.3 and 2.06) +#pragma description Oska Software DeskMates FAS (Frames and Sequences) (Oska DeskMate 1.03 and 2.06) #pragma endian little enum Compression : u32 { - BI_RGB, - BI_RLE8, - BI_RLE4, - BI_BITFIELDS, - BI_JPEG, - BI_PNG, - BI_ALPHABITFIELDS, - BI_CMYK, - BI_CMYKRLE8, - BI_CMYKRLE4, + BI_RGB, + BI_RLE8, + BI_RLE4, + BI_BITFIELDS, + BI_JPEG, + BI_PNG, + BI_ALPHABITFIELDS, + BI_CMYK, + BI_CMYKRLE8, + BI_CMYKRLE4, }; struct Colors { - u8 blue; - u8 green; - u8 red; - u8 reserved; + u8 blue; + u8 green; + u8 red; + u8 reserved; }; struct FASHeader { @@ -59,14 +59,14 @@ struct AnimSequence { }; struct SeqHeader { - le u32 size; - le u32 count; - le u32 strPointers[count*2]; + le u32 size; + le u32 count; + le u32 strPointers[count*2]; }; struct Frame { - u8 colorBitmap[fas.fasHeader.frameSizeColor]; - u8 maskBitmap[fas.fasHeader.frameSizeMask]; + u8 colorBitmap[parent.fasHeader.frameSizeColor] [[sealed]]; + u8 maskBitmap[parent.fasHeader.frameSizeMask] [[sealed]]; }; struct FramesHeader { @@ -80,8 +80,7 @@ struct FAS { SeqHeader seqHeader; AnimSequence sequences[seqHeader.count]; FramesHeader framesHeader; + Frame frames[framesHeader.count]; }; - FAS fas @ 0x00; -Frame frames[fas.framesHeader.count] @ $; diff --git a/patterns/was_oskasoftware.hexpat b/patterns/was_oskasoftware.hexpat index 9feed166..ccdfe707 100644 --- a/patterns/was_oskasoftware.hexpat +++ b/patterns/was_oskasoftware.hexpat @@ -7,8 +7,8 @@ import type.size; struct Sound { char name[]; u32 length; - u8 data[length]; -}; + u8 data[length] [[sealed]]; +} [[name(name), hex::visualize("sound", data, 1, 8000)]]; struct Header { type::Size size; @@ -21,4 +21,4 @@ struct WAS { Sound sounds[header.count]; }; -WAS was @ 0x00; \ No newline at end of file +WAS was @ 0x00; From 70dd55aa6b1d2cc16dbbec136805b620e48ca097 Mon Sep 17 00:00:00 2001 From: Tom Arrow Date: Sun, 31 Aug 2025 09:31:20 +0000 Subject: [PATCH 32/93] patterns/q3demo: Quake 3 demos: Handle corrupted files more gracefully (#414) Quake 3 demos: Basic safeguard against corrupted files Co-authored-by: Tom --- patterns/q3demo.hexpat | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/patterns/q3demo.hexpat b/patterns/q3demo.hexpat index 4d013f28..015c224f 100644 --- a/patterns/q3demo.hexpat +++ b/patterns/q3demo.hexpat @@ -118,10 +118,15 @@ fn readString(ref auto data, s32 len, s32 bitindex){ return test; }; +bool corrupted = false; + struct Message { le s32 messageNum; le s32 len; - if(len != FINAL_DEMO_MESSAGE_LENGTH || messageNum != FINAL_DEMO_MESSAGE_NUMBER) { + if(len+$ > std::mem::size()){ + corrupted = true; + } + if(!corrupted && (len != FINAL_DEMO_MESSAGE_LENGTH || messageNum != FINAL_DEMO_MESSAGE_NUMBER)) { u8 data[len]; if( len>=10){ // should usually be true unless corrupted @@ -189,7 +194,7 @@ namespace format { } struct Q3Demo { - Message messages[while(!std::mem::eof())]; + Message messages[while(!std::mem::eof() && !corrupted)]; }; From ff68d1e23d364e40352d52c61a4feb1ebd3d4935 Mon Sep 17 00:00:00 2001 From: Henri Asseily Date: Sun, 31 Aug 2025 12:36:00 +0300 Subject: [PATCH 33/93] patterns: Added Apple IIGS SHR + SHR 3200 + SHR PWA Animation pattern (#432) * Added SHR pattern * Added IIGS SHR animation test file * Added pattern to readme * Added description and author --------- Co-authored-by: Nik --- README.md | 1 + patterns/SHR.hexpat | 189 ++++++++++++++++++ tests/patterns/test_data/SHR_animation#C20000 | Bin 0 -> 34892 bytes 3 files changed, 190 insertions(+) create mode 100644 patterns/SHR.hexpat create mode 100644 tests/patterns/test_data/SHR_animation#C20000 diff --git a/README.md b/README.md index c6704725..5d18b361 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | SDB | | [`patterns/sdb.hexpat`](patterns/sdb.hexpat) | [Shim DataBase](https://learn.microsoft.com/en-us/windows/win32/devnotes/application-compatibility-database) file format | | Shell Link | `application/x-ms-shortcut` | [`patterns/lnk.hexpat`](patterns/lnk.hexpat) | Windows Shell Link file format | | shp | | [`patterns/shp.hexpat`](patterns/shp.hexpat) | ESRI shape file | +| SHR | | [`patterns/SHR.hexpat`](patterns/SHR.hexpat) | Apple IIgs Super Hi-Res (SHR) + PaintWorks Animation (ANI) | | shx | | [`patterns/shx.hexpat`](patterns/shx.hexpat) | ESRI index file | | smk | | [`patterns/smk.hexpat`](patterns/smk.hexpat) | Smacker video file | | sup | | [`patterns/sup.hexpat`](patterns/sup.hexpat) | PGS Subtitle | diff --git a/patterns/SHR.hexpat b/patterns/SHR.hexpat new file mode 100644 index 00000000..0c4386f8 --- /dev/null +++ b/patterns/SHR.hexpat @@ -0,0 +1,189 @@ +/*! + Apple IIgs Super Hi-Res (SHR) + PaintWorks Animation (ANI) — ImHex pattern + + Supports: + • PIC $C1/$0000 — 32 KB uncompressed SHR screen image + • PIC $C1/$0002 — 3200-color (“Brooks”) per-scanline palettes + • ANI $C2/$0000 — PaintWorks animation: + 0x0000..0x7FFF : base SHR $C1/$0000 image + 0x8000.. : animation header + chunks + + PaintWorks animation structure (per reversed docs): + - Base image: uncompressed SHR ($C1/$0000) + - +0x8000 u32: total animation data length after header (file_len - 0x8008) + - +0x8004 u16: global frame delay in VBLs + - +0x8006 u16: flag/? (commonly 0x00C0 or 0x00C1) + - +0x8008 ...: one or more animation chunks: + chunk: + u32 chunk_len (includes this length field) + repeated { u16 offset; u16 value; } pairs + offset==0x0000 marks End-of-Frame (value is ignored) + - Offsets may target any byte in the 32 KB SHR space (pixels, SCBs, palettes). + This enables palette-cycling and SCB effects. + + References: + - CiderPress2 PaintWorks Animation notes (file structure, fields, semantics). +*/ + +import std.core; +import std.sys; +import std.math; + +#pragma endian little +#include + +#pragma description Apple IIgs Super Hi-Res (SHR) + PaintWorks Animation (ANI) +#pragma author hasseily + + +// ------------------------------ Constants ------------------------------ + +const u32 SHR_ROWS = 200; +const u32 SHR_BYTES_PER_ROW = 160; +const u32 SHR_PIXEL_DATA_SIZE = SHR_ROWS * SHR_BYTES_PER_ROW; // 32000 (0x7D00) + +const u32 PIC0000_FILE_SIZE = 0x8000; // 32768 +const u32 PIC0002_FILE_SIZE = 0x9600; // 38400 (32000 + 200*32) + +const u32 PIC0000_OFF_PIXELS = 0x0000; +const u32 PIC0000_OFF_SCB = 0x7D00; +const u32 PIC0000_OFF_RESERVED = 0x7DC8; +const u32 PIC0000_OFF_PALETTES = 0x7E00; + +const u32 PALETTE_COUNT = 16; +const u32 PALETTE_COLORS = 16; + +const u32 ANI_BASE_SHR_SIZE = 0x8000; // First 32 KB is a $C1/$0000 image +const u32 ANI_HDR_OFF = 0x8000; // Animation header starts here +const u32 ANI_MIN_TOTAL_SIZE = 0x8008; // base + header (no chunks) + +// ------------------------------ Types: SHR core ------------------------------ + +struct Row160 { u8 data[SHR_BYTES_PER_ROW]; }; + +// Scanline Control Byte +bitfield ShrSCB { + palette : 4; // 0..15 + reserved : 1; + color_fill : 1; + interrupt : 1; + mode_640 : 1; // 0=320, 1=640 +}; + +// helper: expand a 4-bit channel to 8-bit (0x0..0xF -> 0x00..0xFF) +fn expand4(u8 v) { return (v << 4) | v; }; + +bitfield Colors_gb { + blue : 4; // Blue (B3..B0) + green : 4; // Green (G3..G0) +}; + +bitfield Colors_r0 { + red : 4; // Red (R3..R0) + unused : 4; // Unused / reserved +}; + +// RGB444 stored as 0RGB +struct Rgb444_0RGB { + Colors_gb gb; + Colors_r0 r0; +} [[color(std::format("{:02X}{:02X}{:02X}", expand4(r0.red), expand4(gb.green), expand4(gb.blue)))]]; + +struct Palette16 { Rgb444_0RGB color[PALETTE_COLORS]; }; + +// $C1/$0000 raw 32 KB screen dump +struct SHR_PIC0000 { + Row160 pixels[SHR_ROWS] @ PIC0000_OFF_PIXELS; + ShrSCB scb[SHR_ROWS] @ PIC0000_OFF_SCB; + u8 reserved[56] @ PIC0000_OFF_RESERVED; + Palette16 palettes[PALETTE_COUNT] @ PIC0000_OFF_PALETTES; +}; + +// “Brooks” 3200-color: pixels + 200 per-line palettes (no SCBs) +struct BrooksLinePalette { Rgb444_0RGB color[PALETTE_COLORS]; }; + +struct SHR_PIC0002 { + Row160 pixels[SHR_ROWS] @ 0x0000; + BrooksLinePalette line_palettes[SHR_ROWS] @ SHR_PIXEL_DATA_SIZE; // 0x7D00 +}; + +// ------------------------------ Types: PaintWorks ANI ($C2/$0000) ------------------------------ + +/* Each operation modifies 1 word at an absolute offset in the 32 KB SHR area. + End-of-frame marker: offset == 0x0000 (value is ignored). */ +struct AniOp { + u16 offset [[color("0000AA")]]; // 0x0000..0x7FFE valid; 0x0000 = End-of-Frame + u16 value [[color("00AAAA")]]; // word to store at [offset] + // For convenience in the sidebar: + bool is_eof = (offset == 0x0000); +}; + +// A contiguous animation chunk: length + packed AniOp pairs. +// Most files have exactly one chunk that spans all frames. +struct AniChunk { + u32 chunk_len; // includes this field + + // ops_count = (chunk_len - 4)/4, unless chunk_len == 4 + // in which case: (__file_size - 0x8000 - 12)/4 + u64 ops_count64 = + (chunk_len == 4) + ? ( (__file_size > (0x8000 + 12)) ? ((__file_size - 0x8000 - 12) / 4) : 0 ) + : ( (chunk_len >= 4) ? (u64(chunk_len - 4) / 4) : 0 ); + + u32 ops_count = u32(ops_count64); + + // ops start immediately after chunk_len (offset +4) + AniOp ops[ops_count]; +}; + +// Header located at 0x8000 after the base 32 KB image +struct AniHeader { + u32 anim_data_len [[color("660000")]]; // total bytes of animation data after header + u16 frame_delay_vbl [[color("CC0000")]]; // global per-frame delay in VBLs (NTSC/PAL differ) + u16 flag_unknown; // usually 0x00C0 or 0x00C1 +}; + +// Full PaintWorks animation container +struct ANI_PaintWorks { + // Base frame: a normal uncompressed SHR image + SHR_PIC0000 base @ 0x0000; + + // Global animation header + AniHeader hdr @ ANI_HDR_OFF; + + // One or more chunks, typically exactly one: + AniChunk chunks[ std::math::min( + u32(16), // cap to keep ImHex happy in pathological cases + u32((__file_size - (ANI_HDR_OFF + sizeof(AniHeader))) > 3 ? + 1 + u32((__file_size - (ANI_HDR_OFF + sizeof(AniHeader))) / 0x10000000) : + 1) + ) ] @ (ANI_HDR_OFF + sizeof(AniHeader)); + + // Helpful computed values for inspection: + u64 file_len = __file_size; + u64 expected_anim_end = ANI_HDR_OFF + sizeof(AniHeader) + u64(hdr.anim_data_len); +}; + +// ------------------------------ Dispatcher ------------------------------ + +u64 __file_size = std::mem::size(); + +if (__file_size == PIC0000_FILE_SIZE) { + // Plain SHR dump + SHR_PIC0000 pic0000 @ 0x0000; + +} else if (__file_size == PIC0002_FILE_SIZE) { + // Brooks 3200-color + SHR_PIC0002 pic0002 @ 0x0000; + +} else if (__file_size >= ANI_MIN_TOTAL_SIZE) { + // Heuristic: treat as PaintWorks ANI if there’s room for base+header. + // (Many PW ANI files use ProDOS type $C2/$0000.) + ANI_PaintWorks ani @ 0x0000; + +} else if (__file_size >= SHR_PIXEL_DATA_SIZE) { + // Fallback: show pixels only for odd dumps + Row160 pixels_only[SHR_ROWS] @ 0x0000; +} + +ANI_PaintWorks ani_paintworks_at_0 @ 0; \ No newline at end of file diff --git a/tests/patterns/test_data/SHR_animation#C20000 b/tests/patterns/test_data/SHR_animation#C20000 new file mode 100644 index 0000000000000000000000000000000000000000..bcc964d540064d025efc1ef6a741b8877001d142 GIT binary patch literal 34892 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 x%wFR(aQ%X=0|rKnS4TwSlTlm;fQuJ~BgQN6y Date: Sun, 31 Aug 2025 11:37:59 +0200 Subject: [PATCH 34/93] patterns: Add .NET BinaryFormatter pattern (#416) * Add dotnet BinaryFormatter pattern * Add dotnet BinaryFormatter test * Update README.md --------- Co-authored-by: Nik --- README.md | 1 + patterns/dotnet_binaryformatter.hexpat | 921 ++++++++++++++++++ .../dotnet_binaryformatter.hexpat.bin | Bin 0 -> 372 bytes 3 files changed, 922 insertions(+) create mode 100644 patterns/dotnet_binaryformatter.hexpat create mode 100644 tests/patterns/test_data/dotnet_binaryformatter.hexpat.bin diff --git a/README.md b/README.md index 5d18b361..74b341ce 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | DICOM | `application/dicom` | [`patterns/dicom.hexpat`](patterns/dicom.hexpat) | DICOM image format | | DMG | | [`patterns/dmg.hexpat`](patterns/dmg.hexpat) | Apple Disk Image Trailer (DMG) | | DMP | | [`patterns/dmp64.hexpat`](patterns/dmp64.hexpat) | Windows Kernel Dump(DMP64) | +| DOTNET_BinaryFormatter | | [`patterns/dotnet_binaryformatter.hexpat`](patterns/dotnet_binaryformatter.hexpat) | .NET BinaryFormatter | | DPAPI_Blob | | [`patterns/dpapblob.hexpat`](patterns/dpapiblob.hexpat) | Data protection API Blob File Format | | DPAPI_MasterKey | | [`patterns/dpapimasterkey.hexpat`](patterns/dpapimasterkey.hexpat) | Data protection API MasterKey | | DS_Store | | [`patterns/dsstore.hexpat`](patterns/dsstore.hexpat) | .DS_Store file format | diff --git a/patterns/dotnet_binaryformatter.hexpat b/patterns/dotnet_binaryformatter.hexpat new file mode 100644 index 00000000..ea17ecd8 --- /dev/null +++ b/patterns/dotnet_binaryformatter.hexpat @@ -0,0 +1,921 @@ + +/* + References: + .NET BinaryFormatter Specification "MS-NRBF": + https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/75b9fe09-be15-475f-85b8-ae7b7558cfe5 + .NET runtime: + https://github.com/dotnet/runtime/blob/v8.0.17/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryParser.cs + .NET Library for Parsing MS-NRBF streams: + https://github.com/bbowyersmyth/BinaryFormatDataStructure +*/ + +#pragma author ODeux +#pragma description .NET BinaryFormatter (System.Runtime.Serialization.Formatters.Binary, obsolete) + +#pragma endian little + +import std.core; +import std.sys; +import std.mem; +import std.ptr; + +fn offsetOf(ref auto value, ref auto value_field){ + return addressof(value_field) - addressof(value); +}; + +struct NullableArrayPtr{ + pointerSize pointerValue [[no_unique_address, hidden]]; + if(pointerValue != 0) + T *data[size]: pointerSize; + else + padding[sizeof(pointerSize)]; +}; + +using _Ptr = std::ptr::NullablePtr; +using _ArrayPtr = NullableArrayPtr; + +fn append_value_to_section(ref auto value, std::mem::Section section){ + u128 old_section_size = std::mem::get_section_size(section); + std::mem::copy_value_to_section(value, section, old_section_size); + return old_section_size; +}; + +fn todo(auto message){ + std::error(std::format("@0x{:08X} TODO: " + message, $)); +}; + +using _Trackers; +std::mem::Section _TrackersSection = std::mem::create_section("_TrackersSection"); +_Trackers _trackers @ 0x0 in _TrackersSection; +bool NeedUpdateTrackers = false; +bool IsUpdatingTrackers = false; + +enum _ObjEnum: u64{ + Empty = 0, + SerializedStreamHeader = 1, + ClassWithId = 2, Object = _ObjEnum::ClassWithId, + SystemClassWithMembers = 3, ObjectWithMap = _ObjEnum::SystemClassWithMembers, + ClassWithMembers = 4, ObjectWithMapAssemId = _ObjEnum::ClassWithMembers, + SystemClassWithMembersAndTypes = 5, ObjectWithMapTyped = _ObjEnum::SystemClassWithMembersAndTypes, + ClassWithMembersAndTypes = 6, ObjectWithMapTypedAssemId = _ObjEnum::ClassWithMembersAndTypes, + BinaryObjectString = 7, ObjectString = _ObjEnum::BinaryObjectString, + BinaryArray = 8, Array = _ObjEnum::BinaryArray, + MemberPrimitiveTyped = 9, + MemberReference = 10, + BinaryLibrary = 11, Assembly = _ObjEnum::BinaryLibrary, + ObjectNullMultiple256 = 12, + ObjectNullMultiple = 13, + ArraySinglePrimitive = 14, + ArraySingleObject = 15, + ArraySingleString = 16, + CrossAppDomainMap = 17, + CrossAppDomainString = 18, + CrossAppDomainAssembly = 19, + MethodCall = 20, + MethodReturn = 21 +}; + +fn zeroedCurrObjTrackers(){ + _trackers.currentObj.TypeName.pointerValue = 0; + _trackers.currentObj.AssemblyName.pointerValue = 0; + _trackers.currentObj.objEnum = _ObjEnum::Empty; + _trackers.currentObj.RawPtr = 0; +}; + +fn copyCurrObjAtIdTrackers(auto id){ + NeedUpdateTrackers = true; + _trackers.objs[id].TypeName.pointerValue = _trackers.currentObj.TypeName.pointerValue; + _trackers.objs[id].AssemblyName.pointerValue = _trackers.currentObj.AssemblyName.pointerValue; + /* ! Enum does not get copied if we don't use a cast here for some reason ! */ + _trackers.objs[id].objEnum = u64(_trackers.currentObj.objEnum); + _trackers.objs[id].RawPtr = _trackers.currentObj.RawPtr; +}; + +using BitfieldOrder = std::core::BitfieldOrder; + +using TimeSpan = s64; + +enum PrimitiveTypeEnum: u8{ + Invalid = 0, + Boolean = 1, + Byte = 2, + Char = 3, + Currency = 4, /* Not Used in this protocol */ + Decimal = 5, + Double = 6, + Int16 = 7, + Int32 = 8, + Int64 = 9, + SByte = 10, + Single = 11, + TimeSpan = 12, + DateTime = 13, + UInt16 = 14, + UInt32 = 15, + UInt64 = 16, + Null = 17, + String = 18 +}; + +struct PrimitiveTypeEnumT{ + PrimitiveTypeEnum primitiveTypeEnumT = _primitiveTypeEnumT; + PrimitiveTypeEnum primitiveTypeEnum; + if(_primitiveTypeEnumT > 0) + std::assert(primitiveTypeEnum == primitiveTypeEnumT, std::format("Expected {} but got {}", primitiveTypeEnumT, primitiveTypeEnum)); +}; + +enum BinaryTypeEnum: u8{ + Primitive = 0, + String = 1, + Object = 2, + SystemClass = 3, ObjectUrt = BinaryTypeEnum::SystemClass, + Class = 4, ObjectUser = BinaryTypeEnum::Class, + ObjectArray = 5, + StringArray = 6, + PrimitiveArray = 7 +}; + +enum BinaryArrayTypeEnum: u8{ + Single = 0, + Jagged = 1, + Rectangular = 2, + SingleOffset = 3, + JaggedOffset = 4, + RectangularOffset = 5 +}; + +enum RecordTypeEnum: u8{ + SerializedStreamHeader = 0, + ClassWithId = 1, Object = RecordTypeEnum::ClassWithId, + SystemClassWithMembers = 2, ObjectWithMap = RecordTypeEnum::SystemClassWithMembers, + ClassWithMembers = 3, ObjectWithMapAssemId = RecordTypeEnum::ClassWithMembers, + SystemClassWithMembersAndTypes = 4, ObjectWithMapTyped = RecordTypeEnum::SystemClassWithMembersAndTypes, + ClassWithMembersAndTypes = 5, ObjectWithMapTypedAssemId = RecordTypeEnum::ClassWithMembersAndTypes, + BinaryObjectString = 6, ObjectString = RecordTypeEnum::BinaryObjectString, + BinaryArray = 7, Array = RecordTypeEnum::BinaryArray, + MemberPrimitiveTyped = 8, + MemberReference = 9, + ObjectNull = 10, + MessageEnd = 11, + BinaryLibrary = 12, Assembly = RecordTypeEnum::BinaryLibrary, + ObjectNullMultiple256 = 13, + ObjectNullMultiple = 14, + ArraySinglePrimitive = 15, + ArraySingleObject = 16, + ArraySingleString = 17, + CrossAppDomainMap = 18, + CrossAppDomainString = 19, + CrossAppDomainAssembly = 20, + MethodCall = 21, + MethodReturn = 22 +}; +using BinaryHeaderEnum = RecordTypeEnum; + +struct RecordTypeEnumT{ + RecordTypeEnum recordTypeEnumT = _recordTypeEnumT; + RecordTypeEnum recordTypeEnum; + if(_recordTypeEnumT > 0) + std::assert(recordTypeEnum == recordTypeEnumT, std::format("Expected {} but got {}", recordTypeEnumT, recordTypeEnum)); +}; + +bitfield MessageFlags{ + bool NoArgs: 1; /* Arg Category */ + bool ArgsInline: 1; /* Arg Category */ + bool ArgsIsArray: 1; /* Arg Category */ + bool ArgsInArray: 1; /* Arg Category */ + bool NoContext: 1; /* Context Category */ + bool ContextInline: 1; /* Context Category */ + bool ContextInArray: 1; /* Context Category */ + bool MethodSignatureInArray: 1; /* Signature Category */ + bool PropertiesInArray: 1; /* Property Category */ + bool NoReturnValue: 1; /* Return Category */ + bool ReturnValueVoid: 1; /* Return Category */ + bool ReturnValueInline: 1; /* Return Category */ + bool ReturnValueInArray: 1; /* Return Category */ + bool ExceptionInArray: 1; /* Exception Category */ + bool GenericMethod: 1; /* Generic Category */ + unsigned unused: 17; +} [[bitfield_order(BitfieldOrder::LeastToMostSignificant, 32)]]; + +fn validate_MessageFlags(MessageFlags flags){ + u8 arg_cnt = flags.NoArgs + flags.ArgsInline + flags.ArgsIsArray + flags.ArgsInArray; + u8 ctx_cnt = flags.NoContext + flags.ContextInline + flags.ContextInArray; + u8 sig_cnt = flags.MethodSignatureInArray; + u8 ret_cnt = flags.NoReturnValue + flags.ReturnValueVoid + flags.ReturnValueInline + flags.ReturnValueInArray; + u8 excep_cnt = flags.ExceptionInArray; + u8 prop_cnt = flags.PropertiesInArray; + u8 gen_cnt = flags.GenericMethod; + if(arg_cnt > 1 || ctx_cnt > 1 || sig_cnt > 1 || ret_cnt > 1 || excep_cnt > 1 || prop_cnt > 1 || gen_cnt > 1) + return -1; + if(arg_cnt != 0 && excep_cnt != 0) return -1; + if(ret_cnt != 0 && excep_cnt != 0) return -1; + if(ret_cnt != 0 && sig_cnt != 0) return -1; + if(excep_cnt != 0 && sig_cnt != 0) return -1; + return 1; +}; + +enum DateTimeKind: u8{ + NOT_SPECIFIED = 0, + UTC = 1, + Local = 2 +}; + +bitfield DateTime{ + s64 Ticks: 62; + DateTimeKind kind: 2; +} [[bitfield_order(BitfieldOrder::LeastToMostSignificant, 64)]]; + +struct vLength{ + /* + Can't use that, it breaks when struct get re-parsed in _TrackersSection + u8 data[while(std::mem::read_unsigned($, 1) & 0x80)]; + u8 last; + */ + u64 bytes [[no_unique_address, hidden]]; + u8 cnt = 0; + if(bytes & 0x80){ + if(bytes & 0x8000){ + if(bytes & 0x800000){ + if(bytes & 0x80000000){ + if(bytes & 0x8000000000){ + /* exceeding vLength 5 bytes, caller should crash */ + cnt = 5; + }else cnt = 4; + }else cnt = 3; + }else cnt = 2; + }else cnt = 1; + }else cnt = 0; + u8 data[cnt]; + u8 last; +} [[sealed, transform("LPS_Length_decode"), format("LPS_Length_decode")]]; + +fn LPS_Length_decode(auto Length){ + u64 length = 0; + u8 i = 0; + for(i = 0, i < sizeof(Length.data), i += 1) + length |= u64(Length.data[i] & 0x7F) << i * 7; + length |= u64(Length.last) << i * 7; + return length; +}; + +struct LengthPrefixedString{ + vLength Length; + std::assert(sizeof(Length) <= 5, "LengthPrefixedString.Length must be at most 5 bytes long"); + char String[Length]; +}; + +using Decimal = LengthPrefixedString; + +struct ClassTypeInfo{ + LengthPrefixedString TypeName; + s32 LibraryId; +}; + +struct ValueWithCode{ + PrimitiveTypeEnum PrimitiveType; + match(PrimitiveType){ + (PrimitiveTypeEnum::Boolean): bool Value; + (PrimitiveTypeEnum::Byte): u8 Value; + (PrimitiveTypeEnum::Char): char Value; + (PrimitiveTypeEnum::Currency): std::error("Primitive currency not used in this protocol"); + (PrimitiveTypeEnum::Decimal): Decimal Value; + (PrimitiveTypeEnum::Double): double Value; + (PrimitiveTypeEnum::Int16): s16 Value; + (PrimitiveTypeEnum::Int32): s32 Value; + (PrimitiveTypeEnum::Int64): s64 Value; + (PrimitiveTypeEnum::SByte): s8 Value; + (PrimitiveTypeEnum::Single): float Value; + (PrimitiveTypeEnum::TimeSpan): TimeSpan Value; + (PrimitiveTypeEnum::DateTime): DateTime Value; + (PrimitiveTypeEnum::UInt16): u16 Value; + (PrimitiveTypeEnum::UInt32): u32 Value; + (PrimitiveTypeEnum::UInt64): u64 Value; + (PrimitiveTypeEnum::Null): {} + (PrimitiveTypeEnum::String): LengthPrefixedString Value; + (_): std::error(std::format("Unexpected {}", PrimitiveType)); + } +}; + +struct StringValueWithCode: PrimitiveTypeEnumT{ + LengthPrefixedString StringValue; +}; + +struct ArrayOfValueWithCode{ + s32 Length; + ValueWithCode ListOfValueWithCode[Length]; +}; + +struct ArrayInfo{ + s32 ObjectId; + s32 Length; +}; + +struct ClassInfo{ + s32 ObjectId; + LengthPrefixedString Name; + s32 MemberCount; + LengthPrefixedString MemberNames[MemberCount]; +}; + +struct AdditionalInfo{ + BinaryTypeEnum binaryTypeEnum = _binaryTypeEnum; + match(binaryTypeEnum){ + (BinaryTypeEnum::SystemClass): /* ObjectUrt */ + LengthPrefixedString String; + (BinaryTypeEnum::Class): /* ObjectUser */ + ClassTypeInfo classTypeInfo; + (BinaryTypeEnum::Primitive | BinaryTypeEnum::PrimitiveArray): { + PrimitiveTypeEnum primitiveType; + std::assert(primitiveType != PrimitiveTypeEnum::Null && + primitiveType != PrimitiveTypeEnum::String, "Must not be Null or String"); + } + (BinaryTypeEnum::String | BinaryTypeEnum::Object | + BinaryTypeEnum::ObjectArray | BinaryTypeEnum::StringArray): + {/* not using continue here, need to keep array index matching */} + (_): std::error(std::format("Unrecognized {}", binaryTypeEnum)); + } +}; + +struct MemberTypeInfo{ + BinaryTypeEnum binaryTypeEnums[MemberCount]; + AdditionalInfo additionalInfo[MemberCount]; +}; + +struct UntypedMember{ + str className = _className; + u64 MemberCount = classInfo.MemberCount; + if(className == "System.Guid" && MemberCount == 11){ + match(std::core::array_index()){ + (0): s32 _a; + (1): s16 _b; + (2): s16 _c; + (3): u8 _d; + (4): u8 _e; + (5): u8 _f; + (6): u8 _g; + (7): u8 _h; + (8): u8 _i; + (9): u8 _j; + (10): u8 _k; + (_): std::error("unreachable"); + } + }else if(MemberCount == 1 && classInfo.MemberNames[0].String == "value__"){ + str Name = classInfo.MemberNames[0].String; + s32 member [name(Name)]; + }else + std::error(std::format("Unsupported untyped member: {}", className)); +}; + +using Record; + +struct Members{ + u64 i = std::core::array_index(); + str Name = _Name; + BinaryTypeEnum binaryTypeEnum = memberTypeInfo.binaryTypeEnums[i]; + if(binaryTypeEnum == BinaryTypeEnum::Primitive){ + PrimitiveTypeEnum primitiveTypeEnum = memberTypeInfo.additionalInfo[i].primitiveType; + match(primitiveTypeEnum){ + (PrimitiveTypeEnum::Boolean): bool Value [[name(Name)]]; + (PrimitiveTypeEnum::Byte): u8 Value [[name(Name)]]; + (PrimitiveTypeEnum::Char): char Value [[name(Name)]]; + (PrimitiveTypeEnum::Currency): std::error("Primitive currency not used in this protocol"); + (PrimitiveTypeEnum::Decimal): Decimal Value [[name(Name)]]; + (PrimitiveTypeEnum::Double): double Value [[name(Name)]]; + (PrimitiveTypeEnum::Int16): s16 Value [[name(Name)]]; + (PrimitiveTypeEnum::Int32): s32 Value [[name(Name)]]; + (PrimitiveTypeEnum::Int64): s64 Value [[name(Name)]]; + (PrimitiveTypeEnum::SByte): s8 Value [[name(Name)]]; + (PrimitiveTypeEnum::Single): float Value [[name(Name)]]; + (PrimitiveTypeEnum::TimeSpan): TimeSpan Value [[name(Name)]]; + (PrimitiveTypeEnum::DateTime): DateTime Value [[name(Name)]]; + (PrimitiveTypeEnum::UInt16): u16 Value [[name(Name)]]; + (PrimitiveTypeEnum::UInt32): u32 Value [[name(Name)]]; + (PrimitiveTypeEnum::UInt64): u64 Value [[name(Name)]]; + (PrimitiveTypeEnum::Null): {} + (PrimitiveTypeEnum::String): LengthPrefixedString Value [[name(Name)]]; + (_): std::error(std::format("Unexpected {}", primitiveTypeEnum)); + } + }else{ + Record record [[name(Name)]]; + } +}; + +struct ClassWithMembersAndTypes: RecordTypeEnumT{ + ClassInfo classInfo; + MemberTypeInfo memberTypeInfo; + s32 LibraryId; + Members members[classInfo.MemberCount]; +}; + +struct ClassWithMembers: RecordTypeEnumT{ + ClassInfo classInfo; + s32 LibraryId; + UntypedMember members[classInfo.MemberCount]; +}; + +struct SystemClassWithMembersAndTypes: RecordTypeEnumT{ + ClassInfo classInfo; + MemberTypeInfo memberTypeInfo; + Members members[classInfo.MemberCount]; +}; + +struct SystemClassWithMembers: RecordTypeEnumT{ + ClassInfo classInfo; + UntypedMember members[classInfo.MemberCount]; +}; + +struct ClassWithId: RecordTypeEnumT{ + s32 ObjectId; + s32 MetadataId; + if(!IsUpdatingTrackers && NeedUpdateTrackers){ + IsUpdatingTrackers = true; + _Trackers _trackers @ 0x0 in _TrackersSection; + IsUpdatingTrackers = false; + NeedUpdateTrackers = false; + } + match(_trackers.objs[MetadataId].objEnum){ + (_ObjEnum::ClassWithMembersAndTypes): { + u32 MemberCount = _trackers.objs[MetadataId].classWithMembersAndTypes.data.classInfo.MemberCount; + Members<_trackers.objs[MetadataId].classWithMembersAndTypes.data.classInfo.MemberNames[std::core::array_index()].String, _trackers.objs[MetadataId].classWithMembersAndTypes.data.memberTypeInfo> members[MemberCount]; + } + (_ObjEnum::SystemClassWithMembersAndTypes): { + u32 MemberCount = _trackers.objs[MetadataId].systemClassWithMemberAndTypes.data.classInfo.MemberCount; + Members<_trackers.objs[MetadataId].systemClassWithMemberAndTypes.data.classInfo.MemberNames[std::core::array_index()].String, _trackers.objs[MetadataId].systemClassWithMemberAndTypes.data.memberTypeInfo> members[MemberCount]; + } + (_ObjEnum::SystemClassWithMembers): { + str className = _trackers.objs[MetadataId].TypeName.data.String; + u32 MemberCount = _trackers.objs[MetadataId].systemClassWithMembers.data.classInfo.MemberCount; + UntypedMember members[MemberCount]; + } + (_ObjEnum::ClassWithMembers): { + str className = _trackers.objs[MetadataId].TypeName.data.String; + u32 MemberCount = _trackers.objs[MetadataId].classWithMembers.data.classInfo.MemberCount; + UntypedMember members[MemberCount]; + } + (_): std::error(std::format("Unexpected {}", _trackers.objs[MetadataId].objEnum)); + } +}; + +struct BinaryArray: RecordTypeEnumT{ + s32 ObjectId; + BinaryArrayTypeEnum binaryArrayTypeEnum; + s32 Rank; + s32 Length[Rank]; + if(binaryArrayTypeEnum == BinaryArrayTypeEnum::SingleOffset || + binaryArrayTypeEnum == BinaryArrayTypeEnum::JaggedOffset || + binaryArrayTypeEnum == BinaryArrayTypeEnum::RectangularOffset) + s32 LowerBounds[Rank]; + BinaryTypeEnum TypeEnum; + AdditionalInfo additionalInfo; +}; + +struct ArraySinglePrimitive: RecordTypeEnumT{ + ArrayInfo arrayInfo; + PrimitiveTypeEnum primitiveTypeEnum; + std::assert(primitiveTypeEnum != PrimitiveTypeEnum::Null && + primitiveTypeEnum != PrimitiveTypeEnum::String, "Can't be one of those"); + match(primitiveTypeEnum){ + (PrimitiveTypeEnum::Boolean): bool Values[arrayInfo.Length]; + (PrimitiveTypeEnum::Byte): u8 Value[arrayInfo.Length]; + (PrimitiveTypeEnum::Char): char Value[arrayInfo.Length]; + (PrimitiveTypeEnum::Currency): std::error("Primitive currency not used in this protocol"); + (PrimitiveTypeEnum::Decimal): Decimal Value[arrayInfo.Length]; + (PrimitiveTypeEnum::Double): double Value[arrayInfo.Length]; + (PrimitiveTypeEnum::Int16): s16 Value[arrayInfo.Length]; + (PrimitiveTypeEnum::Int32): s32 Value[arrayInfo.Length]; + (PrimitiveTypeEnum::Int64): s64 Value[arrayInfo.Length]; + (PrimitiveTypeEnum::SByte): s8 Value[arrayInfo.Length]; + (PrimitiveTypeEnum::Single): float Value[arrayInfo.Length]; + (PrimitiveTypeEnum::TimeSpan): TimeSpan Value[arrayInfo.Length]; + (PrimitiveTypeEnum::DateTime): DateTime Value[arrayInfo.Length]; + (PrimitiveTypeEnum::UInt16): u16 Value[arrayInfo.Length]; + (PrimitiveTypeEnum::UInt32): u32 Value[arrayInfo.Length]; + (PrimitiveTypeEnum::UInt64): u64 Value[arrayInfo.Length]; + (PrimitiveTypeEnum::Null): {} + (PrimitiveTypeEnum::String): LengthPrefixedString Value[arrayInfo.Length]; + (_): std::error(std::format("Unexpected {}", primitiveTypeEnum)); + } +}; + +u64 ArraySinglElementSkipCount = 0; +struct ArraySingleElement{ + RecordTypeEnum recordTypeEnum = _recordTypeEnum; + if(ArraySinglElementSkipCount > 0) + ArraySinglElementSkipCount -= 1; + else{ + if(recordTypeEnum == RecordTypeEnum::ArraySingleString){ + Record record [[inline]]; + match(record.recordTypeEnum){ + (RecordTypeEnum::ObjectNullMultiple): + ArraySinglElementSkipCount = record.objectNullMultiple.NullCount; + (RecordTypeEnum::ObjectNullMultiple256): + ArraySinglElementSkipCount = record.objectNullMultiple256.NullCount; + (_): {} + } + }else{ + Record record [[inline]]; + if(record.recordTypeEnum == RecordTypeEnum::BinaryLibrary){ + Record record2 [[inline]]; + match(record2.recordTypeEnum){ + (RecordTypeEnum::ObjectNullMultiple): + ArraySinglElementSkipCount = record2.objectNullMultiple.NullCount; + (RecordTypeEnum::ObjectNullMultiple256): + ArraySinglElementSkipCount = record2.objectNullMultiple256.NullCount; + (_): {} + } + }else{ + match(record.recordTypeEnum){ + (RecordTypeEnum::ObjectNullMultiple): + ArraySinglElementSkipCount = record.objectNullMultiple.NullCount; + (RecordTypeEnum::ObjectNullMultiple256): + ArraySinglElementSkipCount = record.objectNullMultiple256.NullCount; + (_): {} + } + } + } + } +}; + +struct ArraySingleObject: RecordTypeEnumT{ + ArrayInfo arrayInfo; + ArraySinglElementSkipCount = 0; + ArraySingleElement records[arrayInfo.Length]; +}; + +struct ArraySingleString: RecordTypeEnumT{ + ArrayInfo arrayInfo; + ArraySinglElementSkipCount = 0; + ArraySingleElement records[arrayInfo.Length]; +}; + +struct MethodReturnCallArray{ + if(parent.MessageEnum.ReturnValueInArray) + ArraySingleObject ReturnValue; + if(parent.MessageEnum.ArgsInArray) + ArraySingleObject OutputArguments; + if(parent.MessageEnum.ExceptionInArray) + ArraySingleObject Exception; + if(parent.MessageEnum.ContextInArray) + ArraySingleObject CallContext; + if(parent.MessageEnum.PropertiesInArray) + ArraySingleObject MessageProperties; +}; + +struct MethodCallArray{ + if(parent.MessageEnum.ArgsInArray) + ArraySingleObject InputArguments; + if(parent.MessageEnum.GenericMethod) + ArraySingleObject GenericTypeArguments; + if(parent.MessageEnum.MethodSignatureInArray) + ArraySingleObject MethodSignature; + if(parent.MessageEnum.ContextInArray) + ArraySingleObject CallContext; + if(parent.MessageEnum.PropertiesInArray) + ArraySingleObject MessageProperties; +}; + +struct BinaryMethodReturn: RecordTypeEnumT{ + MessageFlags MessageEnum; + std::assert(validate_MessageFlags(MessageEnum) >= 0, "Validation Failed"); + std::assert(!MessageEnum.MethodSignatureInArray && !MessageEnum.GenericMethod, "Can't be one of those"); + if(MessageEnum.ReturnValueInline) + ValueWithCode ReturnValue; + if(MessageEnum.ContextInline) + StringValueWithCode CallContext; + if(MessageEnum.ArgsInline) + ArrayOfValueWithCode Args; + MethodReturnCallArray ReturnCallArray; +}; + +struct BinaryMethodCall: RecordTypeEnumT{ + MessageFlags MessageEnum; + std::assert(validate_MessageFlags(MessageEnum) >= 0, "Validation Failed"); + std::assert(!MessageEnum.NoReturnValue && !MessageEnum.ReturnValueVoid && + !MessageEnum.ReturnValueInline && !MessageEnum.ReturnValueInArray && + !MessageEnum.ExceptionInArray, "Can't be one of those"); + StringValueWithCode MethodName; + StringValueWithCode TypeName; + if(MessageEnum.ContextInline) + StringValueWithCode CallContext; + if(MessageEnum.ArgsInline) + ArrayOfValueWithCode Args; + MethodCallArray CallArray; +}; + +struct MemberPrimitiveTyped: RecordTypeEnumT{ + PrimitiveTypeEnum primitiveTypeEnum; + std::assert(primitiveTypeEnum != PrimitiveTypeEnum::Null && + primitiveTypeEnum != PrimitiveTypeEnum::String, "Can't be one of those"); + match(primitiveTypeEnum){ + (PrimitiveTypeEnum::Boolean): bool Value; + (PrimitiveTypeEnum::Byte): u8 Value; + (PrimitiveTypeEnum::Char): char Value; + (PrimitiveTypeEnum::Currency): std::error("Primitive currency not used in this protocol"); + (PrimitiveTypeEnum::Decimal): Decimal Value; + (PrimitiveTypeEnum::Double): double Value; + (PrimitiveTypeEnum::Int16): s16 Value; + (PrimitiveTypeEnum::Int32): s32 Value; + (PrimitiveTypeEnum::Int64): s64 Value; + (PrimitiveTypeEnum::SByte): s8 Value; + (PrimitiveTypeEnum::Single): float Value; + (PrimitiveTypeEnum::TimeSpan): TimeSpan Value; + (PrimitiveTypeEnum::DateTime): DateTime Value; + (PrimitiveTypeEnum::UInt16): u16 Value; + (PrimitiveTypeEnum::UInt32): u32 Value; + (PrimitiveTypeEnum::UInt64): u64 Value; + (_): std::error(std::format("Unexpected {}", primitiveTypeEnum)); + } +}; + +struct MemberPrimitiveUnTyped{ + match(PrimitiveType){ + (PrimitiveTypeEnum::Boolean): bool Value; + (PrimitiveTypeEnum::Byte): u8 Value; + (PrimitiveTypeEnum::Char): char Value; + (PrimitiveTypeEnum::Currency): std::error("Primitive currency not used in this protocol"); + (PrimitiveTypeEnum::Decimal): Decimal Value; + (PrimitiveTypeEnum::Double): double Value; + (PrimitiveTypeEnum::Int16): s16 Value; + (PrimitiveTypeEnum::Int32): s32 Value; + (PrimitiveTypeEnum::Int64): s64 Value; + (PrimitiveTypeEnum::SByte): s8 Value; + (PrimitiveTypeEnum::Single): float Value; + (PrimitiveTypeEnum::TimeSpan): TimeSpan Value; + (PrimitiveTypeEnum::DateTime): DateTime Value; + (PrimitiveTypeEnum::UInt16): u16 Value; + (PrimitiveTypeEnum::UInt32): u32 Value; + (PrimitiveTypeEnum::UInt64): u64 Value; + (PrimitiveTypeEnum::Null): {} + (PrimitiveTypeEnum::String):std::error("Can't be String"); + (_): std::error(std::format("Unexpected {}", PrimitiveType)); + } +}; + +struct MemberReference: RecordTypeEnumT{ + s32 IdRef; +}; + +struct ObjectNull: RecordTypeEnumT{ +}; + +struct ObjectNullMultiple: RecordTypeEnumT{ + s32 NullCount; +}; + +struct ObjectNullMultiple256: RecordTypeEnumT{ + u8 NullCount; +}; + +struct BinaryObjectString: RecordTypeEnumT{ + s32 ObjectId; + LengthPrefixedString Value; +}; + +struct SerializationHeaderRecord: RecordTypeEnumT{ + s32 RootId; + s32 HeaderId; + s32 MajorVersion; + std::assert_warn(MajorVersion == 1, "MajorVersion should be 1"); + s32 MinorVersion; + std::assert_warn(MinorVersion == 0, "MinorVersion should be 0"); +}; + +struct BinaryLibrary: RecordTypeEnumT{ + s32 LibraryId; + LengthPrefixedString LibraryName; +}; + +struct MessageEnd: RecordTypeEnumT{ +}; + +struct CrossAppDomainMap: RecordTypeEnumT{ + s32 crossAppDomainArrayIndex; +}; + +struct CrossAppDomainString: RecordTypeEnumT{ + s32 ObjectId; + s32 Value; +}; + +struct CrossAppDomainAssembly: RecordTypeEnumT{ + s32 LibraryId; + s32 LibraryIndex; +}; + +struct Record{ + RecordTypeEnum recordTypeEnum [[no_unique_address, hidden]]; + match(recordTypeEnum){ + (RecordTypeEnum::SerializedStreamHeader): + std::error("SerializationStreamHeader can only appear at the top of the document"); + (RecordTypeEnum::ClassWithId): { /* RecordTypeEnum::Object */ + ClassWithId classWithId; + if(!IsUpdatingTrackers){ + zeroedCurrObjTrackers(); + _trackers.currentObj.objEnum = _ObjEnum::ClassWithId; + _trackers.currentObj.RawPtr = append_value_to_section(classWithId, _TrackersSection); + _trackers.currentObj.TypeName.pointerValue = _trackers.objs[classWithId.MetadataId].TypeName.pointerValue; + _trackers.currentObj.AssemblyName.pointerValue =_trackers.objs[classWithId.MetadataId].AssemblyName.pointerValue ; + if(classWithId.ObjectId != 0) + copyCurrObjAtIdTrackers(classWithId.ObjectId); + } + } + (RecordTypeEnum::SystemClassWithMembers): { /* RecordTypeEnum::ObjectWithMap */ + SystemClassWithMembers systemClassWithMembers; + if(!IsUpdatingTrackers){ + zeroedCurrObjTrackers(); + _trackers.currentObj.objEnum = _ObjEnum::SystemClassWithMembers; + _trackers.currentObj.RawPtr = append_value_to_section(systemClassWithMembers, _TrackersSection); + _trackers.currentObj.TypeName.pointerValue = _trackers.currentObj.RawPtr + offsetOf(systemClassWithMembers, systemClassWithMembers.classInfo.Name); + if(systemClassWithMembers.classInfo.ObjectId != 0) + copyCurrObjAtIdTrackers(systemClassWithMembers.classInfo.ObjectId); + } + } + (RecordTypeEnum::ClassWithMembers): { /* RecordTypeEnum::ObjectWithMapAssemId */ + ClassWithMembers classWithMembers; + if(!IsUpdatingTrackers){ + zeroedCurrObjTrackers(); + _trackers.currentObj.objEnum = _ObjEnum::ClassWithMembers; + _trackers.currentObj.RawPtr = append_value_to_section(classWithMembers, _TrackersSection); + _trackers.currentObj.TypeName.pointerValue = _trackers.currentObj.RawPtr + offsetOf(classWithMembers, classWithMembers.classInfo.Name); + _trackers.currentObj.AssemblyName.pointerValue = _trackers.libs[classWithMembers.LibraryId].pointerValue; + if(classWithMembers.classInfo.ObjectId != 0) + copyCurrObjAtIdTrackers(classWithMembers.classInfo.ObjectId); + } + } + (RecordTypeEnum::SystemClassWithMembersAndTypes): { /* RecordTypeEnum::ObjectWithMapTyped */ + SystemClassWithMembersAndTypes systemClassWithMembersAndTypes; + if(!IsUpdatingTrackers){ + zeroedCurrObjTrackers(); + _trackers.currentObj.objEnum = _ObjEnum::SystemClassWithMembersAndTypes; + _trackers.currentObj.RawPtr = append_value_to_section(systemClassWithMembersAndTypes, _TrackersSection); + _trackers.currentObj.TypeName.pointerValue = _trackers.currentObj.RawPtr + offsetOf(systemClassWithMembersAndTypes, systemClassWithMembersAndTypes.classInfo.Name); + if(systemClassWithMembersAndTypes.classInfo.ObjectId != 0) + copyCurrObjAtIdTrackers(systemClassWithMembersAndTypes.classInfo.ObjectId); + } + } + (RecordTypeEnum::ClassWithMembersAndTypes): { /* RecordTypeEnum::ObjectWithMapTypedAssemId */ + ClassWithMembersAndTypes classWithMembersAndTypes; + if(!IsUpdatingTrackers){ + zeroedCurrObjTrackers(); + _trackers.currentObj.objEnum = _ObjEnum::ClassWithMembersAndTypes; + _trackers.currentObj.RawPtr = append_value_to_section(classWithMembersAndTypes, _TrackersSection); + _trackers.currentObj.TypeName.pointerValue = _trackers.currentObj.RawPtr + offsetOf(classWithMembersAndTypes, classWithMembersAndTypes.classInfo.Name); + _trackers.currentObj.AssemblyName.pointerValue = _trackers.libs[classWithMembersAndTypes.LibraryId].pointerValue; + if(classWithMembersAndTypes.classInfo.ObjectId != 0) + copyCurrObjAtIdTrackers(classWithMembersAndTypes.classInfo.ObjectId); + } + } + (RecordTypeEnum::BinaryObjectString): { /* RecordTypeEnum::ObjectString */ + BinaryObjectString binaryObjectString; + if(!IsUpdatingTrackers){ + zeroedCurrObjTrackers(); + _trackers.currentObj.objEnum = _ObjEnum::BinaryObjectString; + _trackers.currentObj.RawPtr = append_value_to_section(binaryObjectString, _TrackersSection); + if(binaryObjectString.ObjectId != 0) + copyCurrObjAtIdTrackers(binaryObjectString.ObjectId); + } + } + (RecordTypeEnum::BinaryArray): { /* RecordTypeEnum::Array */ + BinaryArray binaryArray; + if(!IsUpdatingTrackers){ + zeroedCurrObjTrackers(); + _trackers.currentObj.objEnum = _ObjEnum::BinaryArray; + _trackers.currentObj.RawPtr = append_value_to_section(binaryArray, _TrackersSection); + if(binaryArray.ObjectId != 0) + copyCurrObjAtIdTrackers(binaryArray.ObjectId); + } + } + (RecordTypeEnum::MemberPrimitiveTyped): { + MemberPrimitiveTyped memberPrimitiveTyped; + } + (RecordTypeEnum::MemberReference): { + MemberReference memberReference; + } + (RecordTypeEnum::ObjectNull): {} + (RecordTypeEnum::MessageEnd): break; + (RecordTypeEnum::BinaryLibrary): { /* RecordTypeEnum::Assembly */ + BinaryLibrary library; + if(!IsUpdatingTrackers) + _trackers.libs[library.LibraryId].pointerValue = append_value_to_section(library.LibraryName, _TrackersSection); + } + (RecordTypeEnum::ObjectNullMultiple256): { + ObjectNullMultiple256 objectNullMultiple256; + } + (RecordTypeEnum::ObjectNullMultiple): { + ObjectNullMultiple objectNullMultiple; + } + (RecordTypeEnum::ArraySinglePrimitive): { + ArraySinglePrimitive arraySinglePrimitive; + if(!IsUpdatingTrackers){ + zeroedCurrObjTrackers(); + _trackers.currentObj.objEnum = _ObjEnum::ArraySinglePrimitive; + _trackers.currentObj.RawPtr = append_value_to_section(arraySinglePrimitive, _TrackersSection); + if(arraySinglePrimitive.arrayInfo.ObjectId != 0) + copyCurrObjAtIdTrackers(arraySinglePrimitive.arrayInfo.ObjectId); + } + } + (RecordTypeEnum::ArraySingleObject): { + ArraySingleObject arraySingleObject; + } + (RecordTypeEnum::ArraySingleString): { + ArraySingleString arraySingleString; + } + (RecordTypeEnum::CrossAppDomainMap): + CrossAppDomainMap crossAppDomainMap; + (RecordTypeEnum::CrossAppDomainString): + CrossAppDomainString crossAppDomainString; + (RecordTypeEnum::CrossAppDomainAssembly): + CrossAppDomainAssembly crossAppDomainAssembly; + (RecordTypeEnum::MethodCall): { + BinaryMethodCall binaryMethodCall; + } + (RecordTypeEnum::MethodReturn): { + BinaryMethodReturn binaryMethodReturn; + } + (_): std::error(std::format("Unrecognized {}", recordTypeEnum)); + } +}; + +struct DotNetBinnaryFormatter{ + SerializationHeaderRecord Header; + Record records[while(std::mem::read_unsigned($, 1) != RecordTypeEnum::MessageEnd)]; + MessageEnd End; + if(!IsUpdatingTrackers && NeedUpdateTrackers){ + IsUpdatingTrackers = true; + _Trackers _trackers @ 0x0 in _TrackersSection; + IsUpdatingTrackers = false; + NeedUpdateTrackers = false; + } +}; + +struct _ObjTracker{ + _Ptr TypeName; + _Ptr AssemblyName; + _ObjEnum objEnum; + u64 RawPtr [[no_unique_address]]; + /* Only enable the one we actually use to avoid significant slow down */ + match(objEnum){ + (_ObjEnum::Empty): + u64 ptr; + /* + (_ObjEnum::ClassWithId): // _ObjEnum::Object + _Ptr classWithId; + */ + (_ObjEnum::SystemClassWithMembers): // _ObjEnum::ObjectWithMap + _Ptr systemClassWithMembers; + (_ObjEnum::ClassWithMembers): // _ObjEnum::ObjectWithMapAssemId + _Ptr classWithMembers; + (_ObjEnum::SystemClassWithMembersAndTypes): // _ObjEnum::ObjectWithMapTyped + _Ptr systemClassWithMemberAndTypes; + (_ObjEnum::ClassWithMembersAndTypes): // _ObjEnum::ObjectWithMapTypedAssemId + _Ptr classWithMembersAndTypes; + /* + (_ObjEnum::BinaryObjectString): // _ObjEnum::ObjectString + _Ptr binaryObjectString; + (_ObjEnum::BinaryArray): // _ObjEnum::Array + _Ptr binaryArray; + (_ObjEnum::MemberPrimitiveTyped): + _Ptr memberPrimitiveTyped; + (_ObjEnum::MemberReference): + _Ptr memberReference; + (_ObjEnum::BinaryLibrary): // _ObjEnum::Assembly + _Ptr library; + (_ObjEnum::ObjectNullMultiple256): + _Ptr objectNullMultiple256; + (_ObjEnum::ObjectNullMultiple): + _Ptr objectNullMultiple; + (_ObjEnum::ArraySinglePrimitive): + _Ptr arraySinglePrimitive; + (_ObjEnum::ArraySingleObject): + _Ptr arraySingleObject; + (_ObjEnum::ArraySingleString): + _Ptr arraySingleString; + (_ObjEnum::CrossAppDomainMap): + _Ptr crossAppDomainMap; + (_ObjEnum::CrossAppDomainString): + _Ptr crossAppDomainString; + (_ObjEnum::CrossAppDomainAssembly): + _Ptr crossAppDomainAssembly; + (_ObjEnum::MethodCall): + _Ptr binaryMethodCall; + (_ObjEnum::MethodReturn): + _Ptr binaryMethodReturn; + */ + (_): u64 ptr; //std::error(std::format("Unexpected {}", objEnum)); + } +}; + +/* Should be an In variable with default non zero value in the future until we use a hash map */ +#define _OBJECTS_TRACKER_CNT 232 +#define _LIBRARIES_CNT 8 + +struct _Trackers{ + _ObjTracker currentObj; + /* TODO: this should really be an hash map, the algorithm that generated is unspecified */ + _ObjTracker objs[_OBJECTS_TRACKER_CNT]; + _Ptr libs[_LIBRARIES_CNT]; +}; + +std::mem::set_section_size(_TrackersSection, sizeof(_trackers)); + +DotNetBinnaryFormatter dotNetBinnaryFormatter @ 0x0; diff --git a/tests/patterns/test_data/dotnet_binaryformatter.hexpat.bin b/tests/patterns/test_data/dotnet_binaryformatter.hexpat.bin new file mode 100644 index 0000000000000000000000000000000000000000..a3bb135c16f1a33b8949bf6a42bce0745d22ff55 GIT binary patch literal 372 zcmb_XO=|)%5M4huQtUy+o_fg-Sc0xr^soo9r5pkjkp^VjrtW>wN}bQU60)3_Gt$vhuVIZfKo}6qHHSg7w{Rs(#`lg z_*y$#OvOaxi#!*z=`3Gld=m@?4~2z*UYd-*gioUn&(OTNBiOyf6b<>X=B{{?^2XpH~> literal 0 HcmV?d00001 From 284ca8d3259760ebdfa22dcea835c84ed8b95d5a Mon Sep 17 00:00:00 2001 From: Marc Jones <14953199+marcj303@users.noreply.github.com> Date: Sun, 31 Aug 2025 03:38:32 -0600 Subject: [PATCH 35/93] patterns: Add UEFI Firmare Volume Variable Store pattern (#421) * Add UEFI Firmare Volume Variable Store pattern Add a pattern for UEFI Firmare Volume Variable store. This file type is commonly used with virtual machine UEFI variable files, like OVMF.fd used with QEMU. You could also extract a UEFI firmware binary from a flash device, search for the FV Variable Store, and set this pattern to the FV address. Signed-off-by: Marc Jones * Fixed description pragma --------- Signed-off-by: Marc Jones Co-authored-by: Nik --- README.md | 1 + patterns/uefi_fv_varstore.hexpat | 212 ++++++++++++++++++ .../test_data/uefi_fv_varstore.hexpat.fd | Bin 0 -> 540672 bytes 3 files changed, 213 insertions(+) create mode 100644 patterns/uefi_fv_varstore.hexpat create mode 100644 tests/patterns/test_data/uefi_fv_varstore.hexpat.fd diff --git a/README.md b/README.md index 74b341ce..29a71bd9 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | UPK | | [`patterns/upk-ue3.hexpat`](patterns/upk-ue3.hexpat) | Unreal Engine 3 UPK file | | UEFI | | [`patterns/uefi.hexpat`](patterns/uefi.hexpat)` | UEFI structs for parsing efivars | | UEFI Boot Entry | | [`patterns/uefi_boot_entry.hexpat`](patterns/uefi_boot_entry.hexpat) | UEFI Boot Entry (Load option) | +| UEFI Variable Store | | [`patterns/uefi_fv_varstore.hexpat`](patterns/uefi_fv_varstore.hexpat) | UEFI Firmware Volume Variable Store | | UF2 | | [`patterns/uf2.hexpat`](patterns/uf2.hexpat) | [USB Flashing Format](https://github.com/microsoft/uf2) | | Valve VPK | | [`patterns/valve_vpk.hexpat`](valve_vpk.hexpat) | Valve Package File | | VBMeta | | [`patterns/vbmeta.hexpat`](patterns/vbmeta.hexpat) | Android VBMeta image | diff --git a/patterns/uefi_fv_varstore.hexpat b/patterns/uefi_fv_varstore.hexpat new file mode 100644 index 00000000..e5b3248c --- /dev/null +++ b/patterns/uefi_fv_varstore.hexpat @@ -0,0 +1,212 @@ +/* + * ImHex Pattern for UEFI Firmware Volume Variable Store + * + * This file type is commonly used with virtual machine UEFI variable files, like OVMF.fd + * used with QEMU. You could also extract a UEFI firmware binary from a flash device, + * search for the FV Variable Store, and set this pattern to the FV address. + * + * A 'custom_vars.fd' can be generated with these tools: + * + * https://gitlab.com/kraxel/virt-firmware + * https://github.com/rhuefi/qemu-ovmf-secureboot/tree/master + * https://github.com/LongSoft/UEFITool + * + * 1. Generate a blank .fd file with ovmfvartool. + * + * $ ovmfvartool generate-blank blank.fd + * + * 2. Enroll the Redhat and Microsoft keys with virt-fw-vars in custom_vars.fd. + * + * $ virt-fw-vars -i blank.fd -o custom_vars.fd --enroll-redhat --secure-boot + * + * 3. Dump custom_vars.fd contents + * + * $virt-fw-vars -i custom_vars.fd -pvx + * + * or + * + * $ uefitool custom_vars.fd + * + * or use this pattern with ImHex! + */ + +#pragma author Marc Jones +#pragma description UEFI Firmware Volume Variable Store +// #pragma debug + +import std.core; +import std.mem; +import type.guid; + + +// --- GUIDs --- + +#define NVRAM_FV "{FFF12B8D-7696-4C8B-A985-2747075B4F50}" +#define NVRAM_VARSTORE "{AAF32C78-947B-439A-A180-2E144EC37792}" + + +// --- Enumerations and Bitfields --- + +// Describes the type of a file within the Firmware File System. +enum FfsFileType : u8 { + RAW = 0x01, + FREEFORM = 0x02, + SECURITY_CORE = 0x03, + PEI_CORE = 0x04, + DXE_CORE = 0x05, + PEIM = 0x06, + DRIVER = 0x07, + COMBINED_PEIM_DRIVER = 0x08, + APPLICATION = 0x09, + SMM = 0x0A, + FIRMWARE_VOLUME_IMAGE = 0x0B, + COMBINED_SMM_DXE = 0x0C, + SMM_CORE = 0x0D, + FFS_PAD = 0xF0, +}; + +// Attributes for a UEFI variable, indicating its properties and accessibility. +bitfield VariableAttributes{ + NON_VOLATILE : 1; + BOOTSERVICE_ACCESS : 1; + RUNTIME_ACCESS : 1; + HARDWARE_ERROR_RECORD : 1; + AUTHENTICATED_WRITE_ACCESS : 1; + TIME_BASED_AUTHENTICATED_WRITE_ACCESS : 1; + APPEND_WRITE : 1; + RSVD: 25; +}; + +// +// Variable Store Header Format & State flags +// +enum VariableStoreFormat : u8 { + VARIABLE_STORE_FORMATTED = 0x5a, +}; + +enum VariableStoreState : u8 { + VARIABLE_STORE_HEALTHY = 0xfe, +}; + + +// +// Variable State flags. See https://countchu.blogspot.com/2014/09/the-life-cycle-of-uefi-variable-in.html +// +enum VariableState : u8 { + VAR_IN_DELETED_TRANSITION = 0xfe, + VAR_DELETED = 0xfd, + VAR_HEADER_VALID_ONLY = 0x7f, + VAR_ADDED = 0x3f, + VAR_ADDED__VAR_IN_DELETED_TRANSITION__VAR_DELETED = 0x3c, + VAR_ADDED__VAR_IN_DELETED_TRANSITION = 0x3e, + VAR_ADDED__VAR_DELETED = 0x3d, +}; + + +// --- Other Structures --- + +struct EFI_TIME { + u16 Year; + u8 Month; + u8 Day; + u8 Hour; + u8 Minute; + u8 Second; + u8 Pad1; + u32 Nanosecond; + u16 TimeZone; + u8 Daylight; + u8 Pad2; +}; + + +// --- Firmware Volume Structures --- + +// Header for a block in the firmware volume map. +struct EFI_FV_BLOCK_MAP_ENTRY { + u32 NumBlocks; + u32 Length; +}; + +// The main header for a Firmware Volume. +struct EFI_FIRMWARE_VOLUME_HEADER { + u128 ZeroVector; + type::GUID FileSystemGuid; + u64 FvLength; + u32 Signature; + u32 Attributes; + u16 HeaderLength; + u16 Checksum; + u16 ExtHeaderOffset; + u8 Reserved; + u8 Revision; + EFI_FV_BLOCK_MAP_ENTRY BlockMap[while(std::mem::read_unsigned($, 4) != 0 || std::mem::read_unsigned($ + 4, 4) != 0)]; + EFI_FV_BLOCK_MAP_ENTRY BlockMapTerminator; // After the loop, explicitly parse the (0,0) terminator entry +}[[single_color]]; + + +// --- UEFI Variable Structures --- + +struct VARIABLE_STORE_HEADER { + type::GUID Signature; + u32 Size; + VariableStoreFormat Format; + VariableStoreState State; + u16 Reserved; + u32 Reserved1; +}[[single_color]]; + + +#define VAR_START_ID 0x55AA + +struct VARIABLE_HEADER { + u16 StartId; + VariableState State; + u8 Reserved; + VariableAttributes Attributes; + u32 NameSize; + u32 DataSize; + type::GUID VendorGuid; +}; + +struct AUTHENTICATED_VARIABLE_HEADER { + u16 StartId; + VariableState State; + u8 Reserved; + VariableAttributes Attributes; + u64 MonotonicCount; + EFI_TIME TimeStamp; + u32 PubKeyIndex; + u32 NameSize; + u32 DataSize; + type::GUID VendorGuid; +}; + +struct UEFI_VARIABLE { + AUTHENTICATED_VARIABLE_HEADER Header; // TODO: Check authenticated vs normal variable... + char16 Name[Header.NameSize / 2]; // Name is a UTF-16 string + u8 Data[Header.DataSize]; + // Align to the next 4-byte boundary for the next variable. + u8 pad[std::mem::align_to(4, sizeof(this)) - sizeof(this)]; +} [[name(this.Name), single_color]]; + + +// --- Main Pattern Entry Point --- + +EFI_FIRMWARE_VOLUME_HEADER FV_Header @ 0; + +if (std::core::formatted_value(FV_Header.FileSystemGuid) != "{FFF12B8D-7696-4C8B-A985-2747075B4F50}") { + std::error(std::format("Unknown FV_Header.FileSystemGuid {}", std::core::formatted_value(FV_Header.FileSystemGuid))); +} + +// The next structure should be the Variable Store Header +VARIABLE_STORE_HEADER VarStore @ $; + +if (std::core::formatted_value(VarStore.Signature) != NVRAM_VARSTORE) { + std::error(std::format("Unknown VarStore.Signature {}", std::core::formatted_value(VarStore.Signature))); +} + +// Index through the Uefi variables until we don't find a Variable Signature 0x55AA +UEFI_VARIABLE UefiVars[while(std::mem::read_unsigned($, 2) == VAR_START_ID)] @ $; + +// TODO: grey out the Uefi variables that are in the non-active state, != VAR_ADDED. diff --git a/tests/patterns/test_data/uefi_fv_varstore.hexpat.fd b/tests/patterns/test_data/uefi_fv_varstore.hexpat.fd new file mode 100644 index 0000000000000000000000000000000000000000..b3ee104808ee9a04c2c7aa83feca9f3adde29d42 GIT binary patch literal 540672 zcmeIuF-TQW7zglU3K~U8h!EBik%kgY4T7XTErVA{p+rSdJ*gpkWo1GI4Ml^rXiCo@ z4I5MPzwihqtx2C&^q+6#QMVvk%RhEMtva1ajC7s(bg-#+I1$c=v*BDwht9Aw+Ebw; z{yz6Vnd{~=pD!EzTr%0-oS6xwOJCMD?Ts>yMY&zkvoXp{NBPa+R5%ijhLidHf_#2u zeA9f*78k_7x1eH~WVBvATK%@RZm@ZDYWB^F;~nW#l+zRSH~@6L%!mv@Z3?g+nzD+X#NYf^Jj{=1bSUyFE^ zC;|is5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkKc HkpzANH80}V literal 0 HcmV?d00001 From d95390ea42a30a4293c522c928af3ce2d6ef0f9e Mon Sep 17 00:00:00 2001 From: Kris Dekeyser Date: Sun, 31 Aug 2025 11:40:27 +0200 Subject: [PATCH 36/93] patterns/jpeg: added support for extra data in APP0 section (#417) Apple Multi-Picture Format JPEGs often have 4 extra bytes in the APP0 section. The pattern now skips any extra bytes beyond the fixed APP0 data. Co-authored-by: Nik --- patterns/jpeg.hexpat | 1 + 1 file changed, 1 insertion(+) diff --git a/patterns/jpeg.hexpat b/patterns/jpeg.hexpat index 1e7269f6..76860bfc 100644 --- a/patterns/jpeg.hexpat +++ b/patterns/jpeg.hexpat @@ -145,6 +145,7 @@ struct Segment { u16 length; if (marker == Marker::APP0) { APP0 data; + u8 xtra_data[length - sizeof(length) - sizeof(data)]; } else if (marker == Marker::APP14) { APP14 data; } else if (marker == Marker::COM) { From fb214600ec0bd4aeba898b0aba97ba7d94a8849a Mon Sep 17 00:00:00 2001 From: Ivy Fan-Chiang Date: Sun, 31 Aug 2025 08:32:21 -0400 Subject: [PATCH 37/93] patterns/ico: Add embedded BMP and PNG parsing to ICO pattern (#426) Co-authored-by: Nik --- patterns/ico.hexpat | 69 +++++++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/patterns/ico.hexpat b/patterns/ico.hexpat index 15ab4708..041fdc21 100644 --- a/patterns/ico.hexpat +++ b/patterns/ico.hexpat @@ -4,6 +4,10 @@ #pragma endian little import std.sys; +import std.mem; +import std.string; +import * from bmp as BMP; +import * from png as PNG; #pragma MIME image/vnd.microsoft.icon #pragma MIME image/x-icon @@ -13,38 +17,61 @@ import std.sys; #pragma MIME application/ico enum ImageType : u16 { - Icon = 1, - Cursor = 2 + Icon = 1, + Cursor = 2 }; struct ICONDIR { - u16 reserved [[hidden]]; - ImageType type; - u16 num_images; + u16 reserved [[hidden]]; + ImageType type; + u16 num_images; +}; + +struct BMPData { + u8 data[sizeof(parent.data)] [[hidden, no_unique_address]]; + std::mem::Section section = std::mem::create_section("BMP Image " + std::string::to_string(addressof(this))); + std::mem::set_section_size(section, sizeof(data) + 14); + u8 headerData[14] @ 0x00 in section [[hidden]]; + headerData[0x00] = 0x42; + headerData[0x01] = 0x4D; + u32 size @ 0x02 in section [[hidden]]; + size = std::mem::get_section_size(section); + std::mem::copy_value_to_section(data, section, 0x0E); + BMP image @ 0x00 in section; + u32 offset @ 0x0D in section [[hidden]]; + offset = addressof(image.lineData); }; struct ImageData { - u8 data[parent.image_data_size] [[inline]]; + std::mem::Bytes data [[no_unique_address]]; + be u64 png_magic [[hidden, no_unique_address]]; + if (png_magic == 0x89504e470d0a1a0a) { + PNG png_image @ $ [[inline, highlight_hidden]]; + } else { + BMPData data [[hidden]]; + BMP bmp_image @ addressof(this) - 14 [[inline, highlight_hidden]]; + } }; struct ICONDIRENTRY { - u8 width, height; - u8 num_colors; - u8 reserved [[hidden]]; - - if (header.type == ImageType::Icon) { - u16 color_planes; - u16 bits_per_pixel; - } else if (header.type == ImageType::Cursor) { - u16 horizontal_hotspot_coordinate; - u16 vertical_hotspot_coordinate; - } - - u32 image_data_size; - ImageData *image_data : u32; + u8 width, height; + u8 num_colors; + u8 reserved [[hidden]]; + + if (header.type == ImageType::Icon) { + u16 color_planes; + u16 bits_per_pixel; + } else if (header.type == ImageType::Cursor) { + u16 horizontal_hotspot_coordinate; + u16 vertical_hotspot_coordinate; + } + + u32 image_data_size; + u32 image_data_offset; + ImageData image_data @ image_data_offset; }; ICONDIR header @ 0x00; ICONDIRENTRY images[header.num_images] @ $; -std::assert(header.reserved == 0x00, "Invalid ICO header"); \ No newline at end of file +std::assert(header.reserved == 0x00, "Invalid ICO header"); From 75bcb487ee00cc147fc94f32f789dc37b160cbf8 Mon Sep 17 00:00:00 2001 From: Nik Date: Mon, 1 Sep 2025 22:22:43 +0200 Subject: [PATCH 38/93] patterns/ico: Fixed duplicate variable name --- patterns/ico.hexpat | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patterns/ico.hexpat b/patterns/ico.hexpat index 041fdc21..91eaa29a 100644 --- a/patterns/ico.hexpat +++ b/patterns/ico.hexpat @@ -28,7 +28,7 @@ struct ICONDIR { }; struct BMPData { - u8 data[sizeof(parent.data)] [[hidden, no_unique_address]]; + u8 data[sizeof(parent.raw_data)] [[hidden, no_unique_address]]; std::mem::Section section = std::mem::create_section("BMP Image " + std::string::to_string(addressof(this))); std::mem::set_section_size(section, sizeof(data) + 14); u8 headerData[14] @ 0x00 in section [[hidden]]; @@ -43,7 +43,7 @@ struct BMPData { }; struct ImageData { - std::mem::Bytes data [[no_unique_address]]; + std::mem::Bytes raw_data [[no_unique_address]]; be u64 png_magic [[hidden, no_unique_address]]; if (png_magic == 0x89504e470d0a1a0a) { PNG png_image @ $ [[inline, highlight_hidden]]; From 5359e385eaf91396ddbd4106af8036b72b791cb3 Mon Sep 17 00:00:00 2001 From: WerWolv Date: Sat, 6 Sep 2025 07:33:32 +0200 Subject: [PATCH 39/93] git: Added build folders to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9a0898a8..8698987f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ tests/cmake*/ tests/build*/ build/ +cmake-build-*/ .vscode/ .devcontainer/ From 7dfacc413936b6e1579afc69f7837983d123038b Mon Sep 17 00:00:00 2001 From: WerWolv Date: Sat, 6 Sep 2025 07:38:18 +0200 Subject: [PATCH 40/93] revert: includes/std: Added section parameters to a few std::mem functions --- includes/std/mem.pat | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/includes/std/mem.pat b/includes/std/mem.pat index b56e2b74..835505e9 100644 --- a/includes/std/mem.pat +++ b/includes/std/mem.pat @@ -10,8 +10,6 @@ namespace auto std::mem { A Handle for a custom Section */ using Section = u128; - const u64 SectionMain = 0; - const u64 SectionCurrent = 0xFFFF'FFFF'FFFF'FFFF; /** The endianness of a value @@ -57,16 +55,16 @@ namespace auto std::mem { Gets the base address of the data @return The base address of the memory */ - fn base_address(u64 section = SectionCurrent) { - return builtin::std::mem::base_address(section); + fn base_address() { + return builtin::std::mem::base_address(); }; /** Gets the size of the data @return The size of the memory */ - fn size(u64 section = SectionCurrent) { - return builtin::std::mem::size(section); + fn size() { + return builtin::std::mem::size(); }; /** @@ -123,8 +121,8 @@ namespace auto std::mem { @param [endian] The endianness of the value to read. Defaults to native @return The value read */ - fn read_unsigned(u128 address, u8 size, Endian endian = Endian::Native, Section section = SectionCurrent) { - return builtin::std::mem::read_unsigned(address, size, u32(endian), section); + fn read_unsigned(u128 address, u8 size, Endian endian = Endian::Native) { + return builtin::std::mem::read_unsigned(address, size, u32(endian)); }; /** @@ -134,8 +132,8 @@ namespace auto std::mem { @param [endian] The endianness of the value to read. Defaults to native @return The value read */ - fn read_signed(u128 address, u8 size, Endian endian = Endian::Native, Section section = SectionCurrent) { - return builtin::std::mem::read_signed(address, size, u32(endian), section); + fn read_signed(u128 address, u8 size, Endian endian = Endian::Native) { + return builtin::std::mem::read_signed(address, size, u32(endian)); }; /** @@ -144,8 +142,8 @@ namespace auto std::mem { @param size The size of the value to read @return The value read */ - fn read_string(u128 address, u128 size, Section section = SectionCurrent) { - return builtin::std::mem::read_string(address, size, section); + fn read_string(u128 address, u128 size) { + return builtin::std::mem::read_string(address, size); }; /** From 50d776f497c61e9794483f7de125d0c7cfa5b10d Mon Sep 17 00:00:00 2001 From: Zackary Newman <34630441+Zman350x@users.noreply.github.com> Date: Tue, 9 Sep 2025 12:21:12 -0400 Subject: [PATCH 41/93] patterns/ext4: Fix group descriptor table location for non-1024 block sizes (#439) --- patterns/fs/ext4.hexpat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/fs/ext4.hexpat b/patterns/fs/ext4.hexpat index 3e891a9c..0cd85787 100644 --- a/patterns/fs/ext4.hexpat +++ b/patterns/fs/ext4.hexpat @@ -540,4 +540,4 @@ struct ext4_group_descriptors { } }; -ext4_group_descriptors group_descs @ block_to_address(2); \ No newline at end of file +ext4_group_descriptors group_descs @ block_to_address(super_block.s_first_data_block + 1); From b24ae3663847db9be50f2a2c6ddec12025dbbb36 Mon Sep 17 00:00:00 2001 From: DmitriLeon2000 Date: Tue, 9 Sep 2025 18:22:02 +0200 Subject: [PATCH 42/93] patterns: Add .gmf (Game Maker 3.x Data) pattern file and its test files (#438) * Add .fas and .was pattern files (Oska DeskMates) * Update .was pattern file * Update .was/.wa3 pattern file * Update README.md * Update README.md * Update .fas & .was pattern files * Update README.md * Update fas_oskasoftware_old.hexpat * Added WAS test file * Update WAS test file * Update was_oskasoftware.hexpat * Update was_oskasoftware.hexpat * Update fas_oskasoftware_old.hexpat * Update fas_oskasoftware.hexpat * Update README.md Replacing backward slashes with forward ones in the `WAS` row. * Update fas_oskasoftware_old.hexpat * Update was_oskasoftware.hexpat * Add files via upload * Add Game Maker 3.x Data pattern * Update gmf.hexpat --- patterns/gmf.hexpat | 229 ++++++++++++++++++ .../test_data/gmf.hexpat/gmf.hexpat.gmf | Bin 0 -> 5671 bytes .../test_data/gmf.hexpat/gmf.hexpat_data/help | Bin 0 -> 207 bytes 3 files changed, 229 insertions(+) create mode 100644 patterns/gmf.hexpat create mode 100644 tests/patterns/test_data/gmf.hexpat/gmf.hexpat.gmf create mode 100644 tests/patterns/test_data/gmf.hexpat/gmf.hexpat_data/help diff --git a/patterns/gmf.hexpat b/patterns/gmf.hexpat new file mode 100644 index 00000000..13f4f436 --- /dev/null +++ b/patterns/gmf.hexpat @@ -0,0 +1,229 @@ +#pragma author DmitriLeon2000 +#pragma description Game Maker 3.x data +#pragma endian little + +import std.mem; +import std.io; + +struct Colors { + u8 red [[color("FF0000")]]; + u8 green [[color("00FF00")]]; + u8 blue [[color("0000FF")]]; + padding[1]; +} [[static, color(std::format("{:02X}{:02X}{:02X}", red, green, blue)), + hex::inline_visualize("color", red, green, blue, 255)]]; + +struct PascalString { + u32 length; + char data[length]; +}; + +struct BitmapData { + u8 type[2]; + u32 size; + u8 data[size-6] [[sealed]]; +}; + +enum BackgroundType : s32 { + Solid = 0, + Image = 1, + Gradient = 2, + None = 3 +}; + +enum BackImageStyle : s32 { + Tile = 0, + Stretch = 1, + Scrolling = 2, + LeftTop = 3 +}; + +enum Transition : u32 { + None = 0, + CreateFromLeft = 1, + CreateFromRight = 2, + CreateFromTop = 3, + CreateFromBottom = 4, + CreateFromCenter = 5, + ShiftFromLeft = 6, + ShiftFromRight = 7, + ShiftFromTop = 8, + ShiftFromBottom = 9, + PushFromLeft = 10, + PushFromRight = 11, + PushFromTop = 12, + PushFromBottom = 13 +}; + +struct SpriteBoundingBox { + u32 left, right, bottom, top; +}; + +enum ActionID : s32 { + EnumerateActions = 0, + EndAction = 1, + MoveFixed = 101, + ReverseHorizontal = 102, + ReverseVertical = 103, + JumpPosition = 104, + JumpRandom = 105, + Bounce = 106, + SetSpeedHorizontal = 111, + SetSpeedVertical = 112, + SetGravity = 121, + SetFriction = 122, + MoveFree = 131, + SetAlarm = 201, + InstanceCreate = 311, + InstanceDestroy = 312, + InstanceChange = 313, + PositionDestroy = 321, + Sound = 401, + ShowMessage = 411, + SetScore = 421, + DisplayHighscore = 431, + GoToRoom = 501, + EndGame = 511, + Sleep = 521, + ExitEvent = 601, + CheckPlaceFree = 701, + CheckInstanceNumber = 702, + CheckPlaceObject = 703, + CheckPlaceCollision = 704, + CheckQuestion = 721, + Else = 751, + CheckExpression = 801, + ExecuteCode = 811, + SetVariable = 821, + BlockStart = 901, + BlockEnd = 902, + DrawImage = 1001, + DrawText = 1002, + SetFont = 1003 +}; + +struct GameObjectAction { + s32 version_verify [[hidden]]; + ActionID action_id; + s32 apply_to; // -1 - self; -2 - not available + bool relative; + padding[3]; + u32 param_count; + PascalString params[param_count]; + s32 action_group; +}; + +struct GameObjectSubEvent { + s32 subevent_number; + s32 version_verify [[hidden]]; + u32 action_count; + GameObjectAction actions[action_count]; +}; + +struct GameObjectEvent { + GameObjectSubEvent subevents[while(std::mem::read_unsigned($, 4) != 0xFFFFFFFF)]; + padding[4]; +}; + +struct GameSprite { + s32 version_verify [[hidden]]; + s32 width, height; + SpriteBoundingBox bounding_box1; + u32 image_count; + BitmapData images[image_count]; + SpriteBoundingBox bounding_box2; +}; + +struct GameObject { + s32 version_verify [[hidden]]; + PascalString name; + bool solid; + padding[3]; + bool active; + padding[3]; + if (version_verify >= 320) + u32 parent_id; + GameObjectEvent events[13]; + // Events: 0 - Create, 1 - Destroy, 2 - Alarm, 3 - Step, 4 - Collision, + // 5 - Meeting, 6 - Mouse, 7 - Keyboard, 8 - Other, 9 - Drawing + // 10 - Create duplicate, 11-13 - unused + s32 cap [[hidden]]; + GameSprite sprite; +} [[name(name.data)]]; + +struct GameRoomView { + bool enabled; + padding[3]; + if (parent.version_verify >= 320) + s32 left, top, left_cell, top_cell; + u32 width; + u32 height; + if (parent.version_verify >= 320) { + u32 border_horiz, border_vert; + } else + u32 border; + u32 object_to_follow; +}; + +struct GameRoomInstance { + s32 left, top, object_index; +}; + +struct GameRoom { + u32 version_verify [[hidden]]; + PascalString name; + Colors back_color1; + if (version_verify >= 320) { + Colors back_color2; + bool vertical_gradient; + padding[3]; + } + // specify the name of the external image file from the data folder + PascalString back_image_name; + BackgroundType back_type; + BackImageStyle back_image_style; + s32 back_scroll_speed_horizontal, back_scroll_speed_vertical; + s32 speed, width, height, cell_size; + if (version_verify >= 320) { + bool enable_views; + padding[3]; + GameRoomView views[4]; + } else + GameRoomView view; + if (version_verify >= 320) { + Transition transition; + u32 transition_time, transition_steps; + } + u32 instance_count; + GameRoomInstance instances[instance_count]; +} [[name(name.data)]]; + +struct GameSound { + u32 version_verify [[hidden]]; + PascalString name; + u32 reserved; + // specify the name of the external audio file from the data folder + PascalString filename; + bool allow_for_sound_effects; + padding[3]; + u32 buffer_count; +} [[name(name.data)]]; + +struct GMF { + u32 version; + u32 verification[2]; + u32 version_verify_options [[hidden]]; + u32 option_count; + PascalString options[option_count]; + u32 version_verify_objects [[hidden]]; + u32 object_count; + GameObject objects[object_count]; + u32 version_verify_rooms [[hidden]]; + u32 room_count; + GameRoom rooms[room_count]; + u32 version_verify_sounds [[hidden]]; + u32 sound_count; + GameSound sounds[sound_count]; +}; + +GMF gmf @ 0x0; diff --git a/tests/patterns/test_data/gmf.hexpat/gmf.hexpat.gmf b/tests/patterns/test_data/gmf.hexpat/gmf.hexpat.gmf new file mode 100644 index 0000000000000000000000000000000000000000..ddb8bd64a84a61f4486494a09ae88c59b7427342 GIT binary patch literal 5671 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 TQU(uD$m!P%yotdzXn=eVhxSGl literal 0 HcmV?d00001 diff --git a/tests/patterns/test_data/gmf.hexpat/gmf.hexpat_data/help b/tests/patterns/test_data/gmf.hexpat/gmf.hexpat_data/help new file mode 100644 index 0000000000000000000000000000000000000000..5fba4b3984d35ca1fe1eae957d8d098bcc109595 GIT binary patch literal 207 zcmXwyO%B2!5QXd3#5-_;23lM+-RM1LLt#K7p=O}EXnJ?Ys+*U|eDCLSXqpwMdIctz zxQl%R)twbM^j>~6nH-eV!wP=RwN8l-2|mXJ-rfuqtWsPlnlx^V)Q Date: Tue, 9 Sep 2025 21:39:59 +0100 Subject: [PATCH 43/93] patterns: Add terminfo pattern (#437) * patterns/terminfo: Add pattern for compiled term info entry files. This adds support for the compiled (legacy and extended) term info entry files that are used for determining terminal capabilities. * Add .bin extension to the terminfo test file. --- README.md | 1 + patterns/terminfo.hexpat | 348 +++++++++++++++++++ tests/patterns/test_data/terminfo.hexpat.bin | Bin 0 -> 4071 bytes 3 files changed, 349 insertions(+) create mode 100644 patterns/terminfo.hexpat create mode 100644 tests/patterns/test_data/terminfo.hexpat.bin diff --git a/README.md b/README.md index 29a71bd9..e20e8b76 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ZSTD | `application/zstd` | [`patterns/zstd.hexpat`](patterns/zstd.hexpat) | Zstandard compressed data format | | MOD | `3d-model/mod` | [`patterns/DMC3HD-Mod.hexpat`](patterns/dmc3_hd_mod.hexpat) | 3D Model files used in Devil May Cry 3 HD Collection | | CBM BASIC | | [`commodore_basic.hexpat`](patterns/commodore_basic.hexpat) | Commodore BASIC | +| Terminfo | `application/x-terminfo` and `application/x-terminfo2` | [`patterns/terminfo.hexpat`](patterns/terminfo.hexpat) | Compiled *(legacy and extended)* term info entry | ### Scripts diff --git a/patterns/terminfo.hexpat b/patterns/terminfo.hexpat new file mode 100644 index 00000000..63bc5201 --- /dev/null +++ b/patterns/terminfo.hexpat @@ -0,0 +1,348 @@ +/* info */ +#pragma author Nightowl286 +#pragma description Compiled term info entry + +#pragma endian little + +#pragma magic [1e 02] @ 0x00 +#pragma MIME application/x-terminfo2 + +#pragma magic [1a 01] @ 0x00 +#pragma MIME application/x-terminfo + +/* + Format has been primarily taken from: + https://www.man7.org/linux/man-pages/man5/term.5.html + + Technically you can't count on the binary formats being compatible + on different unix versions, however *(as I've been told)* those versions + are pretty much dead nowadays so this format is common enough to work + in like the majority of the cases. +*/ + +/* imports */ +import std.array; +import std.mem; +import std.string; +import std.ctype; + + +/* utility */ +fn is_char(char ch) { + char value = std::mem::read_unsigned($, 1); + return value == ch; +}; + +fn is_not_char(char ch) { + char value = std::mem::read_unsigned($, 1); + return value != ch; +}; + + +/* types */ +enum number_format : s16 { + legacy = 0o432, + extended = 0o1036 +} +[[format("number_format_to_string")]]; + +struct boolean_capability { s8 value; } +[[ + sealed, + format("boolean_capability_format"), + comment("Indicates the presence of a boolean capability.\n\n(An empty value indicates that the capability is missing).") +]]; + +struct numerical_capability { + if (parent.header.format == number_format::extended) + s32 value; + else + s16 value; +} +[[ + sealed, + format("numerical_capability_format"), + comment("Indicates the presence of a numerical capability.\n\n(An empty value indicates that the capability is missing).") +]]; + +struct string_table_value_offset { s16 value; } +[[ + sealed, + format("string_table_value_offset_format"), + comment("Indicates the offset of a string capability value in the string table.\n\n(An empty value indicates that the capability is missing).") +]]; + +struct string_table_name_offset { s16 value; } +[[ + sealed, + format("string_table_name_offset_format"), + comment("Indicates the offset of a name string in the string table.\n\n(This value is relative to the name portion of the string table).") +]]; + +struct terminal_name { + char name[while(is_not_char('|') && is_not_char(0x00))]; + padding[1]; + + if (is_char('|')) $ += 1; + if (is_char(0)) break; +} +[[ + sealed, + comment("Represents a terminal name/alias.") +]]; + +struct table_string { + char value[while(is_not_char(0x00))]; + if (is_char(0)) + $ += 1; + +} +[[ + sealed, + format("string_table_value_format"), + comment("A null-terminated string in the string table.") +]]; + +struct string_table { + std::ByteSizedArray array [[inline]]; +}; + +struct legacy_header { + number_format format + [[comment("The number format of the terminfo entry.")]]; + + s16 name_size + [[ + name("name size"), + comment("The size of the terminal names in bytes.") + ]]; + + s16 boolean_count + [[ + name("boolean capability count"), + comment("The amount of known boolean capabilities in the file.") + ]]; + + s16 numerical_count + [[ + name("numerical capability count"), + comment("The amount of known numerical capabilities in the file.") + ]]; + + s16 string_count + [[ + name("string capability count"), + comment("The amount of known string capabilities in the file.") + ]]; + + s16 string_table_size + [[ + name("string table size"), + comment("The size of the string table (in bytes).") + ]]; +}; + +struct extended_header { + s16 boolean_count + [[ + name("boolean capability count"), + comment("The amount of extended boolean capabilities in the file.") + ]]; + + s16 numerical_count + [[ + name("numerical capability count"), + comment("The amount of extended numerical capabilities in the file.") + ]]; + + s16 string_count + [[ + name("string capability count"), + comment("The amount of extended string capabilities in the file.") + ]]; + + s16 table_item_count + [[ + name("table item count"), + comment("The amount of items in the string table.") + ]]; + + s16 string_table_size + [[ + name("string table size"), + comment("The size of the extended string table (in bytes).") + ]]; +}; + +struct extended_data { + extended_header header + [[comment("The metadata about the extended section of the file.")]]; + + boolean_capability booleans[header.boolean_count] + [[ + name("boolean capabilities"), + comment("The extended boolean capabilities.") + ]]; + + if ($ % 2 != 0) $ += 1; // align to even boundary before numerical capabilities are read; + + numerical_capability numerical[header.numerical_count] + [[ + name("numerical capabilities"), + comment("The extended numerical capabilities.") + ]]; + + string_table_value_offset string_offsets[header.string_count] + [[ + name("string table value offsets"), + comment("The string table offsets for the extended string capability values.") + ]]; + + string_table_name_offset name_offsets[header.table_item_count - header.string_count] + [[ + name("string table name offsets"), + comment("The string table offsets for all of the extended capability names.\n\n(These values are relative to the name portion of the string table).") + ]]; + + string_table string_table + [[ + name("string table"), + comment("Contains all of the string values used in the extended section of the file.") + ]]; +}; + +struct file { + legacy_header header + [[comment("The metadata about the file.")]]; + + terminal_name terminal_names[1000] + [[ + format_entries("terminal_name_format"), + name("terminal names"), + comment("The names and aliases that the terminfo file is describing.") + ]]; + + boolean_capability booleans[header.boolean_count] + [[ + name("boolean capabilities"), + comment("The known boolean capabilities.") + ]]; + + if ($ % 2 != 0) $ += 1; // align to even boundary before numerical capabilities are read; + + numerical_capability numerical[header.numerical_count] + [[ + name("numerical capabilities"), + comment("The known numerical capabilities.") + ]]; + + string_table_value_offset string_offsets[header.string_count] + [[ + name("string table value offsets"), + comment("The string table offsets for the known string capability values.") + ]]; + + string_table string_table + [[ + name("string table"), + comment("Contains all of the string values used in the (legacy storage format section of) file.") + ]]; + + if (std::mem::eof() == false) { + extended_data extended + [[comment("Contains the information from extended storage format section.")]]; + } + +} [[inline]]; + + +/* formatting */ +fn terminal_name_format(terminal_name name) { + return '"' + name.name + '"'; +}; + +fn boolean_capability_format(boolean_capability value) { + match (value.value) { + (-2): return "cancelled"; + (-1 | 0): return ""; + (1): return "present"; + (_): return value.value; + } + return value; +}; + +fn numerical_capability_format(numerical_capability value) { + match (value.value) { + (-2): return "cancelled"; + (-1): return ""; + (_): return value.value; + } + return value; +}; + +fn string_table_value_offset_format(string_table_value_offset value) { + match (value.value) { + (-2): return "cancelled"; + (-1): return ""; + (_): return value.value; + } + return value; +}; + +fn string_table_name_offset_format(string_table_name_offset value) { + return value.value; +}; + +fn string_table_value_format(ref table_string value) { + str view = "\""; + + for (s16 i = 0, i < sizeof(value.value), i += 1) { + char ch = value.value[i]; + if (ch == '\\') view += "\\\\"; + else if (ch == '"') view += "\\\""; + else if (std::ctype::isgraph(ch)) view += ch; + else view += to_hex_ch(ch); + } + + view += "\""; + return view; +}; + +fn to_hex_ch(char ch) { + if (ch == '\x1b') return "\\e"; + if (ch == ' ') return " "; + if (ch == '\r') return "\\r"; + if (ch == '\n') return "\\n"; + if (ch == '\a') return "\\a"; + if (ch == '\b') return "\\b"; + if (ch == '\t') return "\\t"; + if (ch == 11) return "\\v"; + if (ch == '\f') return "\\f"; + + s8 second = ch % 16; + s8 first = ch / 16; + + return "\\x" + to_hex_digit(first) + to_hex_digit(second); +}; + +fn to_hex_digit(u8 num) { + if (num == 10) return 'A'; + if (num == 11) return 'B'; + if (num == 12) return 'C'; + if (num == 13) return 'D'; + if (num == 14) return 'E'; + if (num == 15) return 'F'; + + return std::string::to_string(num); +}; + +fn number_format_to_string(number_format format) { + if (format == number_format::extended) return "extended"; + if (format == number_format::legacy) return "legacy"; + + return "unknown (" + std::string::to_string(s16(format)) + ")"; +}; + + +/* actual data */ +file file @ 0x00; \ No newline at end of file diff --git a/tests/patterns/test_data/terminfo.hexpat.bin b/tests/patterns/test_data/terminfo.hexpat.bin new file mode 100644 index 0000000000000000000000000000000000000000..2a9f42b40ea5317efeba69a432dce6eb43714e95 GIT binary patch literal 4071 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 literal 0 HcmV?d00001 From 69077b919d94f4abba2e5e3588f9635e9618a98c Mon Sep 17 00:00:00 2001 From: Zackary Newman <34630441+Zman350x@users.noreply.github.com> Date: Fri, 12 Sep 2025 16:42:55 -0400 Subject: [PATCH 44/93] patterns/ext4: Increase pattern limit to 2 GiB (#440) --- patterns/fs/ext4.hexpat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/fs/ext4.hexpat b/patterns/fs/ext4.hexpat index 0cd85787..26963cb9 100644 --- a/patterns/fs/ext4.hexpat +++ b/patterns/fs/ext4.hexpat @@ -8,7 +8,7 @@ #pragma endian little #pragma magic [53 EF] @ 0x438 -#pragma pattern_limit 0x90000 +#pragma pattern_limit 0x80000000 import type.time; import type.size; From ff737c3665cdf793e50ffc19fc200f9e4c01c2bf Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 17 Sep 2025 02:11:29 +0300 Subject: [PATCH 45/93] Add files via upload --- patterns/ACU_DATA_Compressed.hexpat | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 patterns/ACU_DATA_Compressed.hexpat diff --git a/patterns/ACU_DATA_Compressed.hexpat b/patterns/ACU_DATA_Compressed.hexpat new file mode 100644 index 00000000..edc54d93 --- /dev/null +++ b/patterns/ACU_DATA_Compressed.hexpat @@ -0,0 +1,45 @@ +#pragma description Assassin's Creed: Unity's Compressed .data file +#pragma MIME compressed/data + +// author = haru233, many thanks to AxCut +// ImHex Hex Pattern File for Assassin's Creed: Unity's Compressed .data files + + +import std.core; +import std.mem; + +struct CHUNK { + u16 Uncompressed_Size; + u16 Compressed_Size; +}; + +struct CHUNK_Data { + u32 Hash; + + u64 i = std::core::array_index(); + u8 data[parent.chunk[i].Compressed_Size]; +}; + +struct PACK { + u64 ID; + padding[2]; + u8 Compression_Type; + padding[3]; + u8 Version; + u16 CHUNK_Count; + + CHUNK chunk[CHUNK_Count]; + CHUNK_Data data[CHUNK_Count]; + + +}; + +enum CompressionType : u8 { + LZO1X = 0x01, + LZO2A = 0x02, + xmemdecompress = 0x03, + LZO1C = 0x05 +}; + + +PACK pack[while(!std::mem::eof())] @0x00; \ No newline at end of file From baf773f569febc96953c6c36b530479e80f2724b Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 17 Sep 2025 02:13:26 +0300 Subject: [PATCH 46/93] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1f352a42..cef990fd 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ZLIB | `application/zlib` | [`patterns/zlib.hexpat`](patterns/zlib.hexpat) | ZLIB compressed data format | | ZSTD | `application/zstd` | [`patterns/zstd.hexpat`](patterns/zstd.hexpat) | Zstandard compressed data format | | MOD | `3d-model/mod` | [`patterns/DMC3HD-Mod.hexpat`](patterns/dmc3_hd_mod.hexpat) | 3D Model files used in Devil May Cry 3 HD Collection | +| DATA | `compressed/data` | [`patterns/ACU_DATA_Compressed.hexpat`](patterns/acu_data_compressed.hexpat) | Compressed .data archive files (contained inside .forge archives) used in Assassin's Creed: Unity | ### Scripts From bee880299678761e5ff0992030824173566e15ef Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 17 Sep 2025 02:14:09 +0300 Subject: [PATCH 47/93] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cef990fd..e0d78385 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ZLIB | `application/zlib` | [`patterns/zlib.hexpat`](patterns/zlib.hexpat) | ZLIB compressed data format | | ZSTD | `application/zstd` | [`patterns/zstd.hexpat`](patterns/zstd.hexpat) | Zstandard compressed data format | | MOD | `3d-model/mod` | [`patterns/DMC3HD-Mod.hexpat`](patterns/dmc3_hd_mod.hexpat) | 3D Model files used in Devil May Cry 3 HD Collection | -| DATA | `compressed/data` | [`patterns/ACU_DATA_Compressed.hexpat`](patterns/acu_data_compressed.hexpat) | Compressed .data archive files (contained inside .forge archives) used in Assassin's Creed: Unity | +| DATA | `compressed/data` | [`patterns/ACU_DATA_Compressed.hexpat`](patterns/acu_data_compressed.hexpat) | Compressed .data archive files (found inside .forge archives) used in Assassin's Creed: Unity | ### Scripts From 691c10cc62608ae006bf992938ce4fc08621cd41 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 17 Sep 2025 02:18:40 +0300 Subject: [PATCH 48/93] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e0d78385..c60b3d85 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,8 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ZLIB | `application/zlib` | [`patterns/zlib.hexpat`](patterns/zlib.hexpat) | ZLIB compressed data format | | ZSTD | `application/zstd` | [`patterns/zstd.hexpat`](patterns/zstd.hexpat) | Zstandard compressed data format | | MOD | `3d-model/mod` | [`patterns/DMC3HD-Mod.hexpat`](patterns/dmc3_hd_mod.hexpat) | 3D Model files used in Devil May Cry 3 HD Collection | -| DATA | `compressed/data` | [`patterns/ACU_DATA_Compressed.hexpat`](patterns/acu_data_compressed.hexpat) | Compressed .data archive files (found inside .forge archives) used in Assassin's Creed: Unity | +| DATA | `archives/data` | [`patterns/ACU_DATA_Compressed.hexpat`](patterns/acu_data_compressed.hexpat) | Compressed .data archive files (found inside .forge archives) used in Assassin's Creed: Unity | +| FORGE | `archives/forge` | [`patterns/ACU_FORGE.hexpat`](patterns/ACU_FORGE.hexpat) | .forge archive files used in Assassin's Creed: Unity | ### Scripts From a5d43094cb4e8bd6d31a37ea136c2d7262239421 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 17 Sep 2025 02:21:09 +0300 Subject: [PATCH 49/93] Add files via upload --- patterns/ACU_FORGE.hexpat | 64 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 patterns/ACU_FORGE.hexpat diff --git a/patterns/ACU_FORGE.hexpat b/patterns/ACU_FORGE.hexpat new file mode 100644 index 00000000..e98cc8ea --- /dev/null +++ b/patterns/ACU_FORGE.hexpat @@ -0,0 +1,64 @@ +#pragma description Assassin's Creed: Unity's .forge archive file +#pragma MIME archives/forge + +// author = haru233, many thanks to AxCut +// ImHex Hex Pattern File for Assassin's Creed: Unity's Compressed .data files + +import std.core; + +struct Forge_Header { + char MAGIC[8]; + padding[1]; + u32 Version; + u32 File_Data_Header_Offset; +}; + +struct File_Data_Header { + u32 File_Count; + padding[32]; + u64 File_Data_Header2_Offset; +}; + +struct File_Data_Header2 { + u32 File_Count2; + padding[4]; + u64 File_Table_Offset; + padding[12]; + u32 File_Count3; + u64 File_Name_Table_Offset; + padding[8]; +}; + + +struct File_Table { + u64 Raw_Data_Offset; + u64 File_ID; + u32 Raw_Data_Size; +}; + +struct File_Name_Table { + u32 Raw_Data_Size; + padding[40]; + char Filename[128]; + padding[20]; +}; + + +Forge_Header forge_header @0x00; + +File_Data_Header file_data_header @(forge_header.File_Data_Header_Offset); + +File_Data_Header2 file_data_header2 @(file_data_header.File_Data_Header2_Offset); + +File_Table file_table[file_data_header.File_Count] @(file_data_header2.File_Table_Offset); + +File_Name_Table file_name_table[file_data_header.File_Count] @(file_data_header2.File_Name_Table_Offset); + + +struct Raw_Data_Table { + u64 i = std::core::array_index(); + u8 Raw_Data[file_table[i].Raw_Data_Size] @ file_table[i].Raw_Data_Offset; +}; + + +Raw_Data_Table raw_data_table[file_data_header.File_Count] @0x00; \ No newline at end of file From cba726fdb542474b7c1201e8816997303007a28d Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 17 Sep 2025 02:22:10 +0300 Subject: [PATCH 50/93] Update ACU_FORGE.hexpat --- patterns/ACU_FORGE.hexpat | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patterns/ACU_FORGE.hexpat b/patterns/ACU_FORGE.hexpat index e98cc8ea..519dc9e8 100644 --- a/patterns/ACU_FORGE.hexpat +++ b/patterns/ACU_FORGE.hexpat @@ -2,7 +2,7 @@ #pragma MIME archives/forge // author = haru233, many thanks to AxCut -// ImHex Hex Pattern File for Assassin's Creed: Unity's Compressed .data files +// ImHex Hex Pattern File for Assassin's Creed: Unity's .forge files import std.core; @@ -61,4 +61,4 @@ struct Raw_Data_Table { }; -Raw_Data_Table raw_data_table[file_data_header.File_Count] @0x00; \ No newline at end of file +Raw_Data_Table raw_data_table[file_data_header.File_Count] @0x00; From 4279de2b8f81b6e9f6a0cba729aef1575c899713 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 17 Sep 2025 02:23:08 +0300 Subject: [PATCH 51/93] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c60b3d85..2bf7b86f 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ZLIB | `application/zlib` | [`patterns/zlib.hexpat`](patterns/zlib.hexpat) | ZLIB compressed data format | | ZSTD | `application/zstd` | [`patterns/zstd.hexpat`](patterns/zstd.hexpat) | Zstandard compressed data format | | MOD | `3d-model/mod` | [`patterns/DMC3HD-Mod.hexpat`](patterns/dmc3_hd_mod.hexpat) | 3D Model files used in Devil May Cry 3 HD Collection | -| DATA | `archives/data` | [`patterns/ACU_DATA_Compressed.hexpat`](patterns/acu_data_compressed.hexpat) | Compressed .data archive files (found inside .forge archives) used in Assassin's Creed: Unity | +| DATA | `archives/data` | [`patterns/ACU_DATA_Compressed.hexpat`](patterns/ACU_DATA_Compressed.hexpat) | Compressed .data archive files (found inside .forge archives) used in Assassin's Creed: Unity | | FORGE | `archives/forge` | [`patterns/ACU_FORGE.hexpat`](patterns/ACU_FORGE.hexpat) | .forge archive files used in Assassin's Creed: Unity | ### Scripts From df54d67756a3e0e5ce2fe7ecf9681de2437a9411 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 17 Sep 2025 02:26:00 +0300 Subject: [PATCH 52/93] Update and rename ACU_DATA_Compressed.hexpat to acu_data_compressed.hexpat --- .../{ACU_DATA_Compressed.hexpat => acu_data_compressed.hexpat} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename patterns/{ACU_DATA_Compressed.hexpat => acu_data_compressed.hexpat} (89%) diff --git a/patterns/ACU_DATA_Compressed.hexpat b/patterns/acu_data_compressed.hexpat similarity index 89% rename from patterns/ACU_DATA_Compressed.hexpat rename to patterns/acu_data_compressed.hexpat index edc54d93..8e27bee2 100644 --- a/patterns/ACU_DATA_Compressed.hexpat +++ b/patterns/acu_data_compressed.hexpat @@ -42,4 +42,5 @@ enum CompressionType : u8 { }; -PACK pack[while(!std::mem::eof())] @0x00; \ No newline at end of file + +PACK pack[while(!std::mem::eof())] @0x00; From dd23dfaf2cd226c4ef8ba27ac1516efd07305b19 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 17 Sep 2025 02:26:13 +0300 Subject: [PATCH 53/93] Update and rename ACU_FORGE.hexpat to acu_forge.hexpat --- patterns/{ACU_FORGE.hexpat => acu_forge.hexpat} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename patterns/{ACU_FORGE.hexpat => acu_forge.hexpat} (100%) diff --git a/patterns/ACU_FORGE.hexpat b/patterns/acu_forge.hexpat similarity index 100% rename from patterns/ACU_FORGE.hexpat rename to patterns/acu_forge.hexpat From 400a06b94a83d7a8788cd6cf66547da88f883852 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 17 Sep 2025 02:26:59 +0300 Subject: [PATCH 54/93] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2bf7b86f..f5263337 100644 --- a/README.md +++ b/README.md @@ -198,8 +198,8 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ZLIB | `application/zlib` | [`patterns/zlib.hexpat`](patterns/zlib.hexpat) | ZLIB compressed data format | | ZSTD | `application/zstd` | [`patterns/zstd.hexpat`](patterns/zstd.hexpat) | Zstandard compressed data format | | MOD | `3d-model/mod` | [`patterns/DMC3HD-Mod.hexpat`](patterns/dmc3_hd_mod.hexpat) | 3D Model files used in Devil May Cry 3 HD Collection | -| DATA | `archives/data` | [`patterns/ACU_DATA_Compressed.hexpat`](patterns/ACU_DATA_Compressed.hexpat) | Compressed .data archive files (found inside .forge archives) used in Assassin's Creed: Unity | -| FORGE | `archives/forge` | [`patterns/ACU_FORGE.hexpat`](patterns/ACU_FORGE.hexpat) | .forge archive files used in Assassin's Creed: Unity | +| DATA | `archives/data` | [`patterns/acu_data_compressed.hexpat`](patterns/acu_data_compressed.hexpat) | Compressed .data archive files (found inside .forge archives) used in Assassin's Creed: Unity | +| FORGE | `archives/forge` | [`patterns/acu_forge.hexpat`](patterns/acu_forge.hexpat) | .forge archive files used in Assassin's Creed: Unity | ### Scripts From 7e0ab43e4fdbba37b91f3ad63c3797ae73fe2ac4 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Wed, 17 Sep 2025 03:21:28 +0300 Subject: [PATCH 55/93] Update acu_data_compressed.hexpat --- patterns/acu_data_compressed.hexpat | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/patterns/acu_data_compressed.hexpat b/patterns/acu_data_compressed.hexpat index 8e27bee2..48808833 100644 --- a/patterns/acu_data_compressed.hexpat +++ b/patterns/acu_data_compressed.hexpat @@ -8,6 +8,14 @@ import std.core; import std.mem; +enum CompressionType : u8 { + LZO1X_ = 0x00, // Both 0x00 and 0x01 mean LZO1X + LZO1X = 0x01, + LZO2A = 0x02, + xmemdecompress = 0x03, + LZO1C = 0x05 +}; + struct CHUNK { u16 Uncompressed_Size; u16 Compressed_Size; @@ -23,7 +31,7 @@ struct CHUNK_Data { struct PACK { u64 ID; padding[2]; - u8 Compression_Type; + CompressionType Compression_Type; padding[3]; u8 Version; u16 CHUNK_Count; @@ -34,13 +42,6 @@ struct PACK { }; -enum CompressionType : u8 { - LZO1X = 0x01, - LZO2A = 0x02, - xmemdecompress = 0x03, - LZO1C = 0x05 -}; - PACK pack[while(!std::mem::eof())] @0x00; From caf13272d7983ba813bf1c7f1f632b421d312a8c Mon Sep 17 00:00:00 2001 From: haruse23 Date: Thu, 18 Sep 2025 13:30:40 +0300 Subject: [PATCH 56/93] Update acu_data_compressed.hexpat --- patterns/acu_data_compressed.hexpat | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/patterns/acu_data_compressed.hexpat b/patterns/acu_data_compressed.hexpat index 48808833..8d4bb53d 100644 --- a/patterns/acu_data_compressed.hexpat +++ b/patterns/acu_data_compressed.hexpat @@ -1,5 +1,5 @@ #pragma description Assassin's Creed: Unity's Compressed .data file -#pragma MIME compressed/data +#pragma MIME archives/data // author = haru233, many thanks to AxCut // ImHex Hex Pattern File for Assassin's Creed: Unity's Compressed .data files @@ -45,3 +45,4 @@ struct PACK { PACK pack[while(!std::mem::eof())] @0x00; + From 556f81342e27cb6c5582534df0df9d4437f79b26 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Thu, 18 Sep 2025 23:55:06 +0300 Subject: [PATCH 57/93] Add files via upload --- patterns/ACU_DATA_Decompressed.hexpat | 93 +++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 patterns/ACU_DATA_Decompressed.hexpat diff --git a/patterns/ACU_DATA_Decompressed.hexpat b/patterns/ACU_DATA_Decompressed.hexpat new file mode 100644 index 00000000..c0ac8704 --- /dev/null +++ b/patterns/ACU_DATA_Decompressed.hexpat @@ -0,0 +1,93 @@ +#pragma description Assassin's Creed: Unity's Decompressed .data file +#pragma MIME archives/acu_data_decompressed + +// Thanks to yretenai on GitHub for helping with the Block Allocator part + +import std.core; +import std.mem; + +struct Block_Allocator_Type0 { + padding[4]; + u32 Class_ID; + u32 Size; +}; + +struct Block_Allocator_Type1 { + padding[4]; + u32 Type_ID; + u32 Size; +}; + +struct Block_Allocator { + u16 Version; + + if (Version == 0) { + u32 Block_Allocator_Number; + Block_Allocator_Type0 block_allocator_type0[Block_Allocator_Number]; + } + + else if (Version == 1) { + u32 Block_Allocator_Number; + Block_Allocator_Type1 block_allocator_type1_[Block_Allocator_Number]; + } + + else if (Version == 2) { + bool Has_Secondary_Block_Allocator; + u32 Main_Block_Allocator_Number; + + Block_Allocator_Type1 block_allocator_type1__[Main_Block_Allocator_Number]; + + if (Has_Secondary_Block_Allocator) { + u32 Secondary_Block_Allocator_Number; + Block_Allocator_Type1 block_allocator_type1___[Secondary_Block_Allocator_Number+1]; + } + } + + +}; + +struct File { + u32 Object_Hash; + u32 File_Size; + u32 Filename_Length; + + if (File_Size > 0) { + if (Filename_Length == 0) { + bool HasBlockAllocator; + + if (HasBlockAllocator) { + Block_Allocator block_allocator; + u8 File_Data[File_Size]; + } + + else + u8 File_Data[File_Size]; + } + + else { + char Filename[Filename_Length]; + + bool HasBlockAllocator; + + if (HasBlockAllocator) { + Block_Allocator block_allocator; + u8 File_Data[File_Size]; + } + + else + u8 File_Data[File_Size]; + + + } + + } + + else + continue; + + + +}; + + +File file[while(!std::mem::eof())] @0x00; \ No newline at end of file From f5aeb83ba0e6284b648ecab148e80a0b42c0e259 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Thu, 18 Sep 2025 23:55:28 +0300 Subject: [PATCH 58/93] Update and rename ACU_DATA_Decompressed.hexpat to acu_data_decompressed.hexpat --- ...CU_DATA_Decompressed.hexpat => acu_data_decompressed.hexpat} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename patterns/{ACU_DATA_Decompressed.hexpat => acu_data_decompressed.hexpat} (93%) diff --git a/patterns/ACU_DATA_Decompressed.hexpat b/patterns/acu_data_decompressed.hexpat similarity index 93% rename from patterns/ACU_DATA_Decompressed.hexpat rename to patterns/acu_data_decompressed.hexpat index c0ac8704..db930534 100644 --- a/patterns/ACU_DATA_Decompressed.hexpat +++ b/patterns/acu_data_decompressed.hexpat @@ -90,4 +90,4 @@ struct File { }; -File file[while(!std::mem::eof())] @0x00; \ No newline at end of file +File file[while(!std::mem::eof())] @0x00; From 32b762b70e704f3771df27bdeaa062baff7ddb1d Mon Sep 17 00:00:00 2001 From: haruse23 Date: Thu, 18 Sep 2025 23:57:27 +0300 Subject: [PATCH 59/93] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f5263337..81e1b4bc 100644 --- a/README.md +++ b/README.md @@ -198,8 +198,11 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ZLIB | `application/zlib` | [`patterns/zlib.hexpat`](patterns/zlib.hexpat) | ZLIB compressed data format | | ZSTD | `application/zstd` | [`patterns/zstd.hexpat`](patterns/zstd.hexpat) | Zstandard compressed data format | | MOD | `3d-model/mod` | [`patterns/DMC3HD-Mod.hexpat`](patterns/dmc3_hd_mod.hexpat) | 3D Model files used in Devil May Cry 3 HD Collection | -| DATA | `archives/data` | [`patterns/acu_data_compressed.hexpat`](patterns/acu_data_compressed.hexpat) | Compressed .data archive files (found inside .forge archives) used in Assassin's Creed: Unity | | FORGE | `archives/forge` | [`patterns/acu_forge.hexpat`](patterns/acu_forge.hexpat) | .forge archive files used in Assassin's Creed: Unity | +| DATA | `archives/acu_data_compressed` | [`patterns/acu_data_compressed.hexpat`](patterns/acu_data_compressed.hexpat) | Compressed .data archive files (found inside .forge archives) used in Assassin's Creed: Unity | +| DATA | `archives/acu_data_decompressed` | [`patterns/acu_data_decompressed.hexpat`](patterns/acu_data_decompressed.hexpat) | Decompressed .data archive files (found inside .forge archives) used in Assassin's Creed: Unity | + + ### Scripts From 7cb7460ca0e2b4e7926e6ba91dc8c8078b6e9774 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Thu, 18 Sep 2025 23:58:02 +0300 Subject: [PATCH 60/93] Update acu_data_compressed.hexpat --- patterns/acu_data_compressed.hexpat | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/patterns/acu_data_compressed.hexpat b/patterns/acu_data_compressed.hexpat index 8d4bb53d..bd31c28b 100644 --- a/patterns/acu_data_compressed.hexpat +++ b/patterns/acu_data_compressed.hexpat @@ -1,5 +1,5 @@ #pragma description Assassin's Creed: Unity's Compressed .data file -#pragma MIME archives/data +#pragma MIME archives/acu_data_compressed // author = haru233, many thanks to AxCut // ImHex Hex Pattern File for Assassin's Creed: Unity's Compressed .data files @@ -46,3 +46,4 @@ struct PACK { PACK pack[while(!std::mem::eof())] @0x00; + From db46129cf51eed85614136c529fb463bd702cec5 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Thu, 18 Sep 2025 23:58:59 +0300 Subject: [PATCH 61/93] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 81e1b4bc..b4d86526 100644 --- a/README.md +++ b/README.md @@ -199,8 +199,8 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ZSTD | `application/zstd` | [`patterns/zstd.hexpat`](patterns/zstd.hexpat) | Zstandard compressed data format | | MOD | `3d-model/mod` | [`patterns/DMC3HD-Mod.hexpat`](patterns/dmc3_hd_mod.hexpat) | 3D Model files used in Devil May Cry 3 HD Collection | | FORGE | `archives/forge` | [`patterns/acu_forge.hexpat`](patterns/acu_forge.hexpat) | .forge archive files used in Assassin's Creed: Unity | -| DATA | `archives/acu_data_compressed` | [`patterns/acu_data_compressed.hexpat`](patterns/acu_data_compressed.hexpat) | Compressed .data archive files (found inside .forge archives) used in Assassin's Creed: Unity | -| DATA | `archives/acu_data_decompressed` | [`patterns/acu_data_decompressed.hexpat`](patterns/acu_data_decompressed.hexpat) | Decompressed .data archive files (found inside .forge archives) used in Assassin's Creed: Unity | +| DATA | `archives/acu_data_compressed` | [`patterns/acu_data_compressed.hexpat`](patterns/acu_data_compressed.hexpat) | Compressed .data archive files used in Assassin's Creed: Unity | +| DATA | `archives/acu_data_decompressed` | [`patterns/acu_data_decompressed.hexpat`](patterns/acu_data_decompressed.hexpat) | Decompressed .data archive files used in Assassin's Creed: Unity | From 0e67ee102bcdad9b6bc9d1089e84bfdba9e08bbf Mon Sep 17 00:00:00 2001 From: Nik Date: Sun, 21 Sep 2025 10:44:20 +0200 Subject: [PATCH 62/93] patterns/ico: Disable BMP processing until issues in the pattern language have been resolved --- patterns/ico.hexpat | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/patterns/ico.hexpat b/patterns/ico.hexpat index 91eaa29a..cee8485b 100644 --- a/patterns/ico.hexpat +++ b/patterns/ico.hexpat @@ -47,10 +47,14 @@ struct ImageData { be u64 png_magic [[hidden, no_unique_address]]; if (png_magic == 0x89504e470d0a1a0a) { PNG png_image @ $ [[inline, highlight_hidden]]; - } else { + } + + // TODO: Sections currently can't be properly accessed accross imported types + // TODO: Uncomment this again once that's been fixed + /*else { BMPData data [[hidden]]; BMP bmp_image @ addressof(this) - 14 [[inline, highlight_hidden]]; - } + }*/ }; struct ICONDIRENTRY { From 7a9a5097a2a71dbc8a4880151cb378f80b6db452 Mon Sep 17 00:00:00 2001 From: ODeux Date: Sun, 21 Sep 2025 11:17:40 +0200 Subject: [PATCH 63/93] patterns: Add Python Pickle Pattern (#446) * Add pickle pattern file * Add test file * Update README.md --------- Co-authored-by: Nik --- README.md | 1 + patterns/pickle.hexpat | 357 +++++++++++++++++++++ tests/patterns/test_data/pickle.hexpat.bin | Bin 0 -> 541 bytes 3 files changed, 358 insertions(+) create mode 100644 patterns/pickle.hexpat create mode 100644 tests/patterns/test_data/pickle.hexpat.bin diff --git a/README.md b/README.md index e20e8b76..7fc5ec93 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | 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) | +| Pickle | | [`patterns/pickle.hexpat`](patterns/pickle.hexpat) | Python Pickle Protocol | | PIF | `image/pif` | [`patterns/pif.hexpat`](patterns/pif.hexpat) | PIF Image Format | | PKM | | [`patterns/pkm.hexpat`](patterns/pkm.hexpat) | PKM texture format | | PNG | `image/png` | [`patterns/png.hexpat`](patterns/png.hexpat) | PNG image files | diff --git a/patterns/pickle.hexpat b/patterns/pickle.hexpat new file mode 100644 index 00000000..5252b6df --- /dev/null +++ b/patterns/pickle.hexpat @@ -0,0 +1,357 @@ + +/* + References: + Pickle Source Code: + https://github.com/python/cpython/blob/main/Lib/pickle.py + Pickle Protocol Version Breakdown: + https://docs.python.org/3.13/library/pickle.html#data-stream-format + Pickle OpCode Breakdown: + https://github.com/python/cpython/blob/main/Lib/pickletools.py +*/ + +#pragma author ODeux +#pragma description Python Binary Object Serialization Protocol + +#pragma endian little + +import std.mem; +import std.string; + +#pragma array_limit 524288 + +fn todo(auto message){ + std::error(std::format("@0x{:08X} TODO: " + message, $)); +}; + +fn utf8_fmt(auto s){ + return std::format("{}", s); +}; +#define UTF8_FMT format("utf8_fmt"), transform("utf8_fmt") + +fn utf8_rl_fmt(auto s){ + str new_s = std::string::substr(s, 0, std::string::length(s) - 1); + return std::format("{}", new_s); +}; +#define UTF8_RL_FMT format("utf8_rl_fmt"), transform("utf8_rl_fmt") + +fn int_rl_fmt(auto s){ + if(s == "01\n") return true; /* == TRUE(b'I01\n')[1:] */ + if(s == "00\n") return false; /* == FALSE(b'I00\n')[1:] */ + str new_s = std::string::substr(s, 0, std::string::length(s) - 1); + return std::string::parse_int(new_s, 0); +}; +#define INT_RL_FMT format("int_rl_fmt"), transform("int_rl_fmt") + +fn float_rl_fmt(auto s){ + str new_s = std::string::substr(s, 0, std::string::length(s) - 1); + return std::string::parse_float(new_s); +}; +#define FLOAT_RL_FMT format("float_rl_fmt"), transform("float_rl_fmt") + +fn long_rl_fmt(auto s){ + str new_s = std::string::substr(s, 0, std::string::length(s) - 1); + if(new_s != "" && std::string::at(new_s, std::string::length(new_s) - 1) == "L") + new_s = std::string::substr(new_s, 0, std::string::length(new_s) - 1); + return std::string::parse_int(new_s, 0); +}; +#define LONG_RL_FMT format("long_rl_fmt"), transform("long_rl_fmt") + +fn ascii_rl_fmt(auto s){ + return std::string::substr(s, 0, std::string::length(s) - 1); +}; +#define ASCII_RL_FMT format("ascii_rl_fmt"), transform("ascii_rl_fmt") + +fn integer_rl_fmt(auto s){ + str new_s = std::string::substr(s, 0, std::string::length(s) - 1); + return std::string::parse_int(new_s, 0); +}; +#define INTEGER_RL_FMT format("integer_rl_fmt"), transform("integer_rl_fmt") + +fn string_rl_fmt(auto s){ + str new_s = std::string::substr(s, 0, std::string::length(s) - 1); + auto length = std::string::length(new_s); + if(length >= 2 && new_s[0] == new_s[length - 1] && (new_s[0] == '\'' || new_s[0] == '"')) + new_s = std::string::substr(new_s, 1, length - 1); + else std::error("the STRING opcode argument must be quoted"); + return new_s; +}; +#define STRING_RL_FMT format("string_rl_fmt"), transform("string_rl_fmt") + +enum OpcodesEnum: u8{ + MARK = '(', /* push special markobject on stack */ + STOP = '.', /* every pickle ends with STOP */ + POP = '0', /* discard topmost stack item */ + POP_MARK = '1', /* discard stack top through topmost markobject */ + DUP = '2', /* duplicate top stack item */ + FLOAT = 'F', /* push float object; decimal string argument */ + INT = 'I', /* push integer or bool; decimal string argument */ + BININT = 'J', /* push four-byte signed int */ + BININT1 = 'K', /* push 1-byte unsigned int */ + LONG = 'L', /* push long; decimal string argument */ + BININT2 = 'M', /* push 2-byte unsigned int */ + NONE = 'N', /* push None */ + PERSID = 'P', /* push persistent object; id is taken from string arg */ + BINPERSID = 'Q', /* " " " ; " " " " stack */ + REDUCE = 'R', /* apply callable to argtuple, both on stack */ + STRING = 'S', /* push string; NL-terminated string argument */ + BINSTRING = 'T', /* push string; counted binary string argument */ + SHORT_BINSTRING = 'U', /* " " ; " " " " < 256 bytes */ + UNICODE = 'V', /* push Unicode string; raw-unicode-escaped'd argument */ + BINUNICODE = 'X', /* " " " ; counted UTF-8 string argument */ + APPEND = 'a', /* append stack top to list below it */ + BUILD = 'b', /* call __setstate__ or __dict__.update() */ + GLOBAL = 'c', /* push self.find_class(modname, name); 2 string args */ + DICT = 'd', /* build a dict from stack items */ + EMPTY_DICT = '}', /* push empty dict */ + APPENDS = 'e', /* extend list on stack by topmost stack slice */ + GET = 'g', /* push item from memo on stack; index is string arg */ + BINGET = 'h', /* " " " " " " ; " " 1-byte arg */ + INST = 'i', /* build & push class instance */ + LONG_BINGET = 'j', /* push item from memo on stack; index is 4-byte arg */ + LIST = 'l', /* build list from topmost stack items */ + EMPTY_LIST = ']', /* push empty list */ + OBJ = 'o', /* build & push class instance */ + PUT = 'p', /* store stack top in memo; index is string arg */ + BINPUT = 'q', /* " " " " " ; " " 1-byte arg */ + LONG_BINPUT = 'r', /* " " " " " ; " " 4-byte arg */ + SETITEM = 's', /* add key+value pair to dict */ + TUPLE = 't', /* build tuple from topmost stack items */ + EMPTY_TUPLE = ')', /* push empty tuple */ + SETITEMS = 'u', /* modify dict by adding topmost key+value pairs */ + BINFLOAT = 'G', /* push float; arg is 8-byte float encoding */ + /* ---- Protocol 2 ---- */ + PROTO = 0x80, /* identify pickle protocol */ + NEWOBJ = 0x81, /* build object by applying cls.__new__ to argtuple */ + EXT1 = 0x82, /* push object from extension registry; 1-byte index */ + EXT2 = 0x83, /* ditto, but 2-byte index */ + EXT4 = 0x84, /* ditto, but 4-byte index */ + TUPLE1 = 0x85, /* build 1-tuple from stack top */ + TUPLE2 = 0x86, /* build 2-tuple from two topmost stack items */ + TUPLE3 = 0x87, /* build 3-tuple from three topmost stack items */ + NEWTRUE = 0x88, /* push True */ + NEWFALSE = 0x89, /* push False */ + LONG1 = 0x8A, /* push long from < 256 bytes */ + LONG4 = 0x8B, /* push really big long */ + /* ---- Protocol 3 (Python 3.x) ---- */ + BINBYTES = 'B', /* push bytes; counted binary string argument */ + SHORT_BINBYTES = 'C', /* " " ; " " " " < 256 bytes */ + /* ---- Protocol 4 ---- */ + SHORT_BINUNICODE = 0x8C, /* push short string; UTF-8 length < 256 bytes */ + BINUNICODE8 = 0x8D, /* push very long string */ + BINBYTES8 = 0x8E, /* push very long bytes string */ + EMPTY_SET = 0x8F, /* push empty set on the stack */ + ADDITEMS = 0x90, /* modify set by adding topmost stack items */ + FROZENSET = 0x91, /* build frozenset from topmost stack items */ + NEWOBJ_EX = 0x92, /* like NEWOBJ but work with keyword only arguments */ + STACK_GLOBAL = 0x93, /* same as GLOBAL but using names on the stacks */ + MEMOIZE = 0x94, /* store top of the stack in memo */ + FRAME = 0x95, /* indicate the beginning of a new frame */ + /* ---- Protocol 5 ---- */ + BYTEARRAY8 = 0x96, /* push bytearray */ + NEXT_BUFFER = 0x97, /* push next out-of-band buffer */ + READONLY_BUFFER = 0x98 /* make top of stack readonly */ +}; + +fn readline(){ + auto i = 0; + while(std::mem::read_unsigned($ + i, 1) != '\n') i += 1; + return i + 1; +}; + +struct Opcodes{ + OpcodesEnum opcode; + match(opcode){ + (OpcodesEnum::MARK): {} + (OpcodesEnum::STOP): break; + (OpcodesEnum::POP): {} + (OpcodesEnum::POP_MARK): {} + (OpcodesEnum::DUP): {} + (OpcodesEnum::FLOAT): { + char Float[readline()] [[FLOAT_RL_FMT]]; /* float(readline()[:1]) */ + } + (OpcodesEnum::INT): { + /* == TRUE(b'I01\n')[1:], == FALSE(b'I00\n')[1:], int(readline(), 0) */ + char Int[readline()] [[INT_RL_FMT]]; + } + (OpcodesEnum::BININT): { + s32 Int; + } + (OpcodesEnum::BININT1): { + s8 Int; + } + (OpcodesEnum::LONG): { + /* val = readline()[:-1], val = val and val[-1] == b"L"[0] ? val[:-1]: val */ + char Long[readline()] [[LONG_RL_FMT]]; /* int(val, 0) */ + } + (OpcodesEnum::BININT2): { + u16 Int; + } + (OpcodesEnum::NONE): {} + (OpcodesEnum::PERSID): { + char id[readline()] [[ASCII_RL_FMT]]; /* readline()[:-1].decode("ascii") */ + } + (OpcodesEnum::BINPERSID): {} + (OpcodesEnum::REDUCE): {} + /* + def _decode_string(self, value): + # Used to allow strings from Python 2 to be decoded either as bytes or Unicode strings. + # This should be used only with the STRING, BINSTRING and SHORT_BINSTRING opcodes. + if self.encoding == "bytes": + return value + else: + return value.decode(self.encoding, self.errors) + */ + (OpcodesEnum::STRING): { + /* data must be in quotes ("..." or '...'), dataStripped = stripQuote(readline()[:-1]) */ + /* _decode_string(codecs.escape_decode(dataStripped)[0]) */ + char data[readline()] [[STRING_RL_FMT]]; + } + (OpcodesEnum::BINSTRING): { + s32 length; + char data[length]; /* _decode_string(data) */ + } + (OpcodesEnum::SHORT_BINSTRING): { + u8 length; + char data[length]; /* _decode_string(data) */ + } + (OpcodesEnum::UNICODE): { + /* + "raw-unicode-escape": + Latin-1 encoding with \uXXXX and \UXXXXXXXX for other code points. + Existing backslashes are not escaped in any way. + */ + char data[readline()] [[UTF8_RL_FMT]]; /* str(readline()[:-1], "raw-unicode-escape") */ + } + (OpcodesEnum::BINUNICODE): { + u32 length; + char data[length] [[UTF8_FMT]]; /* str(data, "utf-8", "surrogatepass") */ + } + (OpcodesEnum::APPEND): {} + (OpcodesEnum::BUILD): {} + (OpcodesEnum::GLOBAL): { + char module[readline()] [[UTF8_RL_FMT]]; /* readline()[:-1].decode("utf-8") */ + char name[readline()] [[UTF8_RL_FMT]]; /* readline()[:-1].decode("utf-8") */ + } + (OpcodesEnum::DICT): {} + (OpcodesEnum::EMPTY_DICT): {} + (OpcodesEnum::APPENDS): {} + (OpcodesEnum::GET): { + char index[readline()] [[INTEGER_RL_FMT]]; /* int(readline()[:-1]) */ + } + (OpcodesEnum::BINGET): { + u8 index; + } + (OpcodesEnum::INST): { + char module[readline()] [[ASCII_RL_FMT]]; /* readline()[:-1].decode("ascii") */ + char name[readline()] [[ASCII_RL_FMT]]; /* readline()[:-1].decode("ascii") */ + } + (OpcodesEnum::LONG_BINGET): { + u32 index; + } + (OpcodesEnum::LIST): {} + (OpcodesEnum::EMPTY_LIST): {} + (OpcodesEnum::OBJ): {} + (OpcodesEnum::PUT): { + char index[readline()] [[INTEGER_RL_FMT]]; /* int(readline()[:-1]) */ + } + (OpcodesEnum::BINPUT): { + s8 index; + } + (OpcodesEnum::LONG_BINPUT): { + u32 index; + } + (OpcodesEnum::SETITEM): {} + (OpcodesEnum::TUPLE): {} + (OpcodesEnum::EMPTY_TUPLE): {} + (OpcodesEnum::SETITEMS): {} + (OpcodesEnum::BINFLOAT): { + be double Double; + } + /* ---- Protocol 2 ---- */ + (OpcodesEnum::PROTO): { + u8 version; + } + (OpcodesEnum::NEWOBJ): {} + (OpcodesEnum::EXT1): { + u8 code; + } + (OpcodesEnum::EXT2): { + u16 code; + } + (OpcodesEnum::EXT4): { + s32 code; + } + (OpcodesEnum::TUPLE1): {} + (OpcodesEnum::TUPLE2): {} + (OpcodesEnum::TUPLE3): {} + (OpcodesEnum::NEWTRUE): {} + (OpcodesEnum::NEWFALSE): {} + /* + def decode_long(data): + r"""Decode a long from a two's complement little-endian binary string. + >>> decode_long(b"") => 0 + >>> decode_long(b"\xff\x00") => 255 + >>> decode_long(b"\xff\x7f") => 32767 + >>> decode_long(b"\x00\xff") => -256 + >>> decode_long(b"\x00\x80") => -32768 + >>> decode_long(b"\x80") => -128 + >>> decode_long(b"\x7f") => 127 + """ + return int.from_bytes(data, byteorder="little", signed=True) + */ + (OpcodesEnum::LONG1): { + u8 length; + u8 data[length]; /* decode_long(data) */ + } + (OpcodesEnum::LONG4): { + s32 length; + u8 data[length]; /* decode_long(data) */ + } + /* ---- Protocol 3 (Python 3.x) ---- */ + (OpcodesEnum::BINBYTES): { + u32 length; + u8 bytes[length]; + } + (OpcodesEnum::SHORT_BINBYTES): { + u8 length; + u8 bytes[length]; + } + /* ---- Protocol 4 ---- */ + (OpcodesEnum::SHORT_BINUNICODE): { + u8 length; + char data[length] [[UTF8_FMT]]; /* str(data, "utf-8", "surrogatepass") */ + } + (OpcodesEnum::BINUNICODE8): { + u64 length; + char data[length] [[UTF8_FMT]]; /* str(data, "utf-8", "surrogatepass") */ + } + (OpcodesEnum::BINBYTES8): { + u64 length; + u8 bytes[length]; + } + (OpcodesEnum::EMPTY_SET): {} + (OpcodesEnum::ADDITEMS): {} + (OpcodesEnum::FROZENSET): {} + (OpcodesEnum::NEWOBJ_EX): {} + (OpcodesEnum::STACK_GLOBAL): {} + (OpcodesEnum::MEMOIZE): {} + (OpcodesEnum::FRAME): { + u64 length; + Opcodes opcodes[while($ < addressof(length) + sizeof(length) + length)]; + } + /* ---- Protocol 5 ---- */ + (OpcodesEnum::BYTEARRAY8): { + u64 length; + u8 array[length]; + } + (OpcodesEnum::NEXT_BUFFER): {} + (OpcodesEnum::READONLY_BUFFER): {} + (_): std::error(std::format("Unrecognized {}", opcode)); + } +}; + +struct Pickle{ + Opcodes opcodes[while(!std::mem::eof())]; +}; + +Pickle pickle @ 0x0; diff --git a/tests/patterns/test_data/pickle.hexpat.bin b/tests/patterns/test_data/pickle.hexpat.bin new file mode 100644 index 0000000000000000000000000000000000000000..1931e649aed215f43a919746747d432af59b91d3 GIT binary patch literal 541 zcmY*WJxjwt7_K$38f^s;N2!ZARKflNMa7Rm2N9ekT+(ZMc)csho!CKW9sGb76#ClT z*}>KS;uyinU*O^;HBxVSzTV^g=-E#T`CR(0a1AUW1;rZ7`BHL9gr`DH@eYa^Zx9|) zyoYJZIqO6$LYVe73c3 zz;19!6z*@$tbCvBt}a6JVuy`RLDdVp?F{# zm6h_T40(M8p`-;1WSx3hU@7ho;_E@&e~J6gasPGw<8Ao)1PHT!SN+9yQdZ82oYW&> zNV+(5Mh)Z=tvu4eR@(H8?u46nCvOof&LV|pxHeK;%e5&?MpWTLragjFXMti;kg$vC zG0tgs)68-~xTXeJhF3FQMGa2J45_?^7Qw$W*r{%|1G6C!P3i@lL{YLiJLQS9IOcff Uc#4RCVyoiXuH#PJnkDLuA6@9qu>b%7 literal 0 HcmV?d00001 From 4fc11f1b914c88de07359eaf01f2043967f5737a Mon Sep 17 00:00:00 2001 From: Tim Schneeberger Date: Sun, 21 Sep 2025 11:30:38 +0200 Subject: [PATCH 64/93] patterns: Add ESP32 image pattern (#449) Co-authored-by: Nik --- README.md | 1 + patterns/esp32_image.hexpat | 119 ++++++++++++++++++ .../patterns/test_data/esp32_image.hexpat.bin | Bin 0 -> 15104 bytes 3 files changed, 120 insertions(+) create mode 100644 patterns/esp32_image.hexpat create mode 100644 tests/patterns/test_data/esp32_image.hexpat.bin diff --git a/README.md b/README.md index 7fc5ec93..3d2f2d70 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | DTA | | [`patterns/max_v104.hexpat`](patterns/max_v104.hexpat) | Mechanized Assault and Exploration v1.04 (strategy game) save file format | | DTED | | [`patterns/dted.hexpat`](patterns/dted.hexpat) | Digital Terrain Elevation Data (DTED) | | ELF | `application/x-executable` | [`patterns/elf.hexpat`](patterns/elf.hexpat) | ELF header in elf binaries | +| ESP32 Image | | [`patterns/esp32_image.hexpat`](patterns/esp32_image.hexpat) | Firmware image format for the ESP32 chip family | | EVTX | `application/x-ms-evtx` | [`patterns/evtx.hexpat`](patterns/evtx.hexpat) | MS Windows Vista Event Log | | EXFAT | | [`patterns/fs/exfat.hexpat`](patterns/fs/exfat.hexpat) | Extensible File Allocation Table (exFAT) | | EXT4 | | [`patterns/fs/ext4.hexpat`](patterns/fs/ext4.hexpat) | Ext4 File System | diff --git a/patterns/esp32_image.hexpat b/patterns/esp32_image.hexpat new file mode 100644 index 00000000..f35daf9c --- /dev/null +++ b/patterns/esp32_image.hexpat @@ -0,0 +1,119 @@ +#pragma author timschneeb +#pragma description ESP32 Firmware Image Format + +#pragma endian little + +// Reference: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html + +import std.mem; + +enum esp_chip_id_t : u16 { + ESP_CHIP_ID_ESP32 = 0x0000, + ESP_CHIP_ID_ESP32S2 = 0x0002, + ESP_CHIP_ID_ESP32C3 = 0x0005, + ESP_CHIP_ID_ESP32S3 = 0x0009, + ESP_CHIP_ID_ESP32C2 = 0x000C, + ESP_CHIP_ID_ESP32C6 = 0x000D, + ESP_CHIP_ID_ESP32H2 = 0x0010, + ESP_CHIP_ID_ESP32P4 = 0x0012, + ESP_CHIP_ID_ESP32C5 = 0x0017, + ESP_CHIP_ID_ESP32C61 = 0x0014, + ESP_CHIP_ID_ESP32H21 = 0x0019, + ESP_CHIP_ID_ESP32H4 = 0x001C, + ESP_CHIP_ID_INVALID = 0xFFFF +}; + +enum esp_image_spi_mode_t : u8 { + QIO, + QOUT, + DIO, + DOUT, + FAST_READ, + SLOW_READ +}; + +enum esp_image_spi_freq_t : u8 { + DIV_2, + DIV_3, + DIV_4, + DIV_1 = 0xF +}; + +enum esp_image_flash_size_t : u8 { + FLASH_1MB, + FLASH_2MB, + FLASH_4MB, + FLASH_8MB, + FLASH_16MB, + FLASH_32MB, + FLASH_64MB, + FLASH_128MB, + FLASH_MAX +}; + +bitfield spi_config_t { + esp_image_spi_freq_t spi_speed : 4; + esp_image_flash_size_t spi_size : 4; +}; + +const u32 ESP_APP_DESC_MAGIC_WORD = 0xABCD5432; + +struct esp_app_desc_t { + u32 magic_word; // ESP_APP_DESC_MAGIC_WORD + u32 secure_version; + u32 reserv1[2]; + char version[32]; + char project_name[32]; + char compile_time[16]; + char compile_date[16]; + char idf_ver[32]; + u8 app_elf_sha256[32]; + u16 min_efuse_blk_rev_full; + u16 max_efuse_blk_rev_full; + u8 mmu_page_size; // in log2 format + u8 reserv3[3]; + u32 reserv2[18]; +}; + +struct esp_image_header_t { + u8 magic; // 0xE9 + u8 segment_count; + esp_image_spi_mode_t spi_mode; + spi_config_t spi_cfg; + u32 entry_addr; + u8 wp_pin [[comment("Write protect pin")]]; + u8 spi_pin_drv[3] [[comment("Drive settings for the SPI flash pins")]]; + esp_chip_id_t chip_id; + u8 min_chip_rev [[comment("Deprecated, replaced by min_chip_rev_full")]]; + u16 min_chip_rev_full [[comment("Minimal revision (major*100+minor)")]]; + u16 max_chip_rev_full [[comment("Maximal revision (major*100+minor)")]]; + u8 reserved[4]; + + u8 hash_appended [[comment("If 1, a SHA256 digest 'simple hash' (of the entire image) is appended after the checksum")]]; +}; + +struct esp_image_segment_header_t { + u32 load_addr; + u32 data_len; +}; + +union esp_image_segment_data_t { + u8 data[parent.header.data_len] [[hidden]]; + + // Application segment + if (std::mem::read_unsigned(addressof(data), 4) == ESP_APP_DESC_MAGIC_WORD) { + esp_app_desc_t app_descriptor; + } +}; + +struct esp_image_segment_t { + esp_image_segment_header_t header; + esp_image_segment_data_t data; +}; + +struct esp_image_t { + esp_image_header_t header; + esp_image_segment_t segments[header.segment_count]; +}; + +esp_image_t image @ 0x0; diff --git a/tests/patterns/test_data/esp32_image.hexpat.bin b/tests/patterns/test_data/esp32_image.hexpat.bin new file mode 100644 index 0000000000000000000000000000000000000000..fcf755ebedbdef14738d8cbae2164b51f4b0f17a GIT binary patch literal 15104 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 b@_k|1vWKcxcU-)F>ZA87AMO6Duk8N;VAoTW literal 0 HcmV?d00001 From 7eabc06f4381d8b18e6e3b86bc6adb9ac90cd0b2 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Sun, 21 Sep 2025 14:25:51 +0300 Subject: [PATCH 65/93] Delete patterns/acu_data_compressed.hexpat --- patterns/acu_data_compressed.hexpat | 49 ----------------------------- 1 file changed, 49 deletions(-) delete mode 100644 patterns/acu_data_compressed.hexpat diff --git a/patterns/acu_data_compressed.hexpat b/patterns/acu_data_compressed.hexpat deleted file mode 100644 index bd31c28b..00000000 --- a/patterns/acu_data_compressed.hexpat +++ /dev/null @@ -1,49 +0,0 @@ -#pragma description Assassin's Creed: Unity's Compressed .data file -#pragma MIME archives/acu_data_compressed - -// author = haru233, many thanks to AxCut -// ImHex Hex Pattern File for Assassin's Creed: Unity's Compressed .data files - - -import std.core; -import std.mem; - -enum CompressionType : u8 { - LZO1X_ = 0x00, // Both 0x00 and 0x01 mean LZO1X - LZO1X = 0x01, - LZO2A = 0x02, - xmemdecompress = 0x03, - LZO1C = 0x05 -}; - -struct CHUNK { - u16 Uncompressed_Size; - u16 Compressed_Size; -}; - -struct CHUNK_Data { - u32 Hash; - - u64 i = std::core::array_index(); - u8 data[parent.chunk[i].Compressed_Size]; -}; - -struct PACK { - u64 ID; - padding[2]; - CompressionType Compression_Type; - padding[3]; - u8 Version; - u16 CHUNK_Count; - - CHUNK chunk[CHUNK_Count]; - CHUNK_Data data[CHUNK_Count]; - - -}; - - - -PACK pack[while(!std::mem::eof())] @0x00; - - From ac54dccf7454e7165270cdf106b68fa6c5c8a9d4 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Sun, 21 Sep 2025 14:26:10 +0300 Subject: [PATCH 66/93] Delete patterns/acu_data_decompressed.hexpat --- patterns/acu_data_decompressed.hexpat | 93 --------------------------- 1 file changed, 93 deletions(-) delete mode 100644 patterns/acu_data_decompressed.hexpat diff --git a/patterns/acu_data_decompressed.hexpat b/patterns/acu_data_decompressed.hexpat deleted file mode 100644 index db930534..00000000 --- a/patterns/acu_data_decompressed.hexpat +++ /dev/null @@ -1,93 +0,0 @@ -#pragma description Assassin's Creed: Unity's Decompressed .data file -#pragma MIME archives/acu_data_decompressed - -// Thanks to yretenai on GitHub for helping with the Block Allocator part - -import std.core; -import std.mem; - -struct Block_Allocator_Type0 { - padding[4]; - u32 Class_ID; - u32 Size; -}; - -struct Block_Allocator_Type1 { - padding[4]; - u32 Type_ID; - u32 Size; -}; - -struct Block_Allocator { - u16 Version; - - if (Version == 0) { - u32 Block_Allocator_Number; - Block_Allocator_Type0 block_allocator_type0[Block_Allocator_Number]; - } - - else if (Version == 1) { - u32 Block_Allocator_Number; - Block_Allocator_Type1 block_allocator_type1_[Block_Allocator_Number]; - } - - else if (Version == 2) { - bool Has_Secondary_Block_Allocator; - u32 Main_Block_Allocator_Number; - - Block_Allocator_Type1 block_allocator_type1__[Main_Block_Allocator_Number]; - - if (Has_Secondary_Block_Allocator) { - u32 Secondary_Block_Allocator_Number; - Block_Allocator_Type1 block_allocator_type1___[Secondary_Block_Allocator_Number+1]; - } - } - - -}; - -struct File { - u32 Object_Hash; - u32 File_Size; - u32 Filename_Length; - - if (File_Size > 0) { - if (Filename_Length == 0) { - bool HasBlockAllocator; - - if (HasBlockAllocator) { - Block_Allocator block_allocator; - u8 File_Data[File_Size]; - } - - else - u8 File_Data[File_Size]; - } - - else { - char Filename[Filename_Length]; - - bool HasBlockAllocator; - - if (HasBlockAllocator) { - Block_Allocator block_allocator; - u8 File_Data[File_Size]; - } - - else - u8 File_Data[File_Size]; - - - } - - } - - else - continue; - - - -}; - - -File file[while(!std::mem::eof())] @0x00; From 2f586be9a7b29953f876723bcecff1223cc6c639 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Sun, 21 Sep 2025 14:26:24 +0300 Subject: [PATCH 67/93] Delete patterns/acu_forge.hexpat --- patterns/acu_forge.hexpat | 64 --------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 patterns/acu_forge.hexpat diff --git a/patterns/acu_forge.hexpat b/patterns/acu_forge.hexpat deleted file mode 100644 index 519dc9e8..00000000 --- a/patterns/acu_forge.hexpat +++ /dev/null @@ -1,64 +0,0 @@ -#pragma description Assassin's Creed: Unity's .forge archive file -#pragma MIME archives/forge - -// author = haru233, many thanks to AxCut -// ImHex Hex Pattern File for Assassin's Creed: Unity's .forge files - -import std.core; - -struct Forge_Header { - char MAGIC[8]; - padding[1]; - u32 Version; - u32 File_Data_Header_Offset; -}; - -struct File_Data_Header { - u32 File_Count; - padding[32]; - u64 File_Data_Header2_Offset; -}; - -struct File_Data_Header2 { - u32 File_Count2; - padding[4]; - u64 File_Table_Offset; - padding[12]; - u32 File_Count3; - u64 File_Name_Table_Offset; - padding[8]; -}; - - -struct File_Table { - u64 Raw_Data_Offset; - u64 File_ID; - u32 Raw_Data_Size; -}; - -struct File_Name_Table { - u32 Raw_Data_Size; - padding[40]; - char Filename[128]; - padding[20]; -}; - - -Forge_Header forge_header @0x00; - -File_Data_Header file_data_header @(forge_header.File_Data_Header_Offset); - -File_Data_Header2 file_data_header2 @(file_data_header.File_Data_Header2_Offset); - -File_Table file_table[file_data_header.File_Count] @(file_data_header2.File_Table_Offset); - -File_Name_Table file_name_table[file_data_header.File_Count] @(file_data_header2.File_Name_Table_Offset); - - -struct Raw_Data_Table { - u64 i = std::core::array_index(); - u8 Raw_Data[file_table[i].Raw_Data_Size] @ file_table[i].Raw_Data_Offset; -}; - - -Raw_Data_Table raw_data_table[file_data_header.File_Count] @0x00; From f2cbf61772b7aa487e976423cf632a21ab4ffbb9 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Sun, 21 Sep 2025 14:27:10 +0300 Subject: [PATCH 68/93] Create acu_forge.hexpat --- .../Assassin's Creed: Unity/acu_forge.hexpat | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 patterns/Assassin's Creed: Unity/acu_forge.hexpat diff --git a/patterns/Assassin's Creed: Unity/acu_forge.hexpat b/patterns/Assassin's Creed: Unity/acu_forge.hexpat new file mode 100644 index 00000000..e000a492 --- /dev/null +++ b/patterns/Assassin's Creed: Unity/acu_forge.hexpat @@ -0,0 +1,64 @@ +#pragma description Assassin's Creed: Unity's .forge archive file +#pragma author haru233 + +// many thanks to AxCut +// ImHex Hex Pattern File for Assassin's Creed: Unity's .forge files + +import std.core; + +struct Forge_Header { + char MAGIC[8]; + padding[1]; + u32 Version; + u32 File_Data_Header_Offset; +}; + +struct File_Data_Header { + u32 File_Count; + padding[32]; + u64 File_Data_Header2_Offset; +}; + +struct File_Data_Header2 { + u32 File_Count2; + padding[4]; + u64 File_Table_Offset; + padding[12]; + u32 File_Count3; + u64 File_Name_Table_Offset; + padding[8]; +}; + + +struct File_Table { + u64 Raw_Data_Offset; + u64 File_ID; + u32 Raw_Data_Size; +}; + +struct File_Name_Table { + u32 Raw_Data_Size; + padding[40]; + char Filename[128]; + padding[20]; +}; + + +Forge_Header forge_header @0x00; + +File_Data_Header file_data_header @(forge_header.File_Data_Header_Offset); + +File_Data_Header2 file_data_header2 @(file_data_header.File_Data_Header2_Offset); + +File_Table file_table[file_data_header.File_Count] @(file_data_header2.File_Table_Offset); + +File_Name_Table file_name_table[file_data_header.File_Count] @(file_data_header2.File_Name_Table_Offset); + + +struct Raw_Data_Table { + u64 i = std::core::array_index(); + u8 Raw_Data[file_table[i].Raw_Data_Size] @ file_table[i].Raw_Data_Offset; +}; + + +Raw_Data_Table raw_data_table[file_data_header.File_Count] @0x00; From 0ae3f28c62bc79e77583c4053f8db6ced6d26300 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Sun, 21 Sep 2025 14:27:33 +0300 Subject: [PATCH 69/93] Add files via upload --- .../ACU_DATA_Compressed.hexpat | 47 ++++++++++ .../ACU_DATA_Decompressed.hexpat | 93 +++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 patterns/Assassin's Creed: Unity/ACU_DATA_Compressed.hexpat create mode 100644 patterns/Assassin's Creed: Unity/ACU_DATA_Decompressed.hexpat diff --git a/patterns/Assassin's Creed: Unity/ACU_DATA_Compressed.hexpat b/patterns/Assassin's Creed: Unity/ACU_DATA_Compressed.hexpat new file mode 100644 index 00000000..54f4ee4d --- /dev/null +++ b/patterns/Assassin's Creed: Unity/ACU_DATA_Compressed.hexpat @@ -0,0 +1,47 @@ +#pragma description Assassin's Creed: Unity's Compressed .data file +#pragma author haru233 + +// many thanks to AxCut +// ImHex Hex Pattern File for Assassin's Creed: Unity's Compressed .data files + + +import std.core; +import std.mem; + +enum CompressionType : u8 { + LZO1X_ = 0x00, // Both 0x00 and 0x01 mean LZO1X + LZO1X = 0x01, + LZO2A = 0x02, + xmemdecompress = 0x03, + LZO1C = 0x05 +}; + +struct CHUNK { + u16 Uncompressed_Size; + u16 Compressed_Size; +}; + +struct CHUNK_Data { + u32 Hash; + + u64 i = std::core::array_index(); + u8 data[parent.chunk[i].Compressed_Size]; +}; + +struct PACK { + u64 ID; + padding[2]; + CompressionType Compression_Type; + padding[3]; + u8 Version; + u16 CHUNK_Count; + + CHUNK chunk[CHUNK_Count]; + CHUNK_Data data[CHUNK_Count]; + + +}; + + + +PACK pack[while(!std::mem::eof())] @0x00; \ No newline at end of file diff --git a/patterns/Assassin's Creed: Unity/ACU_DATA_Decompressed.hexpat b/patterns/Assassin's Creed: Unity/ACU_DATA_Decompressed.hexpat new file mode 100644 index 00000000..47373733 --- /dev/null +++ b/patterns/Assassin's Creed: Unity/ACU_DATA_Decompressed.hexpat @@ -0,0 +1,93 @@ +#pragma description Assassin's Creed: Unity's Decompressed .data file +#pragma author haru233 + +// Thanks to yretenai on GitHub for helping with the Block Allocator part + +import std.core; +import std.mem; + +struct Block_Allocator_Type0 { + padding[4]; + u32 Class_ID; + u32 Size; +}; + +struct Block_Allocator_Type1 { + padding[4]; + u32 Type_ID; + u32 Size; +}; + +struct Block_Allocator { + u16 Version; + + if (Version == 0) { + u32 Block_Allocator_Number; + Block_Allocator_Type0 block_allocator_type0[Block_Allocator_Number]; + } + + else if (Version == 1) { + u32 Block_Allocator_Number; + Block_Allocator_Type1 block_allocator_type1_[Block_Allocator_Number]; + } + + else if (Version == 2) { + bool Has_Secondary_Block_Allocator; + u32 Main_Block_Allocator_Number; + + Block_Allocator_Type1 block_allocator_type1__[Main_Block_Allocator_Number]; + + if (Has_Secondary_Block_Allocator) { + u32 Secondary_Block_Allocator_Number; + Block_Allocator_Type1 block_allocator_type1___[Secondary_Block_Allocator_Number+1]; + } + } + + +}; + +struct File { + u32 Object_Hash; + u32 File_Size; + u32 Filename_Length; + + if (File_Size > 0) { + if (Filename_Length == 0) { + bool HasBlockAllocator; + + if (HasBlockAllocator) { + Block_Allocator block_allocator; + u8 File_Data[File_Size]; + } + + else + u8 File_Data[File_Size]; + } + + else { + char Filename[Filename_Length]; + + bool HasBlockAllocator; + + if (HasBlockAllocator) { + Block_Allocator block_allocator; + u8 File_Data[File_Size]; + } + + else + u8 File_Data[File_Size]; + + + } + + } + + else + continue; + + + +}; + + +File file[while(!std::mem::eof())] @0x00; \ No newline at end of file From aa842104cefac5c145ec32394c1ec8d27922d89d Mon Sep 17 00:00:00 2001 From: haruse23 Date: Sun, 21 Sep 2025 14:27:57 +0300 Subject: [PATCH 70/93] Update and rename ACU_DATA_Compressed.hexpat to acu_data_compressed.hexpat --- .../{ACU_DATA_Compressed.hexpat => acu_data_compressed.hexpat} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename patterns/Assassin's Creed: Unity/{ACU_DATA_Compressed.hexpat => acu_data_compressed.hexpat} (90%) diff --git a/patterns/Assassin's Creed: Unity/ACU_DATA_Compressed.hexpat b/patterns/Assassin's Creed: Unity/acu_data_compressed.hexpat similarity index 90% rename from patterns/Assassin's Creed: Unity/ACU_DATA_Compressed.hexpat rename to patterns/Assassin's Creed: Unity/acu_data_compressed.hexpat index 54f4ee4d..cab12712 100644 --- a/patterns/Assassin's Creed: Unity/ACU_DATA_Compressed.hexpat +++ b/patterns/Assassin's Creed: Unity/acu_data_compressed.hexpat @@ -44,4 +44,4 @@ struct PACK { -PACK pack[while(!std::mem::eof())] @0x00; \ No newline at end of file +PACK pack[while(!std::mem::eof())] @0x00; From 12f26f116839a001cc409d14bc5724adcc5c10e9 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Sun, 21 Sep 2025 14:28:16 +0300 Subject: [PATCH 71/93] Update and rename ACU_DATA_Decompressed.hexpat to acu_data_decompressed.hexpat --- ...CU_DATA_Decompressed.hexpat => acu_data_decompressed.hexpat} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename patterns/Assassin's Creed: Unity/{ACU_DATA_Decompressed.hexpat => acu_data_decompressed.hexpat} (93%) diff --git a/patterns/Assassin's Creed: Unity/ACU_DATA_Decompressed.hexpat b/patterns/Assassin's Creed: Unity/acu_data_decompressed.hexpat similarity index 93% rename from patterns/Assassin's Creed: Unity/ACU_DATA_Decompressed.hexpat rename to patterns/Assassin's Creed: Unity/acu_data_decompressed.hexpat index 47373733..84e34e78 100644 --- a/patterns/Assassin's Creed: Unity/ACU_DATA_Decompressed.hexpat +++ b/patterns/Assassin's Creed: Unity/acu_data_decompressed.hexpat @@ -90,4 +90,4 @@ struct File { }; -File file[while(!std::mem::eof())] @0x00; \ No newline at end of file +File file[while(!std::mem::eof())] @0x00; From dcbc6069fd7ecdc4a612c849e1713bfe1565958c Mon Sep 17 00:00:00 2001 From: haruse23 Date: Sun, 21 Sep 2025 14:30:29 +0300 Subject: [PATCH 72/93] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index b4d86526..06486682 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ARC | | [`patterns/arc.hexpat`](patterns/arc.hexpat) | Minecraft Legacy Console Edition ARC files | | ARIA2 | | [`patterns/aria2.hexpat`](patterns/aria2.hexpat) | ARIA2 Download Manager Control files | | ARM VTOR | | [`patterns/arm_cm_vtor.hexpat`](patterns/arm_cm_vtor.hexpat) | ARM Cortex M Vector Table Layout | +| Assassin's Creed: Unity | | [`patterns/Assassin's Creed: Unity`](patterns/Assassin's Creed: Unity) | Assassin's Creed: Unity archive files -- .forge & .data (compressed and decompressed) -- | | Bastion | | [`patterns/bastion/*`](https://gitlab.com/EvelynTSMG/imhex-bastion-pats) | Various [Bastion](https://en.wikipedia.org/wiki/Bastion_(video_game)) files | | BeyondCompare BCSS | | [`patterns/bcss.hexpat`](patterns/bcss.hexpat) | BeyondCompare Snapshot (BCSS) file | | Bencode | `application/x-bittorrent` | [`patterns/bencode.hexpat`](patterns/bencode.hexpat) | Bencode encoding, used by Torrent files | @@ -198,9 +199,6 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ZLIB | `application/zlib` | [`patterns/zlib.hexpat`](patterns/zlib.hexpat) | ZLIB compressed data format | | ZSTD | `application/zstd` | [`patterns/zstd.hexpat`](patterns/zstd.hexpat) | Zstandard compressed data format | | MOD | `3d-model/mod` | [`patterns/DMC3HD-Mod.hexpat`](patterns/dmc3_hd_mod.hexpat) | 3D Model files used in Devil May Cry 3 HD Collection | -| FORGE | `archives/forge` | [`patterns/acu_forge.hexpat`](patterns/acu_forge.hexpat) | .forge archive files used in Assassin's Creed: Unity | -| DATA | `archives/acu_data_compressed` | [`patterns/acu_data_compressed.hexpat`](patterns/acu_data_compressed.hexpat) | Compressed .data archive files used in Assassin's Creed: Unity | -| DATA | `archives/acu_data_decompressed` | [`patterns/acu_data_decompressed.hexpat`](patterns/acu_data_decompressed.hexpat) | Decompressed .data archive files used in Assassin's Creed: Unity | From 1890bf09be0a3fed4a73f572865ca5dd973567f6 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Sun, 21 Sep 2025 14:34:19 +0300 Subject: [PATCH 73/93] Delete patterns/dmc3_hd_mod.hexpat --- patterns/dmc3_hd_mod.hexpat | 165 ------------------------------------ 1 file changed, 165 deletions(-) delete mode 100644 patterns/dmc3_hd_mod.hexpat diff --git a/patterns/dmc3_hd_mod.hexpat b/patterns/dmc3_hd_mod.hexpat deleted file mode 100644 index 9872b781..00000000 --- a/patterns/dmc3_hd_mod.hexpat +++ /dev/null @@ -1,165 +0,0 @@ -#pragma description Devil May Cry 3 HD .mod 3D model file -#pragma MIME 3d-model/capcom.dmc3-hd-mod - -// author = haru233, many thanks to AxCut -// ImHex Hex Pattern File for Capcom's Devil May Cry 3 HD .mod files - - -import std.core; - - -struct ModelHeader { - char ID[4]; - float Version; - padding[8]; - u8 objectCount; - u8 boneCount; - u8 numberTextures; - u8; - u32; - u64; - u64 skeletonOffset; - padding[24]; -}; - -struct ObjectInfo { - u8 meshCount; - u8; - u16 numberVertices; - padding[4]; - u64 meshOffset; - u32 flags; - padding[28]; - float X, Y, Z; - float radius; -}; - -struct Positions { - float positions[3]; -}; - - -struct Normals { - float normal[3]; -}; - - -struct UVs { - s16 uv[2]; -}; - -struct BoneIndices { - u8 boneindex[4]; -}; - -struct Weights { - u16 weight[1]; -}; - -struct MeshSCM { - u16 numberVertices; - u16 textureIndex; - padding[12]; - u64 VerticesPositionsOffset; - u64 NormalsPositionsOffset; - u64 UVsPositionsOffset; - - padding[16]; - u64 unknownOffset; - - u64; - padding[8]; - - Positions positions[numberVertices] @VerticesPositionsOffset; - Normals normals[numberVertices] @NormalsPositionsOffset; - UVs uvs[numberVertices] @UVsPositionsOffset; - - -}; - -struct Mesh { - u16 numberVertices; - u16 textureIndex; - padding[12]; - u64 VerticesPositionsOffset; - u64 NormalsPositionsOffset; - u64 UVsPositionsOffset; - - u64 BoneIndicesOffset; - u64 WeightsOffset; - padding[8]; - - u64; - padding[8]; - - Positions positions[numberVertices] @VerticesPositionsOffset; - Normals normals[numberVertices] @NormalsPositionsOffset; - UVs uvs[numberVertices] @UVsPositionsOffset; - - BoneIndices b_index[numberVertices] @BoneIndicesOffset; - Weights weights[numberVertices] @WeightsOffset; - - -}; - - -struct Hierarchy { - u8 hierarchy; -}; - -struct HierarchyOrder { - u8 hierarchyorder; -}; - -struct Unknown { - u8; -}; - -struct Transform { - float x; - float y; - float z; - float length; // sqrt(x*x + y*y + z*z) - padding[16]; -}; - -struct Skeleton{ - u32 hierarchyOffset; - u32 hierarchyOrderOffset; - u32 unknownOffset; - u32 transformsOffset; -}; - - - - - -ModelHeader modelheader @ 0x00; -ObjectInfo objects_info[modelheader.objectCount] @ 0x40; - -u32 objectOffset; - -struct Object { - u64 i = std::core::array_index(); - if (modelheader.ID == "SCM ") { - objectOffset = objects_info[0].meshOffset; - MeshSCM meshscm[objects_info[i].meshCount] @ objects_info[i].meshOffset; - - - } else { - objectOffset = objects_info[0].meshOffset; - Mesh mesh[objects_info[i].meshCount] @ objects_info[i].meshOffset; - } -}; - -Object objects[modelheader.objectCount] @objectOffset; - -Skeleton skeleton @modelheader.skeletonOffset; - -Hierarchy hierarchy[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.hierarchyOffset); - -HierarchyOrder hierarchyorder[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.hierarchyOrderOffset); - -Unknown unknown[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.unknownOffset); - -Transform transform[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.transformsOffset); From d5dd93b40e3e7aae0e8f0f285e766928dab09287 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Sun, 21 Sep 2025 14:35:04 +0300 Subject: [PATCH 74/93] Create dmc3_hd_mod.hexpat --- .../dmc3_hd_mod.hexpat | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 patterns/Devil May Cry HD Collection/dmc3_hd_mod.hexpat diff --git a/patterns/Devil May Cry HD Collection/dmc3_hd_mod.hexpat b/patterns/Devil May Cry HD Collection/dmc3_hd_mod.hexpat new file mode 100644 index 00000000..41b21733 --- /dev/null +++ b/patterns/Devil May Cry HD Collection/dmc3_hd_mod.hexpat @@ -0,0 +1,165 @@ +#pragma description Devil May Cry 3 HD .mod 3D model file +#pragma MIME 3d-model/capcom.dmc3-hd-mod + +// author = haru233, many thanks to AxCut +// ImHex Hex Pattern File for Capcom's Devil May Cry 3 HD .mod files + + +import std.core; + + +struct ModelHeader { + char ID[4]; + float Version; + padding[8]; + u8 objectCount; + u8 boneCount; + u8 numberTextures; + u8; + u32; + u64; + u64 skeletonOffset; + padding[24]; +}; + +struct ObjectInfo { + u8 meshCount; + u8; + u16 numberVertices; + padding[4]; + u64 meshOffset; + u32 flags; + padding[28]; + float X, Y, Z; + float radius; +}; + +struct Positions { + float positions[3]; +}; + + +struct Normals { + float normal[3]; +}; + + +struct UVs { + s16 uv[2]; +}; + +struct BoneIndices { + u8 boneindex[4]; +}; + +struct Weights { + u16 weight[1]; +}; + +struct MeshSCM { + u16 numberVertices; + u16 textureIndex; + padding[12]; + u64 VerticesPositionsOffset; + u64 NormalsPositionsOffset; + u64 UVsPositionsOffset; + + padding[16]; + u64 unknownOffset; + + u64; + padding[8]; + + Positions positions[numberVertices] @VerticesPositionsOffset; + Normals normals[numberVertices] @NormalsPositionsOffset; + UVs uvs[numberVertices] @UVsPositionsOffset; + + +}; + +struct Mesh { + u16 numberVertices; + u16 textureIndex; + padding[12]; + u64 VerticesPositionsOffset; + u64 NormalsPositionsOffset; + u64 UVsPositionsOffset; + + u64 BoneIndicesOffset; + u64 WeightsOffset; + padding[8]; + + u64; + padding[8]; + + Positions positions[numberVertices] @VerticesPositionsOffset; + Normals normals[numberVertices] @NormalsPositionsOffset; + UVs uvs[numberVertices] @UVsPositionsOffset; + + BoneIndices b_index[numberVertices] @BoneIndicesOffset; + Weights weights[numberVertices] @WeightsOffset; + + +}; + + +struct Hierarchy { + u8 hierarchy; +}; + +struct HierarchyOrder { + u8 hierarchyorder; +}; + +struct Unknown { + u8; +}; + +struct Transform { + float x; + float y; + float z; + float length; // sqrt(x*x + y*y + z*z) + padding[16]; +}; + +struct Skeleton{ + u32 hierarchyOffset; + u32 hierarchyOrderOffset; + u32 unknownOffset; + u32 transformsOffset; +}; + + + + + +ModelHeader modelheader @ 0x00; +ObjectInfo objects_info[modelheader.objectCount] @ 0x40; + +u32 objectOffset; + +struct Object { + u64 i = std::core::array_index(); + if (modelheader.ID == "SCM ") { + objectOffset = objects_info[0].meshOffset; + MeshSCM meshscm[objects_info[i].meshCount] @ objects_info[i].meshOffset; + + + } else { + objectOffset = objects_info[0].meshOffset; + Mesh mesh[objects_info[i].meshCount] @ objects_info[i].meshOffset; + } +}; + +Object objects[modelheader.objectCount] @objectOffset; + +Skeleton skeleton @modelheader.skeletonOffset; + +Hierarchy hierarchy[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.hierarchyOffset); + +HierarchyOrder hierarchyorder[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.hierarchyOrderOffset); + +Unknown unknown[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.unknownOffset); + +Transform transform[modelheader.boneCount] @(modelheader.skeletonOffset + skeleton.transformsOffset); From e481feeba1d452851a536d063df6eccae9af4281 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Sun, 21 Sep 2025 14:38:13 +0300 Subject: [PATCH 75/93] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 06486682..76dfcfa4 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ARC | | [`patterns/arc.hexpat`](patterns/arc.hexpat) | Minecraft Legacy Console Edition ARC files | | ARIA2 | | [`patterns/aria2.hexpat`](patterns/aria2.hexpat) | ARIA2 Download Manager Control files | | ARM VTOR | | [`patterns/arm_cm_vtor.hexpat`](patterns/arm_cm_vtor.hexpat) | ARM Cortex M Vector Table Layout | -| Assassin's Creed: Unity | | [`patterns/Assassin's Creed: Unity`](patterns/Assassin's Creed: Unity) | Assassin's Creed: Unity archive files -- .forge & .data (compressed and decompressed) -- | +| Assassin's Creed: Unity | [`patterns/Assassin's Creed: Unity`](patterns/Assassin's Creed: Unity) | Assassin's Creed: Unity archive files -- .forge & .data (compressed and decompressed) -- | | Bastion | | [`patterns/bastion/*`](https://gitlab.com/EvelynTSMG/imhex-bastion-pats) | Various [Bastion](https://en.wikipedia.org/wiki/Bastion_(video_game)) files | | BeyondCompare BCSS | | [`patterns/bcss.hexpat`](patterns/bcss.hexpat) | BeyondCompare Snapshot (BCSS) file | | Bencode | `application/x-bittorrent` | [`patterns/bencode.hexpat`](patterns/bencode.hexpat) | Bencode encoding, used by Torrent files | @@ -58,6 +58,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | CREDHIST | | [`patterns/credhist.hexpat`](patterns/credhist.hexpat) | CREDHIST Format | | DDS | `image/vnd-ms.dds` | [`patterns/dds.hexpat`](patterns/dds.hexpat) | DirectDraw Surface | | DEX | | [`patterns/dex.hexpat`](patterns/dex.hexpat) | Dalvik EXecutable Format | +| Devil May Cry HD Collection | [`patterns/Devil May Cry HD Collection`](patterns/Devil May Cry HD Collection) | 3D Model files used in Devil May Cry 3 HD Collection | | DICOM | `application/dicom` | [`patterns/dicom.hexpat`](patterns/dicom.hexpat) | DICOM image format | | DMG | | [`patterns/dmg.hexpat`](patterns/dmg.hexpat) | Apple Disk Image Trailer (DMG) | | DMP | | [`patterns/dmp64.hexpat`](patterns/dmp64.hexpat) | Windows Kernel Dump(DMP64) | @@ -198,7 +199,6 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ZIP | `application/zip` | [`patterns/zip.hexpat`](patterns/zip.hexpat) | End of Central Directory Header, Central Directory File Headers | | ZLIB | `application/zlib` | [`patterns/zlib.hexpat`](patterns/zlib.hexpat) | ZLIB compressed data format | | ZSTD | `application/zstd` | [`patterns/zstd.hexpat`](patterns/zstd.hexpat) | Zstandard compressed data format | -| MOD | `3d-model/mod` | [`patterns/DMC3HD-Mod.hexpat`](patterns/dmc3_hd_mod.hexpat) | 3D Model files used in Devil May Cry 3 HD Collection | From 6c706f21d48f34c9dff39d9c220d482cdf699dec Mon Sep 17 00:00:00 2001 From: haruse23 Date: Sun, 21 Sep 2025 14:38:55 +0300 Subject: [PATCH 76/93] Update dmc3_hd_mod.hexpat --- patterns/Devil May Cry HD Collection/dmc3_hd_mod.hexpat | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patterns/Devil May Cry HD Collection/dmc3_hd_mod.hexpat b/patterns/Devil May Cry HD Collection/dmc3_hd_mod.hexpat index 41b21733..67a4ebac 100644 --- a/patterns/Devil May Cry HD Collection/dmc3_hd_mod.hexpat +++ b/patterns/Devil May Cry HD Collection/dmc3_hd_mod.hexpat @@ -1,7 +1,7 @@ #pragma description Devil May Cry 3 HD .mod 3D model file -#pragma MIME 3d-model/capcom.dmc3-hd-mod +#pragma author haru233 -// author = haru233, many thanks to AxCut +// many thanks to AxCut // ImHex Hex Pattern File for Capcom's Devil May Cry 3 HD .mod files From d7a6dab2eb0d6466b4243e92c550a0fa313e8778 Mon Sep 17 00:00:00 2001 From: haruse23 Date: Sun, 21 Sep 2025 14:40:03 +0300 Subject: [PATCH 77/93] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 76dfcfa4..44d79040 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ARC | | [`patterns/arc.hexpat`](patterns/arc.hexpat) | Minecraft Legacy Console Edition ARC files | | ARIA2 | | [`patterns/aria2.hexpat`](patterns/aria2.hexpat) | ARIA2 Download Manager Control files | | ARM VTOR | | [`patterns/arm_cm_vtor.hexpat`](patterns/arm_cm_vtor.hexpat) | ARM Cortex M Vector Table Layout | -| Assassin's Creed: Unity | [`patterns/Assassin's Creed: Unity`](patterns/Assassin's Creed: Unity) | Assassin's Creed: Unity archive files -- .forge & .data (compressed and decompressed) -- | +| Assassin's Creed: Unity | | [`patterns/Assassin's Creed: Unity`](patterns/Assassin's Creed: Unity) | Assassin's Creed: Unity archive files -- .forge & .data (compressed and decompressed) -- | | Bastion | | [`patterns/bastion/*`](https://gitlab.com/EvelynTSMG/imhex-bastion-pats) | Various [Bastion](https://en.wikipedia.org/wiki/Bastion_(video_game)) files | | BeyondCompare BCSS | | [`patterns/bcss.hexpat`](patterns/bcss.hexpat) | BeyondCompare Snapshot (BCSS) file | | Bencode | `application/x-bittorrent` | [`patterns/bencode.hexpat`](patterns/bencode.hexpat) | Bencode encoding, used by Torrent files | @@ -58,7 +58,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | CREDHIST | | [`patterns/credhist.hexpat`](patterns/credhist.hexpat) | CREDHIST Format | | DDS | `image/vnd-ms.dds` | [`patterns/dds.hexpat`](patterns/dds.hexpat) | DirectDraw Surface | | DEX | | [`patterns/dex.hexpat`](patterns/dex.hexpat) | Dalvik EXecutable Format | -| Devil May Cry HD Collection | [`patterns/Devil May Cry HD Collection`](patterns/Devil May Cry HD Collection) | 3D Model files used in Devil May Cry 3 HD Collection | +| Devil May Cry HD Collection | | [`patterns/Devil May Cry HD Collection`](patterns/Devil May Cry HD Collection) | 3D Model files used in Devil May Cry 3 HD Collection | | DICOM | `application/dicom` | [`patterns/dicom.hexpat`](patterns/dicom.hexpat) | DICOM image format | | DMG | | [`patterns/dmg.hexpat`](patterns/dmg.hexpat) | Apple Disk Image Trailer (DMG) | | DMP | | [`patterns/dmp64.hexpat`](patterns/dmp64.hexpat) | Windows Kernel Dump(DMP64) | From a35004665f7a8b09edac97575327369d9271b053 Mon Sep 17 00:00:00 2001 From: gmestanley Date: Mon, 22 Sep 2025 02:15:37 -0300 Subject: [PATCH 78/93] patterns: Credit to NE and improvements on NES (#445) * Add credit to ne.hexpat * Add many changes to nes.hexpat * Fixing dependance on variables declared in if statement --------- Co-authored-by: Nik --- patterns/ne.hexpat | 1 + patterns/nes.hexpat | 308 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 244 insertions(+), 65 deletions(-) diff --git a/patterns/ne.hexpat b/patterns/ne.hexpat index 3cae5c2f..6c825b11 100644 --- a/patterns/ne.hexpat +++ b/patterns/ne.hexpat @@ -1,3 +1,4 @@ +#pragma author gmestanley #pragma description Microsoft DOS NE executable #pragma MIME application/x-ms-ne-executable diff --git a/patterns/nes.hexpat b/patterns/nes.hexpat index 1fdce899..6292ada0 100644 --- a/patterns/nes.hexpat +++ b/patterns/nes.hexpat @@ -2,87 +2,265 @@ #pragma description Nintendo Entertainment System ROM (.nes) #pragma MIME application/x-nes-rom -import std.mem; import std.string; -bitfield iNES07Flags { - mirroringIsVertical : 1; - batterybackedPRGRAM : 1; - trainerOf512Bytes : 1; - ignoreMirroring : 1; - mapperLowerNybble : 4; - vsUnisystem : 1; - playchoice10 : 1; - nes2Format : 2 [[name("nes2.0Format")]]; - mapperHigherNybble : 4; -} [[name("iNES0.7Flags")]]; - -bitfield Flags9 { - isPAL : 1; - padding : 7; -}; - -bitfield Flags10 { - tvSystem : 2; - padding : 2; - prgRAM : 1; - busConflicts : 1; - padding : 2; -}; - -struct iNESFlags { - iNES07Flags ines07Flags [[name("ines0.7Flags")]]; - if (!ines07Flags.nes2Format) { - u8 prgRAM8KBMultiplier; - Flags9 flags9; - Flags10 flags10; - restLength = 5; - } - else { - restLength = 9; - } -}; - -u8 restLength; +bitfield Flags { + mirroringIsHorizontal : 1; + ignoreMirroring : 1; + batterybackedPRGRAM : 1; + trainerOf512Bytes : 1; + lowerMapperNybble : 4; +}; + +enum ConsoleType : u8 { + Regular, + VsSystem, + PlayChoice10, + ExtendedConsoleType +}; + +fn consoleType(u8 bits) { + ConsoleType type = bits; + return type; +}; + +fn nes2Format(u8 bits) { + return std::string::to_string(bits == 2) + " (" + std::string::to_string(bits) + ")"; +}; + +bitfield iNESFlags7 { + consoleType : 2 [[format("consoleType")]]; + nes2Format : 2 [[format("nes2Format"), name("nes2.0Format")]]; + higherMapperNybble : 4; +}; + +bitfield iNESFlags9 { + tvSystem : 1 [[comment("0 = NTSC, 1 = PAL")]]; + padding : 7; +}; + +fn formatDualTVSystem(u8 bits) { + match (bits) { + (0): return "NTSC"; + (2): return "PAL"; + (1 || 3): return "Dual Compatible"; + } +}; + +bitfield iNESFlags10 { + dualTVSystem : 2 [[format("formatDualTVSystem")]]; + padding : 2; + prgRAM : 1; + busConflicts : 1; +}; + +bitfield MapperExtra { + highestMapperNybble : 4; + submapper : 4; +}; + +bitfield ROMSize { + extraPRGROMSize : 4; + extraCHRROMSize : 4; +}; + +bitfield PRGRAMSize { + prgRAMShiftCount : 4; + eepromShiftCount : 4 [[comment("EEPROM = Non-volatile PRG RAM")]]; +}; + +bitfield CHRRAMSize { + chrRAMSizeShiftCount : 4; + chrNVRAMSizeShiftCount : 4; +}; + +enum TimingList : u8 { + NTSC, + PAL, + MultiRegion, + Dendy +}; + +fn Timing(u8 value) { + TimingList type = value; + return type; +}; + +bitfield Timing { + processorTiming : 2 [[format("Timing")]]; + padding : 6; +}; + +bitfield VsSystemType { + vsPPUType : 4; + vsHardwareType: 4; +}; + +enum ExtendedConsoleType : ConsoleType { + DecimalModeFamiclone = 3, + PlugThrough, + VT01, + VT02, + VT03, + VT09, + VT32, + VT3xx, + UM6578, + FamicomNetworkSystem +}; + +fn formatExtendedConsoleType(u8 nybble) { + ExtendedConsoleType type = nybble; + return type; +}; + +bitfield ExtendedConsoleTypeByte { + type : 4 [[format("formatExtendedConsoleType")]]; + padding : 4; +}; + +bitfield MiscellaneousROMs { + numberOfMiscellaneousROMs : 2; + padding : 6; +}; + +bitfield DefaultExpansionDevice { + defaultExpansionDevice : 6; +}; + +struct NES2Attributes { + MapperExtra mapperExtra; + ROMSize romSize; + PRGRAMSize prgRAMSize; + CHRRAMSize chrRAMSize; + Timing timing; + if (parent.inesFlags7.consoleType == ConsoleType::VsSystem) { + VsSystemType vsSystemType; + } + else if (parent.inesFlags7.consoleType == ConsoleType::ExtendedConsoleType) { + ExtendedConsoleTypeByte ExtendedConsoleTypeByte; + } + else { + padding[1]; + } + MiscellaneousROMs miscellaneousROMs; + DefaultExpansionDevice defaultExpansionDevice; +}; + +fn renderEOF(str string) { + return "\"NES\""; +}; struct Header { - char identifier[4]; - u8 prgROM16KBMultiplier; - u8 chrROM8KBMultiplier; - iNESFlags inesFlags; - char rest[restLength]; + char identifier[4] [[format("renderEOF")]]; + u8 prgROMSizeBy16KiBs; + u8 chrROMSizeBy8KiBs; + Flags flags; + if ($[0x07] & 12 != 4) { + iNESFlags7 inesFlags7; + if (inesFlags7.nes2Format) + NES2Attributes nes2Attributes; + else if ($[0x07] & 12 == 0 && !std::mem::read_unsigned($, 4)) { + u8 prgRAMSizeBy8KiBs; + iNESFlags9 inesFlags9; + iNESFlags10 inesFlags10; + } + } }; Header header @ 0x00; +u8 trainer[512*header.flags.trainerOf512Bytes] @ 0x10; + +enum CHRType : u8 { + CHRROM, + CHRRAM +}; + +fn chrType(u8 value) { + CHRType enumValue = value; + return enumValue; +}; + +fn chrSize(u8 value) { + u24 actualSize; + if (value == 4) actualSize = 262144; + else actualSize = 8192 * header.chrROMSizeBy8KiBs; + return std::string::to_string(value) + " (" + std::string::to_string(actualSize) + ")"; +}; + +bitfield MemorySize { + prgROMSizeBy16KiBs : 4; + chrType : 1 [[format("chrType")]]; + chrSize : 3 [[format("chrSize")]]; +}; + +enum ArrangementList : u8 { + Horizontal, + Vertical +}; + +fn arrangement(u8 value) { + ArrangementList enumValue = value; + return enumValue; +}; + +enum MapperList : u8 { + NROM, + CNROM, + UNROM, + GNROM, + MMC +}; + +fn mapper(u8 value) { + MapperList enumValue = value; + return enumValue; +}; + +bitfield CartridgeType { + nametableArrangement : 1 [[format("arrangement")]]; + mapper : 7 [[format("mapper")]]; +}; + enum EncodingType : u8 { - ASCII = 1 + None, + ASCII, + JIS }; +fn titleLength(u8 value) { return value+1; }; + struct OfficialHeader { - char title[16] [[hex::spec_name("Title Registration Area")]]; - u16 programChecksum; - u16 characterChecksum; - u8 memorySize [[hex::spec_name("Cartridge Memory Size")]]; - u8 cartridgeType; - EncodingType encodingType [[hex::spec_name("Registration Character Type Distinction")]]; - u8 titleLength [[hex::spec_name("Registration Characters Count")]]; - u8 makerID [[hex::spec_name("Maker Code")]]; - u8 complementaryChecksum; + char title[16] [[hex::spec_name("Title Registration Area")]]; + u16 programChecksum; + u16 characterChecksum; + MemorySize memorySize [[hex::spec_name("Cartridge Memory Size")]]; + CartridgeType cartridgeType; + EncodingType encodingType [[hex::spec_name("Registration Characters Type Distinction")]]; + u8 titleLength [[hex::spec_name("Registration Characters Count"), transform("titleLength")]]; + u8 licenseeID [[hex::spec_name("Maker Code")]]; + u8 complementaryChecksum [[hex::spec_name("Checksum for characterChecksum~makerID")]]; }; -union OfficialHeaderUnion { - u8 miscellaneousData[26]; - OfficialHeader officialHeader; +u24 calculatedPRGROMSize = 16384 * ((0x0100 * $[9] & 0x0F) * ($[7] & 12 == 8) + header.prgROMSizeBy16KiBs); + +fn hasOfficialHeader() { + u8 sum; + for (u8 i = 0, i < 8, i += 1) { + sum += $[(calculatedPRGROMSize - 14) + i]; + } + return !sum; }; struct PRGROM { - u8 data[16384*header.prgROM16KBMultiplier-32]; - OfficialHeaderUnion officialHeaderUnion; - u16 nmi; - u16 entryPoint; - u16 externalIRQ; + u8 data[calculatedPRGROMSize - 26 * hasOfficialHeader() - 6]; + if (hasOfficialHeader()) + OfficialHeader officialHeader; + u16 nmi; + u16 resetVector [[comment("Entry Point")]]; + u16 externalIRQ; }; -PRGROM prgROM @ sizeof(header); -u8 chrROM[8192*header.chrROM8KBMultiplier] @ sizeof(header)+sizeof(prgROM); \ No newline at end of file +PRGROM prgROM @ 0x10 + sizeof(trainer); +u8 chrROM[8192 * ((0x0100 * $[9] >> 4) * ($[7] & 12 == 8) + header.chrROMSizeBy8KiBs)] @ addressof(prgROM) + 16384 * header.prgROMSizeBy16KiBs; From faff9e0364c5a1ce11c3307543ed26f2783f3459 Mon Sep 17 00:00:00 2001 From: Nik Date: Mon, 1 Dec 2025 21:38:00 +0100 Subject: [PATCH 79/93] git: Add option to customize pattern language repo and git hash with workflow call --- .github/workflows/tests.yml | 7 +++++++ CMakeLists.txt | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c9a61d28..2f0a9c24 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,6 +8,11 @@ on: repository_dispatch: types: [run_tests] workflow_call: + inputs: + pattern_language_git_repo: + type: string + pattern_language_git_hash: + type: string jobs: tests: @@ -63,6 +68,8 @@ jobs: -DIMHEX_PATTERNS_ENABLE_UNIT_TESTS=ON \ -DLIBPL_ENABLE_TESTS=OFF \ -DLIBPL_ENABLE_CLI=OFF \ + -DIMHEX_PATTERNS_LIBPL_GIT_REPO="${{ inputs.pattern_language_git_repo }}" \ + -DIMHEX_PATTERNS_LIBPL_GIT_HASH="${{ inputs.pattern_language_git_hash }}" \ -G Ninja \ .. ninja unit_tests diff --git a/CMakeLists.txt b/CMakeLists.txt index cbbb75e5..2bf89d3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,10 +16,22 @@ endif() if(NOT TARGET libpl) include(FetchContent) + if (NOT DEFINED DIMHEX_PATTERNS_LIBPL_GIT_REPO OR DIMHEX_PATTERNS_LIBPL_GIT_REPO STREQUAL "") + set(LIBPL_GIT_REPO "https://github.com/WerWolv/PatternLanguage") + else() + set(LIBPL_GIT_REPO ${DIMHEX_PATTERNS_LIBPL_GIT_REPO}) + endif() + + if (NOT DEFINED IMHEX_PATTERNS_LIBPL_GIT_HASH OR IMHEX_PATTERNS_LIBPL_GIT_HASH STREQUAL "") + set(LIBPL_GIT_TAG "master") + else() + set(LIBPL_GIT_TAG ${IMHEX_PATTERNS_LIBPL_GIT_HASH}) + endif() + FetchContent_Declare( pattern_language - GIT_REPOSITORY https://github.com/WerWolv/PatternLanguage - GIT_TAG master + GIT_REPOSITORY ${LIBPL_GIT_REPO} + GIT_TAG ${LIBPL_GIT_TAG} ) FetchContent_MakeAvailable(pattern_language) From 7ea863269e3252fe6fefa6ade2e9b36cdcb24821 Mon Sep 17 00:00:00 2001 From: Nik Date: Mon, 1 Dec 2025 21:44:39 +0100 Subject: [PATCH 80/93] build: Fix variable name for Git repository reference --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bf89d3c..17a3e362 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,10 +16,10 @@ endif() if(NOT TARGET libpl) include(FetchContent) - if (NOT DEFINED DIMHEX_PATTERNS_LIBPL_GIT_REPO OR DIMHEX_PATTERNS_LIBPL_GIT_REPO STREQUAL "") + if (NOT DEFINED IMHEX_PATTERNS_LIBPL_GIT_REPO OR IMHEX_PATTERNS_LIBPL_GIT_REPO STREQUAL "") set(LIBPL_GIT_REPO "https://github.com/WerWolv/PatternLanguage") else() - set(LIBPL_GIT_REPO ${DIMHEX_PATTERNS_LIBPL_GIT_REPO}) + set(LIBPL_GIT_REPO ${IMHEX_PATTERNS_LIBPL_GIT_REPO}) endif() if (NOT DEFINED IMHEX_PATTERNS_LIBPL_GIT_HASH OR IMHEX_PATTERNS_LIBPL_GIT_HASH STREQUAL "") @@ -39,4 +39,4 @@ endif() if(IMHEX_PATTERNS_ENABLE_UNIT_TESTS) add_subdirectory(tests) -endif() \ No newline at end of file +endif() From b2f126d22f892ddc5bdeac6aefe498450db8b624 Mon Sep 17 00:00:00 2001 From: Nik Date: Tue, 2 Dec 2025 23:06:30 +0100 Subject: [PATCH 81/93] constants: Update constants to new system --- constants/_schema.json | 15 +- constants/crc16.json | 69 ++-- constants/crc32.json | 27 +- constants/http_status.json | 315 --------------- constants/linux_errors.json | 780 ------------------------------------ 5 files changed, 33 insertions(+), 1173 deletions(-) delete mode 100644 constants/http_status.json delete mode 100644 constants/linux_errors.json diff --git a/constants/_schema.json b/constants/_schema.json index 9cd1cc58..d2b4238c 100644 --- a/constants/_schema.json +++ b/constants/_schema.json @@ -29,23 +29,10 @@ "title": "Items", "type": "object", "required": [ - "type", "value", "name" ], "properties": { - "type": { - "$id": "#root/values/items/type", - "title": "Type", - "type": "string", - "default": "", - "examples": [ - "int16be", - "int16le", - "int10" - ], - "pattern": "^(int10|int16le|int16be)$" - }, "value": { "$id": "#root/values/items/value", "title": "Value", @@ -54,7 +41,7 @@ "examples": [ "ACDC" ], - "pattern": "^([0-9a-fA-F]+)$" + "pattern": "^.*$" }, "name": { "$id": "#root/values/items/name", diff --git a/constants/crc16.json b/constants/crc16.json index 2e7fa22d..aa75c4f8 100644 --- a/constants/crc16.json +++ b/constants/crc16.json @@ -2,118 +2,95 @@ "name": "CRC16 Constants", "values": [ { - "type": "int16be", - "value": "1021", + "value": "10 21", "name": "CRC-16/CCITT-FALSE Polynomial" }, { - "type": "int16be", - "value": "8005", + "value": "80 05", "name": "CRC-16/ARC Polynomial" }, { - "type": "int16be", - "value": "1021", + "value": "10 21", "name": "CRC-16/AUG-CCITT Polynomial" }, { - "type": "int16be", - "value": "8005", + "value": "80 05", "name": "CRC-16/BUYPASS Polynomial" }, { - "type": "int16be", - "value": "C867", + "value": "C8 67", "name": "CRC-16/CDMA2000 Polynomial" }, { - "type": "int16be", - "value": "8005", + "value": "80 05", "name": "CRC-16/DDS-110 Polynomial" }, { - "type": "int16be", - "value": "0589", + "value": "05 89", "name": "CRC-16/DECT-R Polynomial" }, { - "type": "int16be", - "value": "0589", + "value": "05 89", "name": "CRC-16/DECT-X Polynomial" }, { - "type": "int16be", - "value": "3D65", + "value": "3D 65", "name": "CRC-16/DNP Polynomial" }, { - "type": "int16be", - "value": "3D65", + "value": "3D 65", "name": "CRC-16/EN-13757 Polynomial" }, { - "type": "int16be", - "value": "1021", + "value": "10 21", "name": "CRC-16/GENIBUS Polynomial" }, { - "type": "int16be", - "value": "8005", + "value": "80 05", "name": "CRC-16/MAXIM Polynomial" }, { - "type": "int16be", - "value": "1021", + "value": "10 21", "name": "CRC-16/MCRF4XX Polynomial" }, { - "type": "int16be", - "value": "1021", + "value": "10 21", "name": "CRC-16/RIELLO Polynomial" }, { - "type": "int16be", - "value": "8BB7", + "value": "8B B7", "name": "CRC-16/T10-DIF Polynomial" }, { - "type": "int16be", - "value": "A097", + "value": "A0 97", "name": "CRC-16/TELEDISK Polynomial" }, { - "type": "int16be", - "value": "1021", + "value": "10 21", "name": "CRC-16/TMS37157 Polynomial" }, { - "type": "int16be", - "value": "8005", + "value": "80 05", "name": "CRC-16/USB Polynomial" }, { - "type": "int16be", - "value": "1021", + "value": "10 21", "name": "CRC-A Polynomial" }, { - "type": "int16be", - "value": "1021", + "value": "10 21", "name": "CRC-16/KERMIT Polynomial" }, { - "type": "int16be", - "value": "8005", + "value": "80 05", "name": "CRC-16/MODBUS Polynomial" }, { - "type": "int16be", - "value": "1021", + "value": "10 21", "name": "CRC-16/X-25 Polynomial" }, { - "type": "int16be", - "value": "1021", + "value": "10 21", "name": "CRC-16/XMODEM Polynomial" } ] diff --git a/constants/crc32.json b/constants/crc32.json index d4ea31e0..151d9d2b 100644 --- a/constants/crc32.json +++ b/constants/crc32.json @@ -2,48 +2,39 @@ "name": "CRC32 Constants", "values": [ { - "type": "int16be", - "value": "04C11DB7", + "value": "04 C1 1D B7", "name": "CRC-32 Polynomial" }, { - "type": "int16be", - "value": "04C11DB7", + "value": "04 C1 1D B7", "name": "CRC-32/BZIP2 Polynomial" }, { - "type": "int16be", - "value": "1EDC6F41", + "value": "1E DC 6F 41", "name": "CRC-32C Polynomial" }, { - "type": "int16be", - "value": "A833982B", + "value": "A8 33 98 2B", "name": "CRC-32D Polynomial" }, { - "type": "int16be", - "value": "04C11DB7", + "value": "04 C1 1D B7", "name": "CRC-32/MPEG-2 Polynomial" }, { - "type": "int16be", - "value": "04C11DB7", + "value": "04 C1 1D B7", "name": "CRC-32/POSIX Polynomial" }, { - "type": "int16be", - "value": "814141AB", + "value": "81 41 41 AB", "name": "CRC-32Q Polynomial" }, { - "type": "int16be", - "value": "04C11DB7", + "value": "04 C1 1D B7", "name": "CRC-32/JAMCRC Polynomial" }, { - "type": "int16be", - "value": "000000AF", + "value": "00 00 00 AF", "name": "CRC-32/XFER Polynomial" } ] diff --git a/constants/http_status.json b/constants/http_status.json deleted file mode 100644 index e5d4a8c9..00000000 --- a/constants/http_status.json +++ /dev/null @@ -1,315 +0,0 @@ -{ - "name": "HTTP Status Codes", - "values": [ - { - "type": "int10", - "value": "100", - "name": "Continue" - }, - { - "type": "int10", - "value": "101", - "name": "Switching Protocols" - }, - { - "type": "int10", - "value": "102", - "name": "Processing (WebDAV; RFC 2518)" - }, - { - "type": "int10", - "value": "103", - "name": "Early Hints (RFC 8297)" - }, - { - "type": "int10", - "value": "200", - "name": "OK" - }, - { - "type": "int10", - "value": "201", - "name": "Created" - }, - { - "type": "int10", - "value": "202", - "name": "Accepted" - }, - { - "type": "int10", - "value": "203", - "name": "Non-Authoritative Information (since HTTP/1.1)" - }, - { - "type": "int10", - "value": "204", - "name": "No Content" - }, - { - "type": "int10", - "value": "205", - "name": "Reset Content" - }, - { - "type": "int10", - "value": "206", - "name": "Partial Content (RFC 7233)" - }, - { - "type": "int10", - "value": "207", - "name": "Multi-Status (WebDAV; RFC 4918)" - }, - { - "type": "int10", - "value": "208", - "name": "Already Reported (WebDAV; RFC 5842)" - }, - { - "type": "int10", - "value": "226", - "name": "IM Used (RFC 3229)" - }, - { - "type": "int10", - "value": "300", - "name": "Multiple Choices" - }, - { - "type": "int10", - "value": "301", - "name": "Moved Permanently" - }, - { - "type": "int10", - "value": "302", - "name": "Found (Previously \"Moved temporarily\")" - }, - { - "type": "int10", - "value": "303", - "name": "See Other (since HTTP/1.1)" - }, - { - "type": "int10", - "value": "304", - "name": "Not Modified (RFC 7232)" - }, - { - "type": "int10", - "value": "305", - "name": "Use Proxy (since HTTP/1.1)" - }, - { - "type": "int10", - "value": "306", - "name": "Switch Proxy" - }, - { - "type": "int10", - "value": "307", - "name": "Temporary Redirect (since HTTP/1.1)" - }, - { - "type": "int10", - "value": "308", - "name": "Permanent Redirect (RFC 7538)" - }, - { - "type": "int10", - "value": "400", - "name": "Bad Request" - }, - { - "type": "int10", - "value": "401", - "name": "Unauthorized (RFC 7235)" - }, - { - "type": "int10", - "value": "402", - "name": "Payment Required" - }, - { - "type": "int10", - "value": "403", - "name": "Forbidden" - }, - { - "type": "int10", - "value": "404", - "name": "Not Found" - }, - { - "type": "int10", - "value": "405", - "name": "Method Not Allowed" - }, - { - "type": "int10", - "value": "406", - "name": "Not Acceptable" - }, - { - "type": "int10", - "value": "407", - "name": "Proxy Authentication Required (RFC 7235)" - }, - { - "type": "int10", - "value": "408", - "name": "Request Timeout" - }, - { - "type": "int10", - "value": "409", - "name": "Conflict" - }, - { - "type": "int10", - "value": "410", - "name": "Gone" - }, - { - "type": "int10", - "value": "411", - "name": "Length Required" - }, - { - "type": "int10", - "value": "412", - "name": "Precondition Failed (RFC 7232)" - }, - { - "type": "int10", - "value": "413", - "name": "Payload Too Large (RFC 7231)" - }, - { - "type": "int10", - "value": "414", - "name": "URI Too Long (RFC 7231)" - }, - { - "type": "int10", - "value": "415", - "name": "Unsupported Media Type (RFC 7231)" - }, - { - "type": "int10", - "value": "416", - "name": "Range Not Satisfiable (RFC 7233)" - }, - { - "type": "int10", - "value": "417", - "name": "Expectation Failed" - }, - { - "type": "int10", - "value": "418", - "name": "I'm a teapot (RFC 2324, RFC 7168)" - }, - { - "type": "int10", - "value": "421", - "name": "Misdirected Request (RFC 7540)" - }, - { - "type": "int10", - "value": "422", - "name": "Unprocessable Entity (WebDAV; RFC 4918)" - }, - { - "type": "int10", - "value": "424", - "name": "Failed Dependency (WebDAV; RFC 4918)" - }, - { - "type": "int10", - "value": "425", - "name": "Too Early (RFC 8470)" - }, - { - "type": "int10", - "value": "426", - "name": "Upgrade Required" - }, - { - "type": "int10", - "value": "428", - "name": "Precondition Required (RFC 6585)" - }, - { - "type": "int10", - "value": "429", - "name": "Too Many Requests (RFC 6585)" - }, - { - "type": "int10", - "value": "431", - "name": "Request Header Fields Too Large (RFC 6585)" - }, - { - "type": "int10", - "value": "451", - "name": "Unavailable For Legal Reasons (RFC 7725)" - }, - { - "type": "int10", - "value": "500", - "name": "Internal Server Error" - }, - { - "type": "int10", - "value": "501", - "name": "Not Implemented" - }, - { - "type": "int10", - "value": "502", - "name": "Bad Gateway" - }, - { - "type": "int10", - "value": "503", - "name": "Service Unavailable" - }, - { - "type": "int10", - "value": "504", - "name": "Gateway Timeout" - }, - { - "type": "int10", - "value": "505", - "name": "HTTP Version Not Supported" - }, - { - "type": "int10", - "value": "506", - "name": "Variant Also Negotiates (RFC 2295)" - }, - { - "type": "int10", - "value": "507", - "name": "Insufficient Storage (WebDAV; RFC 4918)" - }, - { - "type": "int10", - "value": "508", - "name": "Loop Detected (WebDAV; RFC 5842)" - }, - { - "type": "int10", - "value": "510", - "name": "Not Extended (RFC 2774)" - }, - { - "type": "int10", - "value": "511", - "name": "Network Authentication Required (RFC 6585)" - } - ] -} \ No newline at end of file diff --git a/constants/linux_errors.json b/constants/linux_errors.json deleted file mode 100644 index 5ad59be3..00000000 --- a/constants/linux_errors.json +++ /dev/null @@ -1,780 +0,0 @@ -{ - "name": "Linux Error Codes", - "values": [ - { - "type": "int10", - "value": "1", - "name": "EPERM", - "desc": "Operation not permitted" - }, - { - "type": "int10", - "value": "2", - "name": "ENOENT", - "desc": "No such file or directory" - }, - { - "type": "int10", - "value": "3", - "name": "ESRCH", - "desc": "No such process" - }, - { - "type": "int10", - "value": "4", - "name": "EINTR", - "desc": "Interrupted system call" - }, - { - "type": "int10", - "value": "5", - "name": "EIO", - "desc": "I/O error" - }, - { - "type": "int10", - "value": "6", - "name": "ENXIO", - "desc": "No such device or address" - }, - { - "type": "int10", - "value": "7", - "name": "E2BIG", - "desc": "Argument list too long" - }, - { - "type": "int10", - "value": "8", - "name": "ENOEXEC", - "desc": "Exec format error" - }, - { - "type": "int10", - "value": "9", - "name": "EBADF", - "desc": "Bad file value" - }, - { - "type": "int10", - "value": "10", - "name": "ECHILD", - "desc": "No child processes" - }, - { - "type": "int10", - "value": "11", - "name": "EAGAIN", - "desc": "Try again" - }, - { - "type": "int10", - "value": "12", - "name": "ENOMEM", - "desc": "Out of memory" - }, - { - "type": "int10", - "value": "13", - "name": "EACCES", - "desc": "Permission denied" - }, - { - "type": "int10", - "value": "14", - "name": "EFAULT", - "desc": "Bad address" - }, - { - "type": "int10", - "value": "15", - "name": "ENOTBLK", - "desc": "Block device required" - }, - { - "type": "int10", - "value": "16", - "name": "EBUSY", - "desc": "Device or resource busy" - }, - { - "type": "int10", - "value": "17", - "name": "EEXIST", - "desc": "File exists" - }, - { - "type": "int10", - "value": "18", - "name": "EXDEV", - "desc": "Cross-device link" - }, - { - "type": "int10", - "value": "19", - "name": "ENODEV", - "desc": "No such device" - }, - { - "type": "int10", - "value": "20", - "name": "ENOTDIR", - "desc": "Not a directory" - }, - { - "type": "int10", - "value": "21", - "name": "EISDIR", - "desc": "Is a directory" - }, - { - "type": "int10", - "value": "22", - "name": "EINVAL", - "desc": "Invalid argument" - }, - { - "type": "int10", - "value": "23", - "name": "ENFILE", - "desc": "File table overflow" - }, - { - "type": "int10", - "value": "24", - "name": "EMFILE", - "desc": "Too many open files" - }, - { - "type": "int10", - "value": "25", - "name": "ENOTTY", - "desc": "Not a typewriter" - }, - { - "type": "int10", - "value": "26", - "name": "ETXTBSY", - "desc": "Text file busy" - }, - { - "type": "int10", - "value": "27", - "name": "EFBIG", - "desc": "File too large" - }, - { - "type": "int10", - "value": "28", - "name": "ENOSPC", - "desc": "No space left on device" - }, - { - "type": "int10", - "value": "29", - "name": "ESPIPE", - "desc": "Illegal seek" - }, - { - "type": "int10", - "value": "30", - "name": "EROFS", - "desc": "Read-only file system" - }, - { - "type": "int10", - "value": "31", - "name": "EMLINK", - "desc": "Too many links" - }, - { - "type": "int10", - "value": "32", - "name": "EPIPE", - "desc": "Broken pipe" - }, - { - "type": "int10", - "value": "33", - "name": "EDOM", - "desc": "Math argument out of domain of func" - }, - { - "type": "int10", - "value": "34", - "name": "ERANGE", - "desc": "Math result not representable" - }, - { - "type": "int10", - "value": "35", - "name": "EDEADLK", - "desc": "Resource deadlock would occur" - }, - { - "type": "int10", - "value": "36", - "name": "ENAMETOOLONG", - "desc": "File name too long" - }, - { - "type": "int10", - "value": "37", - "name": "ENOLCK", - "desc": "No record locks available" - }, - { - "type": "int10", - "value": "38", - "name": "ENOSYS", - "desc": "Function not implemented" - }, - { - "type": "int10", - "value": "39", - "name": "ENOTEMPTY", - "desc": "Directory not empty" - }, - { - "type": "int10", - "value": "40", - "name": "ELOOP", - "desc": "Too many symbolic links encountered" - }, - { - "type": "int10", - "value": "42", - "name": "ENOMSG", - "desc": "No message of desired type" - }, - { - "type": "int10", - "value": "43", - "name": "EIDRM", - "desc": "Identifier removed" - }, - { - "type": "int10", - "value": "44", - "name": "ECHRNG", - "desc": "Channel value out of range" - }, - { - "type": "int10", - "value": "45", - "name": "EL2NSYNC", - "desc": "Level 2 not synchronized" - }, - { - "type": "int10", - "value": "46", - "name": "EL3HLT", - "desc": "Level 3 halted" - }, - { - "type": "int10", - "value": "47", - "name": "EL3RST", - "desc": "Level 3 reset" - }, - { - "type": "int10", - "value": "48", - "name": "ELNRNG", - "desc": "Link value out of range" - }, - { - "type": "int10", - "value": "49", - "name": "EUNATCH", - "desc": "Protocol driver not attached" - }, - { - "type": "int10", - "value": "50", - "name": "ENOCSI", - "desc": "No CSI structure available" - }, - { - "type": "int10", - "value": "51", - "name": "EL2HLT", - "desc": "Level 2 halted" - }, - { - "type": "int10", - "value": "52", - "name": "EBADE", - "desc": "Invalid exchange" - }, - { - "type": "int10", - "value": "53", - "name": "EBADR", - "desc": "Invalid request descriptor" - }, - { - "type": "int10", - "value": "54", - "name": "EXFULL", - "desc": "Exchange full" - }, - { - "type": "int10", - "value": "55", - "name": "ENOANO", - "desc": "No anode" - }, - { - "type": "int10", - "value": "56", - "name": "EBADRQC", - "desc": "Invalid request code" - }, - { - "type": "int10", - "value": "57", - "name": "EBADSLT", - "desc": "Invalid slot" - }, - { - "type": "int10", - "value": "59", - "name": "EBFONT", - "desc": "Bad font file format" - }, - { - "type": "int10", - "value": "60", - "name": "ENOSTR", - "desc": "Device not a stream" - }, - { - "type": "int10", - "value": "61", - "name": "ENODATA", - "desc": "No data available" - }, - { - "type": "int10", - "value": "62", - "name": "ETIME", - "desc": "Timer expired" - }, - { - "type": "int10", - "value": "63", - "name": "ENOSR", - "desc": "Out of streams resources" - }, - { - "type": "int10", - "value": "64", - "name": "ENONET", - "desc": "Machine is not on the network" - }, - { - "type": "int10", - "value": "65", - "name": "ENOPKG", - "desc": "Package not installed" - }, - { - "type": "int10", - "value": "66", - "name": "EREMOTE", - "desc": "Object is remote" - }, - { - "type": "int10", - "value": "67", - "name": "ENOLINK", - "desc": "Link has been severed" - }, - { - "type": "int10", - "value": "68", - "name": "EADV", - "desc": "Advertise error" - }, - { - "type": "int10", - "value": "69", - "name": "ESRMNT", - "desc": "Srmount error" - }, - { - "type": "int10", - "value": "70", - "name": "ECOMM", - "desc": "Communication error on send" - }, - { - "type": "int10", - "value": "71", - "name": "EPROTO", - "desc": "Protocol error" - }, - { - "type": "int10", - "value": "72", - "name": "EMULTIHOP", - "desc": "Multihop attempted" - }, - { - "type": "int10", - "value": "73", - "name": "EDOTDOT", - "desc": "RFS specific error" - }, - { - "type": "int10", - "value": "74", - "name": "EBADMSG", - "desc": "Not a data message" - }, - { - "type": "int10", - "value": "75", - "name": "EOVERFLOW", - "desc": "Value too large for defined data type" - }, - { - "type": "int10", - "value": "76", - "name": "ENOTUNIQ", - "desc": "Name not unique on network" - }, - { - "type": "int10", - "value": "77", - "name": "EBADFD", - "desc": "File descriptor in bad state" - }, - { - "type": "int10", - "value": "78", - "name": "EREMCHG", - "desc": "Remote address changed" - }, - { - "type": "int10", - "value": "79", - "name": "ELIBACC", - "desc": "Can not access a needed shared library" - }, - { - "type": "int10", - "value": "80", - "name": "ELIBBAD", - "desc": "Accessing a corrupted shared library" - }, - { - "type": "int10", - "value": "81", - "name": "ELIBSCN", - "desc": ".lib section in a.out corrupted" - }, - { - "type": "int10", - "value": "82", - "name": "ELIBMAX", - "desc": "Attempting to link in too many shared libraries" - }, - { - "type": "int10", - "value": "83", - "name": "ELIBEXEC", - "desc": "Cannot exec a shared library directly" - }, - { - "type": "int10", - "value": "84", - "name": "EILSEQ", - "desc": "Illegal byte sequence" - }, - { - "type": "int10", - "value": "85", - "name": "ERESTART", - "desc": "Interrupted system call should be restarted" - }, - { - "type": "int10", - "value": "86", - "name": "ESTRPIPE", - "desc": "Streams pipe error" - }, - { - "type": "int10", - "value": "87", - "name": "EUSERS", - "desc": "Too many users" - }, - { - "type": "int10", - "value": "88", - "name": "ENOTSOCK", - "desc": "Socket operation on non-socket" - }, - { - "type": "int10", - "value": "89", - "name": "EDESTADDRREQ", - "desc": "Destination address required" - }, - { - "type": "int10", - "value": "90", - "name": "EMSGSIZE", - "desc": "Message too long" - }, - { - "type": "int10", - "value": "91", - "name": "EPROTOTYPE", - "desc": "Protocol wrong type for socket" - }, - { - "type": "int10", - "value": "92", - "name": "ENOPROTOOPT", - "desc": "Protocol not available" - }, - { - "type": "int10", - "value": "93", - "name": "EPROTONOSUPPORT", - "desc": "Protocol not supported" - }, - { - "type": "int10", - "value": "94", - "name": "ESOCKTNOSUPPORT", - "desc": "Socket type not supported" - }, - { - "type": "int10", - "value": "95", - "name": "EOPNOTSUPP", - "desc": "Operation not supported on transport endpoint" - }, - { - "type": "int10", - "value": "96", - "name": "EPFNOSUPPORT", - "desc": "Protocol family not supported" - }, - { - "type": "int10", - "value": "97", - "name": "EAFNOSUPPORT", - "desc": "Address family not supported by protocol" - }, - { - "type": "int10", - "value": "98", - "name": "EADDRINUSE", - "desc": "Address already in use" - }, - { - "type": "int10", - "value": "99", - "name": "EADDRNOTAVAIL", - "desc": "Cannot assign requested address" - }, - { - "type": "int10", - "value": "100", - "name": "ENETDOWN", - "desc": "Network is down" - }, - { - "type": "int10", - "value": "101", - "name": "ENETUNREACH", - "desc": "Network is unreachable" - }, - { - "type": "int10", - "value": "102", - "name": "ENETRESET", - "desc": "Network dropped connection because of reset" - }, - { - "type": "int10", - "value": "103", - "name": "ECONNABORTED", - "desc": "Software caused connection abort" - }, - { - "type": "int10", - "value": "104", - "name": "ECONNRESET", - "desc": "Connection reset by peer" - }, - { - "type": "int10", - "value": "105", - "name": "ENOBUFS", - "desc": "No buffer space available" - }, - { - "type": "int10", - "value": "106", - "name": "EISCONN", - "desc": "Transport endpoint is already connected" - }, - { - "type": "int10", - "value": "107", - "name": "ENOTCONN", - "desc": "Transport endpoint is not connected" - }, - { - "type": "int10", - "value": "108", - "name": "ESHUTDOWN", - "desc": "Cannot send after transport endpoint shutdown" - }, - { - "type": "int10", - "value": "109", - "name": "ETOOMANYREFS", - "desc": "Too many references: cannot splice" - }, - { - "type": "int10", - "value": "110", - "name": "ETIMEDOUT", - "desc": "Connection timed out" - }, - { - "type": "int10", - "value": "111", - "name": "ECONNREFUSED", - "desc": "Connection refused" - }, - { - "type": "int10", - "value": "112", - "name": "EHOSTDOWN", - "desc": "Host is down" - }, - { - "type": "int10", - "value": "113", - "name": "EHOSTUNREACH", - "desc": "No route to host" - }, - { - "type": "int10", - "value": "114", - "name": "EALREADY", - "desc": "Operation already in progress" - }, - { - "type": "int10", - "value": "115", - "name": "EINPROGRESS", - "desc": "Operation now in progress" - }, - { - "type": "int10", - "value": "116", - "name": "ESTALE", - "desc": "Stale NFS file handle" - }, - { - "type": "int10", - "value": "117", - "name": "EUCLEAN", - "desc": "Structure needs cleaning" - }, - { - "type": "int10", - "value": "118", - "name": "ENOTNAM", - "desc": "Not a XENIX named type file" - }, - { - "type": "int10", - "value": "119", - "name": "ENAVAIL", - "desc": "No XENIX semaphores available" - }, - { - "type": "int10", - "value": "120", - "name": "EISNAM", - "desc": "Is a named type file" - }, - { - "type": "int10", - "value": "121", - "name": "EREMOTEIO", - "desc": "Remote I/O error" - }, - { - "type": "int10", - "value": "122", - "name": "EDQUOT", - "desc": "Quota exceeded" - }, - { - "type": "int10", - "value": "123", - "name": "ENOMEDIUM", - "desc": "No medium found" - }, - { - "type": "int10", - "value": "124", - "name": "EMEDIUMTYPE", - "desc": "Wrong medium type" - }, - { - "type": "int10", - "value": "125", - "name": "ECANCELED", - "desc": "Operation Canceled" - }, - { - "type": "int10", - "value": "126", - "name": "ENOKEY", - "desc": "Required key not available" - }, - { - "type": "int10", - "value": "127", - "name": "EKEYEXPIRED", - "desc": "Key has expired" - }, - { - "type": "int10", - "value": "128", - "name": "EKEYREVOKED", - "desc": "Key has been revoked" - }, - { - "type": "int10", - "value": "129", - "name": "EKEYREJECTED", - "desc": "Key was rejected by service" - }, - { - "type": "int10", - "value": "130", - "name": "EOWNERDEAD", - "desc": "Owner died" - }, - { - "type": "int10", - "value": "131", - "name": "ENOTRECOVERABLE", - "desc": "State not recoverable" - } - - ] -} \ No newline at end of file From da005f0172f6e1a2144c23663060d775e3a6be7a Mon Sep 17 00:00:00 2001 From: Nik Date: Fri, 5 Dec 2025 21:10:24 +0100 Subject: [PATCH 82/93] git: Fix json schema checking --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2f0a9c24..1f12f44d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -82,9 +82,9 @@ jobs: - name: 📎 Validate JSON Files run: | cd constants - for file in ./[!_schema.json]*; do jsonschema -i $file _schema.json; done + for file in ./[!_]*; do jsonschema -i $file _schema.json; done cd .. cd tips - for file in ./[!_schema.json]*; do jsonschema -i $file _schema.json; done + for file in ./[!_]*; do jsonschema -i $file _schema.json; done cd .. From 53384a4a548b3fc9a26ef0d9d8325c6539e54adf Mon Sep 17 00:00:00 2001 From: Gal1leo Gal1lei <69310009+GGal1leo@users.noreply.github.com> Date: Fri, 5 Dec 2025 20:12:45 +0000 Subject: [PATCH 83/93] patterns: Added Windows Notepad State File Parser (#463) --- README.md | 1 + patterns/notepad-state.hexpat | 95 +++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 patterns/notepad-state.hexpat diff --git a/README.md b/README.md index 3d2f2d70..c8f935ec 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | NE | `application/x-ms-ne-executable` | [`patterns/ne.hexpat`](patterns/ne.hexpat) | NE header and Standard NE fields | | nes | | [`patterns/nes.hexpat`](patterns/nes.hexpat) | .nes file format | | NotepadCache | | [`patterns/notepad-cache.hexpat`](patterns/notepad-cache.hexpat) | Windows Notepad Cache | +| NotepadStateFile | | [`patterns/notepad-state.hexpat`](patterns/notepad-state.hexpat) | Windows Notepad .bin State files | | NotepadWindowState | | [`patterns/notepadwindowstate.hexpat`](patterns/notepadwindowstate.hexpat) | Windows 11 Notepad - Window State .bin file | | NRO | | [`patterns/nro.hexpat`](patterns/nro.hexpat) | Nintendo Switch NRO files | | NTAG | | [`patterns/ntag.hexpat`](patterns/ntag.hexpat) | NTAG213/NTAG215/NTAG216, NFC Forum Type 2 Tag compliant IC | diff --git a/patterns/notepad-state.hexpat b/patterns/notepad-state.hexpat new file mode 100644 index 00000000..36522761 --- /dev/null +++ b/patterns/notepad-state.hexpat @@ -0,0 +1,95 @@ +#pragma author Gal1leo +#pragma description Windows Notepad State Files +#pragma magic [ 4E 50 00 ] @ 0x00 + + +import type.leb128; +import type.time; +import type.magic; +import std.time; + + +std::mem::Section currentSection; + + +using uLEB128 = type::uLEB128; + +enum Encodings: u8 { + ANSI = 0x01, + UTF_16LE = 0x02, + UTF_16BE = 0x03, + UTF_8BOM = 0x04, + UTF_8 = 0x05, +}; + +enum LineEndings: u8 { + CRLF = 0x01, + CR = 0x02, + LF = 0x03, +}; + +struct MoreOptionsBlock { + u8 unknown_spell_1; + u8 unknown_spell_2; + u8 formatting_type; +}; + + +struct ConfigBlock { + bool word_wrap; + bool rtl; + bool show_unicode; + uLEB128 more_options_number; + MoreOptionsBlock more_options_block; +}; + + +struct UnsavedChunk { + uLEB128 cursor_position; + uLEB128 deletion_number; + uLEB128 addition_number; + char16 chars[addition_number]; + char crc32[4] [[format("hash_format"), comment("CRC32 hash of the entire unsaved chunk structure")]]; +}; + +struct Notepad_File { + type::Magic<"NP\0"> signature [[comment("File signature")]]; + bool is_saved; + if ( is_saved ) { + uLEB128 path_length; + char16 path[path_length] [[comment("Path of saved file on disk")]]; + uLEB128 file_size; + Encodings encoding; + LineEndings line_endings; + uLEB128 last_write [[format("format_filetime")]]; + char sha256[32] [[format("hash_format")]]; + u8 unknown1; + } + u8 unknown2; + uLEB128 selection_start; + uLEB128 selection_end; + ConfigBlock config_block; + type::uLEB128 content_length; + currentSection = std::mem::create_section("File Content"); + char16 content[content_length]; + std::mem::copy_value_to_section(content, currentSection, 0); + bool contain_unsaved_data; + char crc32[4] [[format("hash_format")]]; + UnsavedChunk unsaved_chunks[while(!std::mem::eof())]; +}; + +fn hash_format(auto bytes) { + str hash_hex; + for(u8 i=0, i < sizeof(bytes), i+=1) { + hash_hex = hash_hex + std::format("{:02X}",bytes[i]); + } + return hash_hex; +}; + +fn format_filetime(uLEB128 data) { + // We will only support dates back to the epoch + std::time::Time time64 = std::time::to_utc((data / 10000000.0) - 11644473600.0); + return std::time::format(time64, "%c"); +}; + +Notepad_File file @ $; \ No newline at end of file From cc7eb7d764de2a6b5ef0a4a9a9b0e64c589b1c06 Mon Sep 17 00:00:00 2001 From: Khoo Hao Yit Date: Sat, 6 Dec 2025 04:14:08 +0800 Subject: [PATCH 84/93] patterns: Add support for Unity Asset Bundle (#461) --- README.md | 1 + patterns/unity-asset-bundle.hexpat | 177 +++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 patterns/unity-asset-bundle.hexpat diff --git a/README.md b/README.md index c8f935ec..e7ce061c 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | UEFI Boot Entry | | [`patterns/uefi_boot_entry.hexpat`](patterns/uefi_boot_entry.hexpat) | UEFI Boot Entry (Load option) | | UEFI Variable Store | | [`patterns/uefi_fv_varstore.hexpat`](patterns/uefi_fv_varstore.hexpat) | UEFI Firmware Volume Variable Store | | UF2 | | [`patterns/uf2.hexpat`](patterns/uf2.hexpat) | [USB Flashing Format](https://github.com/microsoft/uf2) | +| Unity Asset Bundle | | [`patterns/unity-asset-bundle.hexpat`](patterns/unity-asset-bundle.hexpat) | Unity Asset Bundle | | Valve VPK | | [`patterns/valve_vpk.hexpat`](valve_vpk.hexpat) | Valve Package File | | VBMeta | | [`patterns/vbmeta.hexpat`](patterns/vbmeta.hexpat) | Android VBMeta image | | VDF | | [`patterns/vdf.hexpat`](patterns/vdf.hexpat) | Binary Value Data Format (.vdf) files | diff --git a/patterns/unity-asset-bundle.hexpat b/patterns/unity-asset-bundle.hexpat new file mode 100644 index 00000000..2ef7e09a --- /dev/null +++ b/patterns/unity-asset-bundle.hexpat @@ -0,0 +1,177 @@ +#pragma author Khoo Hao Yit +#pragma description Unity Asset Bundle + +#pragma magic [ "UnityWeb\0" ] @ 0x00 +#pragma magic [ "UnityRaw\0" ] @ 0x00 +#pragma magic [ "UnityArchive\0" ] @ 0x00 +#pragma magic [ "UnityFS\0" ] @ 0x00 +#pragma endian big + +// Reference: +// https://archive.vg-resource.com/thread-43269.html +// https://github.com/K0lb3/UnityPy +// https://github.com/Perfare/AssetStudio +// https://docs.unity3d.com/550/Documentation/Manual/AssetBundleInternalStructure.html + +import std.core; +import std.sys; +import hex.dec; + +enum Compression: u8 { + None = 0, + Lzma = 1, + Lz4 = 2, + Lz4hc = 3, + Lzham = 4, +}; + +enum Flag : u32 { + CompressionMask = 0b0000111111, + BlocksAndDirectoryInfoCombined = 0b0001000000, + BlockInfoAtTheEnd = 0b0010000000, + OldWebPluginCompatibility = 0b0100000000, + BlockInfoNeedPaddingAtStart = 0b1000000000, +}; + +std::mem::Section blockData; +auto cursor = -1; + +namespace v1 { + struct Level { + u32 compressedSize; + u32 decompressedSize; + }; + + struct DirectoryRecord { + char path[]; + u32 offset; + u32 size; + + std::mem::Bytes data @ offset in blockData; + }; + + struct Header { + u32 nodeCount; + v1::DirectoryRecord nodes[nodeCount]; + }; +} + +namespace v2 { + struct BlockInfo { + u32 decompressedSize; + u32 compressedSize; + u16 flags; + + u8 compressedData[compressedSize] @ cursor in 0 [[sealed, name(std::format("blockInfo{:d}", std::core::array_index()))]]; + cursor += compressedSize; + + std::mem::Section decompressedData = std::mem::create_section("decompressedData"); + match (flags & Flag::CompressionMask) { + (_): std::unimplemented(); + (Compression::None): std::mem::copy_value_to_section(compressedData, decompressedData, 0); + (Compression::Lzma): hex::dec::lzma_decompress(compressedData, decompressedData); + (Compression::Lz4): hex::dec::lz4_decompress(compressedData, decompressedData, false); + (Compression::Lz4hc): hex::dec::lz4_decompress(compressedData, decompressedData, false); + (Compression::Lzham): std::unimplemented(); + } + std::mem::copy_section_to_section( + decompressedData, + 0, + blockData, + std::mem::get_section_size(blockData), + std::mem::get_section_size(decompressedData) + ); + std::mem::delete_section(decompressedData); + }; + + struct DirectoryRecord { + s64 offset; + s64 size; + u32 flags; + char path[]; + + std::mem::Bytes data @ offset in blockData; + }; + + struct Header { + blockData = std::mem::create_section("BlockData"); + std::mem::Bytes<16> dataHash; + s32 blockInfoCount; + v2::BlockInfo blockInfos[blockInfoCount]; + u32 nodeCount; + v2::DirectoryRecord nodes[nodeCount]; + }; +} + +struct AssetBundle { + char signature[]; + u32 version; + char unityVersion[]; + char unityRevision[]; + if (signature == "UnityArchive\0") { + std::unimplemented(); + } + if (signature != "UnityFS\0" && version != 6) { + if (version >= 4) { + std::mem::Bytes<16> hash; + u32 crc; + } + u32 minimumStreamedBytes; + u32 size; + u32 numberOfLevelsToDownloadBeforeStreaming; + s32 levelCount; + v1::Level levels[levelCount]; + if (version >= 2) { + u32 completeFileSize; + } + if (version >= 3) { + u32 fileInfoHeaderSize; + } + $ = size; + u8 compressedBlockInfo[levels[std::core::member_count(levels) - 1].compressedSize] [[sealed]]; + blockData = std::mem::create_section("BlockInfoAndData"); + if (signature == "UnityWeb\0") { + hex::dec::lzma_decompress(compressedBlockInfo, blockData); + } else { + std::mem::copy_value_to_section(compressedBlockInfo, blockData, 0); + } + v1::Header header @ 0 in blockData; + return; + } + s64 size; + u32 compressedBlockInfoSize; + u32 decompressedBlockInfoSize; + u32 flags; + if (signature != "UnityFS\0") { + $ += 1; + } + if (version >= 7) { + $ += -$ & 0b1111; + } + + if (flags & Flag::BlockInfoAtTheEnd) { + u8 compressedBlockInfo[compressedBlockInfoSize] @ std::mem::size() - compressedBlockInfo [[sealed]]; + } else { + std::assert_warn(flags & Flag::BlocksAndDirectoryInfoCombined, "Expected BlocksAndDirectoryInfoCombined to be true"); + u8 compressedBlockInfo[compressedBlockInfoSize] [[sealed]]; + } + + if (flags & Flag::BlockInfoNeedPaddingAtStart) { + $ += -$ & 0b1111; + } + + std::mem::Section blockInfo = std::mem::create_section("BlockInfo"); + match (flags & Flag::CompressionMask) { + (_): std::unimplemented(); + (Compression::None): std::mem::copy_value_to_section(compressedBlockInfo, blockInfo, 0); + (Compression::Lzma): hex::dec::lzma_decompress(compressedBlockInfo, blockInfo); + (Compression::Lz4): hex::dec::lz4_decompress(compressedBlockInfo, blockInfo, false); + (Compression::Lz4hc): hex::dec::lz4_decompress(compressedBlockInfo, blockInfo, false); + (Compression::Lzham): std::unimplemented(); + } + + cursor = $; + v2::Header header @ 0 in blockInfo; +}; + +AssetBundle assetBundle @ 0; From c3946d33a7ed98c4e55b8a552940eadb28d5db40 Mon Sep 17 00:00:00 2001 From: Dexrn ZacAttack <60078656+DexrnZacAttack@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:14:31 -0800 Subject: [PATCH 85/93] patterns/java_class: Add class versions up to j26 (#459) --- patterns/java_class.hexpat | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/patterns/java_class.hexpat b/patterns/java_class.hexpat index 47cac0dc..610f555e 100644 --- a/patterns/java_class.hexpat +++ b/patterns/java_class.hexpat @@ -450,6 +450,15 @@ enum major_version : u2 { Java_SE_15 = 59, Java_SE_16 = 60, Java_SE_17 = 61, + Java_SE_18 = 62, + Java_SE_19 = 63, + Java_SE_20 = 64, + Java_SE_21 = 65, + Java_SE_22 = 66, + Java_SE_23 = 67, + Java_SE_24 = 68, + Java_SE_25 = 69, + Java_SE_26 = 70, }; bitfield access_flags_method { From a5251602437de6a9c8ae550afb4e0ccd40aef14d Mon Sep 17 00:00:00 2001 From: Lucia Date: Fri, 5 Dec 2025 14:14:53 -0600 Subject: [PATCH 86/93] patterns: Added pattern for PopCap's Lua bytecode (#458) * patterns: Added PopCap's proprietary Lua bytecode pattern. * updated README to include new pattern * fixed README link * patterns/popcap_luc.hexpat: fixed comments and sources * patterns/popcap_luc.hexpat: Changed datatype of filename to be more clear about its structure * patterns/popcap_luc.hexpat: fixed improper handling of Nil type and added test file --- README.md | 1 + patterns/popcap_luc.hexpat | 174 ++++++++++++++++++ .../patterns/test_data/popcap_luc.hexpat.luc | Bin 0 -> 105 bytes 3 files changed, 175 insertions(+) create mode 100644 patterns/popcap_luc.hexpat create mode 100644 tests/patterns/test_data/popcap_luc.hexpat.luc diff --git a/README.md b/README.md index e7ce061c..7d97f282 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | Java Class | `application/x-java-applet` | [`patterns/java_class.hexpat`](patterns/java_class.hexpat) | Java Class files | | JPEG | `image/jpeg` | [`patterns/jpeg.hexpat`](patterns/jpeg.hexpat) | JPEG Image Format | | LOC | | [`patterns/loc.hexpat`](patterns/loc.hexpat) | Minecraft Legacy Console Edition Language file | +| LUC | | [`patterns/popcap_luc.hexpat`](patterns/popcap_luc.hexpat) | PopCap's proprietary Lua bytecode | | Lua 5.1 | | [`patterns/lua51.hexpat`](patterns/lua51.hexpat) | Lua 5.1 bytecode | | Lua 5.2 | | [`patterns/lua52.hexpat`](patterns/lua52.hexpat) | Lua 5.2 bytecode | | Lua 5.3 | | [`patterns/lua53.hexpat`](patterns/lua53.hexpat) | Lua 5.3 bytecode | diff --git a/patterns/popcap_luc.hexpat b/patterns/popcap_luc.hexpat new file mode 100644 index 00000000..dca18cf7 --- /dev/null +++ b/patterns/popcap_luc.hexpat @@ -0,0 +1,174 @@ +#pragma author gluecia +#pragma description PopCap's proprietary Lua bytecode (.luc) +#pragma endian little +#pragma magic [ 1B 4C 75 61 56 ] @ 0x00; + +import std.io; +import std.sys; + +using PLChunk; +using PLConst; +using PLLocal; +using PLOp; +using PLStr; +using cType; + + +struct PLHeader { + char magic[5]; + padding[18]; + + // *technically* this is a part of the first top level chunk, though + // it is essentially just a part of the header since it only appears there. + PLStr filename [[name("Source Name")]]; +}; + + +// LOCALS +fn fmtLocal(PLLocal l) { + return l.name; +}; + +struct PLLocal { + u32 nameLength [[hidden]]; + char16 name[nameLength] [[name("Local")]]; + u32 begin [[name("Begin")]]; + u32 end [[name("End")]]; +} [[format("fmtLocal")]]; + + +// CONSTANTS +// constant types +fn fmtCType(cType t) { + match (t) { + (cType::Nil): return "nil"; + (cType::Int): return "int"; + (cType::Float): return "float"; + (cType::Str): return "str"; + (_): return "unknown"; + } +}; + +enum cType : u8 { + Nil, + Float = 3, + Int, + Str +} [[format("fmtCType")]]; + +// lua strings +fn fmtStr(PLStr s) { + return std::format("\"{}\"", s.val); +}; + + +struct PLStr { + u32 len [[hidden]]; + if (len > 0) + char16 val[len]; +} [[format("fmtStr"), sealed]]; + +// constants struct +fn fmtConst(PLConst c) { + if (c.type == cType::Nil) { + return c.type; + } else { + return std::format("{} ({})", c.val, c.type); + } +}; + +struct PLConst { + cType type; + match (type) { + (cType::Float): double val; + (cType::Int): s32 val; + (cType::Str): PLStr val; + // theres definitely a better way to handle the Nil case + (cType::Nil): u8 val = 0; + (_): std::error("unknown cType given"); + } +} [[format("fmtConst"), sealed]]; + + +// PROTOTYPES +struct PLPrototype { + padding[4]; + PLChunk chunk; +} [[inline]]; + + +// UPVALUES +struct PLUpvalue { + u32 len [[hidden]]; + char name[len] [[name("Upvalue")]]; +} [[inline]]; + + +// OPERANDS +fn fmtPLOp(PLOp o) { + return o.opcode; +}; + +// source: https://github.com/wxarmstrong/PopLua-Disassembler/blob/master/popOp.cpp +enum PLOpType : u8 { + MOVE, LOADK, LOADBOOL, LOADNIL, GETUVPVAL, + GETGLOBAL, GETTABLE, SETGLOBAL, SETUPVAL, + SETTABLE, NEWTABLE, SELF, ADD, SUB, MUL, + DIV, MOD, POW, UNM, NOT, SIZ, CONCAT, JMP, + EQ, LT, LE, TEST, CALL, TAILCALL, RETURN, + FORLOOP, TFORLOOP, TFORPREP, SETLIST, + SETLISTO, CLOSE, ALTSELF, CONSTGLOBAL, + CONSTTABLE, DEFGLOBAL, DEFTABLE, + SETSELFORGLOBAL, GETSELFORGLOBAL, + SELFORGLOBAL, CALLSELFORGLOBAL, + TAILCALLSELFORGLOBAL, INT, BREAK, + CLOSURE +}; + +// this is for a different version of lua and its bytecode, but helped a LOT +// https://archive.org/details/a-no-frills-intro-to-lua-5.1-vm-instructions +struct PLOp { + u32 raw [[hidden]]; + PLOpType opcode = raw & 0x3F [[name("Opcode"), hidden, export]]; + u32 rawOperands = raw >> 6; + u8 opA = rawOperands & 0xFF [[name("Operand A"), export]]; + + match(opcode) { + (PLOpType::LOADK | PLOpType::CLOSURE): u32 Bx = (rawOperands >> 8) [[name("Operand Bx"), export]]; + // 131071 is the bias for sBx to make it signed, formed from (2^18 - 1) >> 1 + // where 2^18 - 1 is the maximum value for an 18 bit number + (PLOpType::JMP | PLOpType::FORLOOP | PLOpType::TFORLOOP): s32 opSBx = (rawOperands >> 8) - 131071 [[name("Operand sBx"), comment("Signed displacement added to the PC."), export]]; + (_): { + u16 opB = (rawOperands >> 8) & 0xFF01 [[name("Operand B"), export]]; + u16 opC = (rawOperands >> 17) & 0xFF01 [[name("Operand C"), export]]; + } + } +} [[format("fmtPLOp")]]; + +// CHUNKS +struct PLChunk { + char intro[0xC] [[name("Chunk Intro"), comment("Holds information on the function, but the format is unknown.")]]; + + u32 sizecode [[name("Instructions Count")]]; + u32 linesArray[sizecode] [[name("Line Numbers"), comment("The line numbers for the given operations, this should be interpreted with the operations array.")]]; + + u32 sizelocvars [[name("Locals Count")]]; + PLLocal localsArray[sizelocvars] [[name("Locals")]]; + + u32 sizeupvalues [[name("Upvalues Count")]]; + PLUpvalue upvalsArray[sizeupvalues] [[name("Upvalues")]]; + + u32 sizek [[name("Constants Count")]]; + PLConst constsArray[sizek] [[name("Constants")]]; + + u32 sizep [[name("Prototype Count")]]; + PLPrototype protoArray[sizep] [[name("Prototypes")]]; + + u32 sizecodeVerify [[hidden]]; + std::assert(sizecode == sizecodeVerify, std::format("sizecode ({}) did not match sizecodeVerify ({})!", sizecode, sizecodeVerify)); + + PLOp opsArray[sizecode] [[name("Instructions"), comment("The raw bytes for operations, should be interpreted with the line numbers array.")]]; +}; + +PLHeader Header @ 0x0; +PLChunk Body @ $; \ No newline at end of file diff --git a/tests/patterns/test_data/popcap_luc.hexpat.luc b/tests/patterns/test_data/popcap_luc.hexpat.luc new file mode 100644 index 0000000000000000000000000000000000000000..7d95d37154e94dcc69f9673d772847ebf20877ef GIT binary patch literal 105 zcmb34DNPJxWMW}qVdLQB Date: Fri, 5 Dec 2025 17:15:50 -0300 Subject: [PATCH 87/93] patterns: Improvements to NES & IPS, add SNES, NSF, NSFe (#455) * Add credit to ne.hexpat * Add many changes to nes.hexpat * Fixing dependance on variables declared in if statement * Added mappers and inline to NES 2.0 header, removed needless parenthesises * Add files via upload * Add files via upload * Create nsf.hexpat * Used full name of the SNES on description * Add SNES, NSF & NSFe, new description for NES * Removing erroneous condition in ips.hexpat's truncatedSize * Removing unnecessary std.string import in ips.hexpat * Added both locations for sections in PE, clearer variable names, reorganized DOS stub * Delete patterns/nsfe.hexpat * Delete patterns/nsfmetadata.hexpat * Added chunks from NSFe to NSF * Added NSFe * Fix size of truncatedSize in ips.hexpat --------- Co-authored-by: Nik --- README.md | 5 +- patterns/ips.hexpat | 27 +- patterns/nes.hexpat | 147 +++- patterns/nsf.hexpat | 109 +++ patterns/nsfe.hexpat | 48 ++ patterns/pe.hexpat | 1857 +++++++++++++++++++++--------------------- patterns/snes.hexpat | 110 +++ 7 files changed, 1359 insertions(+), 944 deletions(-) create mode 100644 patterns/nsf.hexpat create mode 100644 patterns/nsfe.hexpat create mode 100644 patterns/snes.hexpat diff --git a/README.md b/README.md index 7d97f282..d7e15c39 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,9 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | NBT | | [`patterns/nbt.hexpat`](patterns/nbt.hexpat) | Minecraft NBT format | | NDS | `application/x-nintendo-ds-rom` | [`patterns/nds.hexpat`](patterns/nds.hexpat) | DS Cartridge Header | | NE | `application/x-ms-ne-executable` | [`patterns/ne.hexpat`](patterns/ne.hexpat) | NE header and Standard NE fields | -| nes | | [`patterns/nes.hexpat`](patterns/nes.hexpat) | .nes file format | +| nes | | [`patterns/nes.hexpat`](patterns/nes.hexpat) | Nintendo Entertainment System ROM | +| NSF | | [`patterns/nsf.hexpat`](patterns/nsf.hexpat) | NES Sound Format | +| NSFe | | [`patterns/nsfe.hexpat`](patterns/nsfe.hexpat) | NES Sound Format extended | | NotepadCache | | [`patterns/notepad-cache.hexpat`](patterns/notepad-cache.hexpat) | Windows Notepad Cache | | NotepadStateFile | | [`patterns/notepad-state.hexpat`](patterns/notepad-state.hexpat) | Windows Notepad .bin State files | | NotepadWindowState | | [`patterns/notepadwindowstate.hexpat`](patterns/notepadwindowstate.hexpat) | Windows 11 Notepad - Window State .bin file | @@ -169,6 +171,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | SHR | | [`patterns/SHR.hexpat`](patterns/SHR.hexpat) | Apple IIgs Super Hi-Res (SHR) + PaintWorks Animation (ANI) | | shx | | [`patterns/shx.hexpat`](patterns/shx.hexpat) | ESRI index file | | smk | | [`patterns/smk.hexpat`](patterns/smk.hexpat) | Smacker video file | +| SNES | | [`patterns/snes.hexpat`](patterns/snes.hexpat) | Super Nintendo Entertainment System ROM header | | sup | | [`patterns/sup.hexpat`](patterns/sup.hexpat) | PGS Subtitle | | SPIRV | | [`patterns/spirv.hexpat`](patterns/spirv.hexpat) | SPIR-V header and instructions | | STDF | | [`patterns/stdfv4.hexpat`](patterns/stdfv4.hexpat) | Standard test data format for IC testers | diff --git a/patterns/ips.hexpat b/patterns/ips.hexpat index bccee99d..91121ae4 100644 --- a/patterns/ips.hexpat +++ b/patterns/ips.hexpat @@ -4,26 +4,25 @@ #pragma endian big -import std.mem; import std.string; struct Hunk { - u24 offset; - u16 length; - if (!length) { - u16 runCount; - u8 payload; - } - else { - u8 payload[length]; - } + u24 offset; + u16 length; + if (!length) { + u16 runCount; + u8 payload; + } + else { + u8 payload[length]; + } }; struct IPS { - char signature[5]; - Hunk hunks[while($ < std::mem::size() - (3 + 3 * (std::mem::read_string(std::mem::size()-3, 3) != "EOF")))]; - char eof[3]; - u24 truncatedSize[3+(std::mem::read_string(std::mem::size()-3, 3) != "EOF")>3]; + char signature[5]; + Hunk hunks[while($ < std::mem::size() - (3 + 3 * (std::mem::read_string(std::mem::size()-3, 3) != "EOF")))]; + char eof[3]; + u24 truncatedSize[std::mem::read_string(std::mem::size()-3, 3) != "EOF"]; }; IPS ips @ 0x00; diff --git a/patterns/nes.hexpat b/patterns/nes.hexpat index 6292ada0..797d8ec2 100644 --- a/patterns/nes.hexpat +++ b/patterns/nes.hexpat @@ -145,7 +145,7 @@ struct NES2Attributes { } MiscellaneousROMs miscellaneousROMs; DefaultExpansionDevice defaultExpansionDevice; -}; +} [[inline]]; fn renderEOF(str string) { return "\"NES\""; @@ -243,7 +243,7 @@ struct OfficialHeader { u8 complementaryChecksum [[hex::spec_name("Checksum for characterChecksum~makerID")]]; }; -u24 calculatedPRGROMSize = 16384 * ((0x0100 * $[9] & 0x0F) * ($[7] & 12 == 8) + header.prgROMSizeBy16KiBs); +u24 calculatedPRGROMSize = 16384 * ((0x0100 * ($[9] & 0x0F)) * ($[7] & 12 == 8) + header.prgROMSizeBy16KiBs); fn hasOfficialHeader() { u8 sum; @@ -263,4 +263,145 @@ struct PRGROM { }; PRGROM prgROM @ 0x10 + sizeof(trainer); -u8 chrROM[8192 * ((0x0100 * $[9] >> 4) * ($[7] & 12 == 8) + header.chrROMSizeBy8KiBs)] @ addressof(prgROM) + 16384 * header.prgROMSizeBy16KiBs; +u8 chrROM[8192 * ((0x0100 * ($[9] >> 4)) * ($[7] & 12 == 8) + header.chrROMSizeBy8KiBs)] @ addressof(prgROM) + 16384 * header.prgROMSizeBy16KiBs; + +fn identifyMapper(u16 mapperValue, u8 submapperValue) { + str mapper; + str submapper; + str designation; + match (mapperValue) { + (0): mapper = "NROM"; + (1): mapper = "MMC1B"; + (2): mapper = "UxROM"; + (3): mapper = "CNROM-32"; + (4): mapper = "MMC3"; + (5): mapper = "MMC5"; + (6): mapper = "Front Fareast Magic Card 1/2M RAM Cartridge"; + (7): mapper = "AxROM"; + (8): mapper = "Front Fareast Magic Card 1/2M RAM Cartridge (Initial latch-based banking mode 4)"; + (9): mapper = "MMC2"; + (10): mapper = "MMC4"; + (11): mapper = "Color Dreams"; + (12): mapper = "(See submapper)"; + (13): mapper = "CPROM"; + (14): mapper = "SL-1632"; + (15): mapper = "K-102xx"; + (16): mapper = "Bandai FCG boards"; + (17): mapper = "Front Fareast Super Magic Card RAM cartridge"; + (18): mapper = "SS88006 (Jaleco)"; + (19): mapper = "Namco 129/163"; + (20): mapper = "Famicom Disk System"; + (21): mapper = "VRC4a/VRC4c"; + (22): mapper = "VRC2a"; + (23): mapper = "VRC4e or VRC2b + VRC4f"; + (24): mapper = "VRC6a"; + (25): mapper = "VRC4d or VRC2c + VRC4b"; + (26): mapper = "VRC6b"; + (27): mapper = "World Hero"; + (28): mapper = "Action 53"; + (29): mapper = "RET-CUFROM"; + (30): mapper = "UNROM 512"; + (31): mapper = "NSF"; + (32): mapper = "G-101"; + (33): mapper = "TC0190"; + (34): mapper = "(See submapper)"; + (35): mapper = "J.Y. Company (8KiB WRAM)"; + (36): mapper = "01-22000-400"; + (37): mapper = "SMB+Tetris+NWC"; + (38): mapper = "Crime Busters"; + (39): mapper = "Study & Game 32-in-1"; + (40): mapper = "NTDEC 27xx"; + (41): mapper = "Caltron 6-in-1"; + (42): mapper = "FDS -> NES Hacks"; + (43): mapper = "TONY-I/YS-612"; + (44): mapper = "Super Big 7-in-1"; + (45): mapper = "GA23C"; + (46): mapper = "Rumble Station"; + (47): mapper = "Super Spike V'Ball + NWC"; + (48): mapper = "TC0690"; + (49): mapper = "Super HIK 4-in-1"; + (50): mapper = "761214"; + (51): mapper = "11-in-1 Ball Games"; + (52): mapper = "Realtec 8213"; + (53): mapper = "Supervision 16-in-1"; + (54): mapper = "Novel Diamond 9999999-in-1"; + (55): mapper = "QFJ"; + (56): mapper = "KS202"; + (57): mapper = "GK"; + (58): mapper = "WQ"; + (59): mapper = "T3H53"; + (60): mapper = "Reset-based NROM-128 4-in-1"; + (61): mapper = "(See submapper)"; + (62): mapper = "Super 700-in-1"; + (63): mapper = "(See submapper)"; + (64): mapper = "RAMBO-1"; + (65): mapper = "H3001"; + (66): mapper = "GxROM"; + (67): mapper = "Sunsoft-3"; + (68): mapper = "Sunsoft-4"; + (69): mapper = "Sunsoft FME-7/Sunsoft 5A/Sunsoft 5B"; + (70): mapper = "Family Trainer Mat"; + (71): mapper = "Camerica"; + (72): mapper = "JF-17"; + (73): mapper = "VRC3"; + (74): mapper = "860908C"; + (75): mapper = "VRC1"; + (76): mapper = "NAMCOT-3446"; + (77): mapper = "Napoleon Senki"; + (78): mapper = "74HC161/32"; + (79): mapper = "NINA-003/NINA-006"; + (80): mapper = "X1-005"; + (81): mapper = "N715021"; + (82): mapper = "X1-017"; + } + if (mapperValue == 3) { + match (submapperValue) { + (0): submapper = "Bus conflict"; + (1): submapper = "No bus conflicts"; + (2): submapper = "AND-type bus conflicts"; + } + } + else if (mapperValue == 4) designation = "TxROM"; + else if (mapperValue == 12) { + match (submapperValue) { + (0): submapper = "SL-5020B (Gouder)"; + (1): submapper = "Front Fareast Magic Card 4M RAM Cartridge"; + } + } + else if (mapperValue == 16) { + match (submapperValue) { + (0): submapper = "FCG-1/2 or LZ93D50"; + (4): submapper = "FCG-1/2"; + (5): submapper = "LZ93D50"; + } + } + else if (mapperValue == 34) { + match (submapperValue) { + (0): submapper = "NINA-001/NINA-002"; + (1): submapper = "BNROM"; + } + } + else if (mapperValue == 40) { + match (submapperValue) { + (0): submapper = "NTDEC 2722"; + (1): submapper = "NTDEC 2752"; + } + } + else if (mapperValue == 61) { + match (submapperValue) { + (0): submapper = "NTDEC 0324"; + (1): submapper = "NTDEC BS-N032"; + (_): submapper = "GS-2017"; + } + } + else if (mapperValue == 63) { + match (submapperValue) { + (0): submapper = "TH2291-3"; + (1): submapper = "82AB"; + } + } + std::print("Mapper: " + mapper + "(" + std::string::to_string(mapperValue) + ")"); + if (submapper) std::print("Submapper: " + submapper + "(" + std::string::to_string(submapperValue) + ")"); + if (designation) std::print("Designation: " + designation); +}; +identifyMapper(0x0100 * ($[8] & 0x0F) + 0x10 * ($[7] & 0x0F) + header.flags.lowerMapperNybble, $[8] >> 4); diff --git a/patterns/nsf.hexpat b/patterns/nsf.hexpat new file mode 100644 index 00000000..b80465e6 --- /dev/null +++ b/patterns/nsf.hexpat @@ -0,0 +1,109 @@ +#pragma author gmestanley +#pragma description NES Sound Format file + +import std.string; + +struct ChunkMetadata { + u32 length; + char ID[4]; +} [[inline]]; + +struct TimeChunkData { + u32 trackLengths[while($\""; +}; + +struct Header { + char signature[5] [[format("renderEOF")]]; + u8 version; + u8 songAmount; + u8 startingSong; + u16 dataLoadAddress; + u16 dataInitAddress; + u16 dataPlayAddress; + char gameName[32] [[format("formatName")]]; + char songwriting[32] [[format("formatName")]]; + char copyrightHolder[32] [[format("formatName")]]; + u16 ntscPlaySpeed; + u8 bankswitchInitValues[8]; + u16 palPlaySpeed; + Region region; + ExtraSoundChipSupport extraSoundChipSupport; + NSF2Flags nsf2flags; + u24 dataLength; +}; + +Header header @ 0x00; + +struct Chunks { + if (header.dataLength) + NSFEChunk chunks[while($ 0) { - AlignmentType enumValue = value; - return enumValue; - } - return value; + if (value) { + AlignmentType enumValue = value; + return enumValue; + } + return value; }; bitfield SectionFlags { - padding : 3; - doNotPad : 1 [[hex::spec_name("IMAGE_SCN_TYPE_NO_PAD")]]; - padding : 1; - containsCode : 1 [[hex::spec_name("IMAGE_SCN_CNT_CODE")]]; - containsInitializedData : 1 [[hex::spec_name("IMAGE_SCN_CNT_INITIALIZED_DATA")]]; - containsUninitializedData : 1 [[hex::spec_name("IMAGE_SCN_CNT_UNINITIALIZED_DATA")]]; - linkOther : 1 [[hex::spec_name("IMAGE_SCN_LNK_OTHER")]]; - linkHasInformation : 1 [[hex::spec_name("IMAGE_SCN_LNK_INFO")]]; - padding : 1; - linkRemove : 1 [[hex::spec_name("IMAGE_SCN_LNK_REMOVE")]]; - linkHasCOMDAT : 1 [[hex::spec_name("IMAGE_SCN_LNK_COMDAT")]]; - padding : 1; - resetSpeculativeExceptions : 1 [[hex::spec_name("")]]; - globalPointerRelocations : 1 [[hex::spec_name("IMAGE_SCN_GPREL")]]; - purgeable : 1 [[hex::spec_name("IMAGE_SCN_MEM_PURGEABLE")]]; - is16Bit : 1 [[hex::spec_name("IMAGE_SCN_MEM_16BIT")]]; - locked : 1 [[hex::spec_name("IMAGE_SCN_MEM_LOCKED")]]; - preloaded : 1 [[hex::spec_name("IMAGE_SCN_MEM_PRELOAD")]]; - dataAlignment : 4 [[format("formatAlignmentBits")]]; - linkExtendedRelocations : 1 [[hex::spec_name("IMAGE_SCN_LNK_NRELOC_OVFL")]]; - discardable : 1 [[hex::spec_name("IMAGE_SCN_MEM_DISCARDABLE")]]; - notCached : 1 [[hex::spec_name("IMAGE_SCN_MEM_NOT_CACHED")]]; - notPageable : 1 [[hex::spec_name("IMAGE_SCN_MEM_NOT_PAGED")]]; - shared : 1 [[hex::spec_name("IMAGE_SCN_MEM_SHARED")]]; - executed : 1 [[hex::spec_name("IMAGE_SCN_MEM_EXECUTE")]]; - read : 1 [[hex::spec_name("IMAGE_SCN_MEM_READ")]]; - writtenOn : 1 [[hex::spec_name("IMAGE_SCN_MEM_WRITE")]]; + padding : 3; + doNotPad : 1 [[hex::spec_name("IMAGE_SCN_TYPE_NO_PAD")]]; + padding : 1; + containsCode : 1 [[hex::spec_name("IMAGE_SCN_CNT_CODE")]]; + containsInitializedData : 1 [[hex::spec_name("IMAGE_SCN_CNT_INITIALIZED_DATA")]]; + containsUninitializedData : 1 [[hex::spec_name("IMAGE_SCN_CNT_UNINITIALIZED_DATA")]]; + linkOther : 1 [[hex::spec_name("IMAGE_SCN_LNK_OTHER")]]; + linkHasInformation : 1 [[hex::spec_name("IMAGE_SCN_LNK_INFO")]]; + padding : 1; + linkRemove : 1 [[hex::spec_name("IMAGE_SCN_LNK_REMOVE")]]; + linkHasCOMDAT : 1 [[hex::spec_name("IMAGE_SCN_LNK_COMDAT")]]; + padding : 1; + resetSpeculativeExceptions : 1 [[hex::spec_name("")]]; + globalPointerRelocations : 1 [[hex::spec_name("IMAGE_SCN_GPREL")]]; + purgeable : 1 [[hex::spec_name("IMAGE_SCN_MEM_PURGEABLE")]]; + is16Bit : 1 [[hex::spec_name("IMAGE_SCN_MEM_16BIT")]]; + locked : 1 [[hex::spec_name("IMAGE_SCN_MEM_LOCKED")]]; + preloaded : 1 [[hex::spec_name("IMAGE_SCN_MEM_PRELOAD")]]; + dataAlignment : 4 [[format("formatAlignmentBits")]]; + linkExtendedRelocations : 1 [[hex::spec_name("IMAGE_SCN_LNK_NRELOC_OVFL")]]; + discardable : 1 [[hex::spec_name("IMAGE_SCN_MEM_DISCARDABLE")]]; + notCached : 1 [[hex::spec_name("IMAGE_SCN_MEM_NOT_CACHED")]]; + notPageable : 1 [[hex::spec_name("IMAGE_SCN_MEM_NOT_PAGED")]]; + shared : 1 [[hex::spec_name("IMAGE_SCN_MEM_SHARED")]]; + executed : 1 [[hex::spec_name("IMAGE_SCN_MEM_EXECUTE")]]; + read : 1 [[hex::spec_name("IMAGE_SCN_MEM_READ")]]; + writtenOn : 1 [[hex::spec_name("IMAGE_SCN_MEM_WRITE")]]; }; fn formatSectionName(str string) { - for (u8 i = 0, i < 8, i += 1) { - if (std::mem::read_unsigned($+i, 1) == 0) { - return "\"" + std::string::substr(string, 0, i) + "\""; - } - } + for (u8 i = 0, i < 8, i += 1) { + if (std::mem::read_unsigned($+i, 1) == 0) { + return "\"" + std::string::substr(string, 0, i) + "\""; + } + } }; struct SectionHeader { - char name[8] [[name("sectionName"), hex::spec_name("name"), format("formatSectionName")]]; - u32 virtualSize; - u32 rva [[hex::spec_name("virtualAddress")]]; - u32 sizeOfRawData; - u32 ptrRawData; - u32 ptrRelocations; - u32 ptrLineNumbers; - u16 numberOfRelocations; - u16 numberOfLineNumbers; - SectionFlags characteristics; + char name[8] [[name("sectionName"), hex::spec_name("name"), format("formatSectionName")]]; + u32 virtualSize; + u32 rva [[hex::spec_name("virtualAddress")]]; + u32 sizeOfRawData; + u32 ptrRawData; + u32 ptrRelocations; + u32 ptrLineNumbers; + u16 numberOfRelocations; + u16 numberOfLineNumbers; + SectionFlags characteristics; }; SectionHeader sectionsTable[coffHeader.numberOfSections] @ addressof(coffHeader.optionalHeader) + coffHeader.sizeOfOptionalHeader; @@ -318,886 +322,887 @@ SectionHeader sectionsTable[coffHeader.numberOfSections] @ addressof(coffHeader. u16 currentSectionIndex; fn relativeVirtualDifference() { - return sectionsTable[currentSectionIndex].rva - sectionsTable[currentSectionIndex].ptrRawData; + return sectionsTable[currentSectionIndex].rva - sectionsTable[currentSectionIndex].ptrRawData; }; fn wordsize() { - return std::mem::read_unsigned(addressof(coffHeader.optionalHeader.magic), 2) / 0x41; -}; - -fn formatNullTerminatedString(str string) { - return "\"" + std::string::substr(string, 0, std::string::length(string)-1) + "\""; + return std::mem::read_unsigned(addressof(coffHeader.optionalHeader.magic), 2) / 0x41; }; // Exception Table bitfield FunctionBitfield { - prologLength : 8; - functionLength : 22; - instructions32Bit : 1; - exceptionHandler : 1; + prologLength : 8; + functionLength : 22; + instructions32Bit : 1; + exceptionHandler : 1; }; struct FunctionTableEntry { - if (coffHeader.architecture == ArchitectureType::MIPSFPU || coffHeader.architecture == ArchitectureType::R3000) { - u32 beginVA; - u32 endVA; - u32 exceptionHandlerPointer; - u32 handlerDataPointer; - u32 prologEndVA; - } else if (coffHeader.architecture == ArchitectureType::ARM || coffHeader.architecture == ArchitectureType::ARM64 || coffHeader.architecture == ArchitectureType::ARMNT || - coffHeader.architecture == ArchitectureType::POWERPC || coffHeader.architecture == ArchitectureType::POWERPCFP || - coffHeader.architecture == ArchitectureType::SH3 || coffHeader.architecture == ArchitectureType::SH3DSP || coffHeader.architecture == ArchitectureType::SH4) { - u32 beginVA; - FunctionBitfield miscellaneousBits; - } else if (coffHeader.architecture == ArchitectureType::AMD64 || coffHeader.architecture == ArchitectureType::IA64) { - u32 beginRVA; - u32 endRVA; - u32 unwindInformationRVA; - } + if (coffHeader.architecture == ArchitectureType::MIPSFPU || coffHeader.architecture == ArchitectureType::R3000) { + u32 beginVA; + u32 endVA; + u32 exceptionHandlerPointer; + u32 handlerDataPointer; + u32 prologEndVA; + } else if (coffHeader.architecture == ArchitectureType::ARM || coffHeader.architecture == ArchitectureType::ARM64 || coffHeader.architecture == ArchitectureType::ARMNT || + coffHeader.architecture == ArchitectureType::POWERPC || coffHeader.architecture == ArchitectureType::POWERPCFP || + coffHeader.architecture == ArchitectureType::SH3 || coffHeader.architecture == ArchitectureType::SH3DSP || coffHeader.architecture == ArchitectureType::SH4) { + u32 beginVA; + FunctionBitfield miscellaneousBits; + } else if (coffHeader.architecture == ArchitectureType::AMD64 || coffHeader.architecture == ArchitectureType::IA64) { + u32 beginRVA; + u32 endRVA; + u32 unwindInformationRVA; + } }; struct ExceptionTable { - FunctionTableEntry functionTableEntries[while($ < (coffHeader.optionalHeader.directories[3].rva - relativeVirtualDifference()) + coffHeader.optionalHeader.directories[3].size)] [[inline]]; + FunctionTableEntry functionTableEntries[while($ < (coffHeader.optionalHeader.directories[3].rva - relativeVirtualDifference()) + coffHeader.optionalHeader.directories[3].size)] [[inline]]; }; // Exports Table struct ExportDirectoryTable { - u32 flags [[name("exportFlags")]]; - type::time32_t timeDateStamp [[name("exportTimeDateStamp")]]; - u16 majorVersion; - u16 minorVersion; - u32 imageNameRVA; - u32 ordinalBase [[name("exportOrdinalBase")]]; - u32 addressesAmount [[name("exportAddressesAmount")]]; - u32 namePointersAmount [[name("exportNamePointersAmount")]]; - u32 addressTableRVA [[name("exportAddressTableRVA")]]; - u32 namePointerTableRVA [[name("exportNamePointerTableRVA")]]; - u32 ordinalTableRVA [[name("exportOrdinalTableRVA")]]; + u32 flags [[name("exportFlags")]]; + type::time32_t timeDateStamp [[name("exportTimeDateStamp")]]; + u16 majorVersion; + u16 minorVersion; + u32 imageNameRVA; + u32 ordinalBase [[name("exportOrdinalBase")]]; + u32 addressesAmount [[name("exportAddressesAmount")]]; + u32 namePointersAmount [[name("exportNamePointersAmount")]]; + u32 addressTableRVA [[name("exportAddressTableRVA")]]; + u32 namePointerTableRVA [[name("exportNamePointerTableRVA")]]; + u32 ordinalTableRVA [[name("exportOrdinalTableRVA")]]; }; struct ExportAddress { - if (sectionsTable[currentSectionIndex].ptrRawData > std::mem::read_unsigned($, 4) > sectionsTable[currentSectionIndex].sizeOfRawData) { - u32 exportRVA; - } - else { - u32 forwarderRVA; - } + if (sectionsTable[currentSectionIndex].ptrRawData > std::mem::read_unsigned($, 4) > sectionsTable[currentSectionIndex].sizeOfRawData) { + u32 exportRVA; + } + else { + u32 forwarderRVA; + } }; struct ExportNamePointer { - u32 exportNameRVA; - char exportName[] @ exportNameRVA - relativeVirtualDifference() [[format("formatNullTerminatedString")]]; + u32 exportNameRVA; + char exportName[] @ exportNameRVA - relativeVirtualDifference() [[format("formatNullTerminatedString")]]; }; struct ExportsTable { - ExportDirectoryTable directoryTable; - ExportAddress exportAddressTable[directoryTable.addressesAmount] @ directoryTable.addressTableRVA - relativeVirtualDifference(); - ExportNamePointer exportNamePointerTable[directoryTable.namePointersAmount] @ directoryTable.namePointerTableRVA - relativeVirtualDifference(); - if (directoryTable.ordinalTableRVA > relativeVirtualDifference()) { - u16 exportOrdinalTable[directoryTable.namePointersAmount] @ directoryTable.ordinalTableRVA - relativeVirtualDifference(); - } - if (directoryTable.imageNameRVA > relativeVirtualDifference()) { - char imageName[] @ directoryTable.imageNameRVA - relativeVirtualDifference() [[format("formatNullTerminatedString")]]; - } - $ = addressof(this)+coffHeader.optionalHeader.directories[0].size; + ExportDirectoryTable directoryTable; + ExportAddress exportAddressTable[directoryTable.addressesAmount] @ directoryTable.addressTableRVA - relativeVirtualDifference(); + ExportNamePointer exportNamePointerTable[directoryTable.namePointersAmount] @ directoryTable.namePointerTableRVA - relativeVirtualDifference(); + if (directoryTable.ordinalTableRVA > relativeVirtualDifference()) { + u16 exportOrdinalTable[directoryTable.namePointersAmount] @ directoryTable.ordinalTableRVA - relativeVirtualDifference(); + } + if (directoryTable.imageNameRVA > relativeVirtualDifference()) { + char imageName[] @ directoryTable.imageNameRVA - relativeVirtualDifference() [[format("formatNullTerminatedString")]]; + } + $ = addressof(this)+coffHeader.optionalHeader.directories[0].size; }; // Imports Table bitfield OrdinalFlagByte { - padding : 7; - flag : 1 [[name("ordinalFlag")]]; + padding : 7; + flag : 1 [[name("ordinalFlag")]]; +}; + +fn formatMangledString(str string) { + return hex::dec::demangle(formatNullTerminatedString(string)); }; struct ImportsName { - u16 hint; - char name[] [[format("formatNullTerminatedString")]]; - if ($ % 2 == 1) { u8 pad; } + u16 hint; + if (std::mem::read_string($, 1) == "?") + char name[] [[format("formatMangledString")]]; + else + char name[] [[format("formatNullTerminatedString")]]; + if ($ % 2 == 1) { u8 pad; } }; struct ImportsAddress { - OrdinalFlagByte ordinalFlagByte @ $+(wordsize()-1); - if (ordinalFlagByte.flag) { - u16 ordinalNumber; - padding[wordsize()-2]; - } else { - u32 nameTableRVA; - if (coffHeader.optionalHeader.magic == PEFormat::PE32Plus) { - padding[4]; - } - } + OrdinalFlagByte ordinalFlagByte @ $+(wordsize()-1); + if (ordinalFlagByte.flag) { + u16 ordinalNumber; + padding[wordsize()-2]; + } else { + u32 nameTableRVA; + if (coffHeader.optionalHeader.magic == PEFormat::PE32Plus) { + padding[4]; + } + } }; struct ImportsLookup : ImportsAddress { - if (!ordinalFlagByte.flag && std::mem::read_unsigned($-wordsize(), wordsize()) > 0) { - ImportsName name @ nameTableRVA - relativeVirtualDifference(); - } + if (!ordinalFlagByte.flag && std::mem::read_unsigned($-wordsize(), wordsize()) > 0) { + ImportsName name @ nameTableRVA - relativeVirtualDifference(); + } }; struct ImportsDirectory { - u32 lookupTableRVA; - u32 timeDateStamp; - u32 forwarderChain; - u32 dllNameRVA; - u32 addressTableRVA; + u32 lookupTableRVA; + u32 timeDateStamp; + u32 forwarderChain; + u32 dllNameRVA; + u32 addressTableRVA; }; struct ImportsStructure { - if (parent.importsDirectoryTable[std::core::array_index()].lookupTableRVA > 0) { - ImportsLookup lookupTable[while(std::mem::read_unsigned($, wordsize()) != 0)] @ parent.importsDirectoryTable[std::core::array_index()].lookupTableRVA - relativeVirtualDifference(); - } - if (parent.importsDirectoryTable[std::core::array_index()].addressTableRVA > 0) { - ImportsAddress addressTable[while(std::mem::read_unsigned($, wordsize()) != 0)] @ parent.importsDirectoryTable[std::core::array_index()].addressTableRVA - relativeVirtualDifference(); - } - if (parent.importsDirectoryTable[std::core::array_index()].dllNameRVA > 0) { - char dllName[] @ parent.importsDirectoryTable[std::core::array_index()].dllNameRVA - relativeVirtualDifference() [[format("formatNullTerminatedString")]]; - } + if (parent.importsDirectoryTable[std::core::array_index()].lookupTableRVA > 0) { + ImportsLookup lookupTable[while(std::mem::read_unsigned($, wordsize()) != 0)] @ parent.importsDirectoryTable[std::core::array_index()].lookupTableRVA - relativeVirtualDifference(); + } + if (parent.importsDirectoryTable[std::core::array_index()].addressTableRVA > 0) { + ImportsAddress addressTable[while(std::mem::read_unsigned($, wordsize()) != 0)] @ parent.importsDirectoryTable[std::core::array_index()].addressTableRVA - relativeVirtualDifference(); + } + if (parent.importsDirectoryTable[std::core::array_index()].dllNameRVA > 0) { + char dllName[] @ parent.importsDirectoryTable[std::core::array_index()].dllNameRVA - relativeVirtualDifference() [[format("formatNullTerminatedString")]]; + } } [[inline]]; struct ImportsTable { - ImportsDirectory importsDirectoryTable[while(std::mem::read_unsigned($, 16) != 0)]; - ImportsStructure importsStructures[sizeof(importsDirectoryTable)/sizeof(importsDirectoryTable[0])] [[inline]]; - $ = addressof(this)+coffHeader.optionalHeader.directories[1].size; + ImportsDirectory importsDirectoryTable[while(std::mem::read_unsigned($, 16) != 0)]; + ImportsStructure importsStructures[sizeof(importsDirectoryTable)/sizeof(importsDirectoryTable[0])] [[inline]]; + $ = addressof(this)+coffHeader.optionalHeader.directories[1].size; }; struct DelayedImportsDirectory { - u32 attributes; - u32 name; - u32 moduleHandle; - u32 delayImportAddressTable; - u32 delayImportNameTable; - u32 boundDelayImportTable; - u32 unloadDelayImportTable; - u32 timeStamp; + u32 attributes; + u32 name; + u32 moduleHandle; + u32 delayImportAddressTable; + u32 delayImportNameTable; + u32 boundDelayImportTable; + u32 unloadDelayImportTable; + u32 timeStamp; }; struct DelayImportsStructure { - if (parent.delayImportsDirectoryTable[std::core::array_index()].delayImportNameTable > 0) { - ImportsLookup delayedLookupTable[while(std::mem::read_unsigned($, wordsize()) != 0)] @ parent.delayImportsDirectoryTable[std::core::array_index()].delayImportNameTable - relativeVirtualDifference(); - } - if (parent.delayImportsDirectoryTable[std::core::array_index()].name > 0) { - char dllName[] @ parent.delayImportsDirectoryTable[std::core::array_index()].name - relativeVirtualDifference() [[format("formatNullTerminatedString")]]; - } + if (parent.delayImportsDirectoryTable[std::core::array_index()].delayImportNameTable > 0) { + ImportsLookup delayedLookupTable[while(std::mem::read_unsigned($, wordsize()) != 0)] @ parent.delayImportsDirectoryTable[std::core::array_index()].delayImportNameTable - relativeVirtualDifference(); + } + if (parent.delayImportsDirectoryTable[std::core::array_index()].name > 0) { + char dllName[] @ parent.delayImportsDirectoryTable[std::core::array_index()].name - relativeVirtualDifference() [[format("formatNullTerminatedString")]]; + } } [[inline]]; // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#delay-load-import-tables-image-only struct DelayedImportsTable { - DelayedImportsDirectory delayImportsDirectoryTable[while(std::mem::read_unsigned($, 16) != 0)]; - DelayImportsStructure delayImportsStructures[sizeof(delayImportsDirectoryTable)/sizeof(delayImportsDirectoryTable[0])] [[inline]]; - $ = addressof(this)+coffHeader.optionalHeader.directories[1].size; + DelayedImportsDirectory delayImportsDirectoryTable[while(std::mem::read_unsigned($, 16) != 0)]; + DelayImportsStructure delayImportsStructures[sizeof(delayImportsDirectoryTable)/sizeof(delayImportsDirectoryTable[0])] [[inline]]; + $ = addressof(this)+coffHeader.optionalHeader.directories[1].size; }; // General Resource Table things fn formatNullTerminatedString16(str string) { - return "\"" + std::string::substr(string, 0, std::string::length(string)) + "\""; + return "\"" + std::string::substr(string, 0, std::string::length(string)) + "\""; }; // * Bitmap Resource struct BitmapHeader { - u32 size [[name("bitmapSize")]]; - u32 width [[name("bitmapWidth")]]; - u32 height [[name("bitmapHeight")]]; - u16 planes [[name("bitmapPlanes")]]; - u16 bitCount [[name("bitmapBitCount")]]; - u32 compression [[name("bitmapCompression")]]; - u32 imageSize [[name("bitmapImageSize")]]; - u32 xPelsPerMeter [[name("bitmapXPelsPerMeter")]]; - u32 yPelsPerMeter [[name("bitmapYPelsPerMeter")]]; - u32 clrUsed [[name("bitmapClrUsed")]]; - u32 clrImportant [[name("bitmapClrImportant")]]; + u32 size [[name("bitmapSize")]]; + u32 width [[name("bitmapWidth")]]; + u32 height [[name("bitmapHeight")]]; + u16 planes [[name("bitmapPlanes")]]; + u16 bitCount [[name("bitmapBitCount")]]; + u32 compression [[name("bitmapCompression")]]; + u32 imageSize [[name("bitmapImageSize")]]; + u32 xPelsPerMeter [[name("bitmapXPelsPerMeter")]]; + u32 yPelsPerMeter [[name("bitmapYPelsPerMeter")]]; + u32 clrUsed [[name("bitmapClrUsed")]]; + u32 clrImportant [[name("bitmapClrImportant")]]; }; struct Colors { - u8 blue [[color("1F77B4")]]; - u8 green [[color("2CA02C")]]; - u8 red [[color("D62728")]]; - u8 reserved [[color("828282")]]; + u8 blue [[color("1F77B4")]]; + u8 green [[color("2CA02C")]]; + u8 red [[color("D62728")]]; + u8 reserved [[color("828282")]]; }; u32 imageDataSize; struct Bitmap { - BitmapHeader bmh [[name("bitmapHeader")]]; + BitmapHeader bmh [[name("bitmapHeader")]]; - if ((bmh.bitCount != 24) && (bmh.bitCount != 32)) { - Colors rgbq[bmh.clrUsed*((1 << bmh.bitCount)*!bmh.clrUsed)]; - imageDataSize = imageDataSize - sizeof(rgbq); - } + if ((bmh.bitCount != 24) && (bmh.bitCount != 32)) { + Colors rgbq[bmh.clrUsed*((1 << bmh.bitCount)*!bmh.clrUsed)]; + imageDataSize = imageDataSize - sizeof(rgbq); + } - u8 imageData[imageDataSize-sizeof(bmh)]; + u8 imageData[imageDataSize-sizeof(bmh)]; }; // * Cursor Resource struct Cursor { - u16 reserved; - u16 type; - if (!type) { - imageDataSize = parent.size-4; - Bitmap bitmapData [[inline]]; - } + u16 reserved; + u16 type; + if (!type) { + imageDataSize = parent.size-4; + Bitmap bitmapData [[inline]]; + } }; // * Dialog Resource enum AtomType : u16 { - Button = 0x80, - Edit, - Static, - ListBox, - ScrollBar, - ComboBox + Button = 0x80, + Edit, + Static, + ListBox, + ScrollBar, + ComboBox }; struct VLSElement { - if (std::mem::read_unsigned($, 2) == 0xFFFF) { - u16 atomDefiner; - } - else { - AtomType atom; - } + if (std::mem::read_unsigned($, 2) == 0xFFFF) { + u16 atomDefiner; + } + else { + AtomType atom; + } }; struct VariableLengthStructure { - if (std::mem::read_unsigned($, 2) == 0xFFFF) { - VLSElement vlsElement[2]; - } - else { - char16 string[] [[format("formatNullTerminatedString16")]]; - } + if (std::mem::read_unsigned($, 2) == 0xFFFF) { + VLSElement vlsElement[2]; + } + else { + char16 string[] [[format("formatNullTerminatedString16")]]; + } }; struct DialogTemplate { - if (parent.parent.parent.parent.id == 0x411) { - u16 version; - u16 signature; - u32 helpContextIdentifier; - u32 extendedStyles; - u32 style; - } - else { - u32 style; - u32 extendedStyles; - } - u16 numberOfItems; - u16 x; - u16 y; - u16 width; - u16 height; - if (parent.parent.parent.parent.id == 0x409 || parent.parent.parent.parent.id == 0x411) { - VariableLengthStructure dialogMenu; - VariableLengthStructure dialogClass; - char16 dialogTitle[] [[format("formatNullTerminatedString16")]]; - u16 dialogFontSize; - if (parent.parent.parent.parent.id == 0x411) { - u16 weight; - bool italic; - u8 charset; - } - char16 dialogFontTypeface[] [[format("formatNullTerminatedString16")]]; - if (($+2)%16 == 0 || 4 || 8 || 12) - u16 alignment; - } + if (parent.parent.parent.parent.id == 0x411) { + u16 version; + u16 signature; + u32 helpContextIdentifier; + u32 extendedStyles; + u32 style; + } + else { + u32 style; + u32 extendedStyles; + } + u16 numberOfItems; + u16 x; + u16 y; + u16 width; + u16 height; + if (parent.parent.parent.parent.id == 0x409 || parent.parent.parent.parent.id == 0x411) { + VariableLengthStructure dialogMenu; + VariableLengthStructure dialogClass; + char16 dialogTitle[] [[format("formatNullTerminatedString16")]]; + u16 dialogFontSize; + if (parent.parent.parent.parent.id == 0x411) { + u16 weight; + bool italic; + u8 charset; + } + char16 dialogFontTypeface[] [[format("formatNullTerminatedString16")]]; + if (($+2)%16 == 0 || 4 || 8 || 12) + u16 alignment; + } }; struct DialogItemTemplate { - if (parent.parent.parent.parent.parent.id == 0x411) { - u32 helpContextIdentifier; - u32 extendedStyles [[name("itemExtendedStyles")]]; - u32 style [[name("itemStyle")]]; - } - else { - u32 style [[name("itemStyle")]]; - u32 extendedStyles [[name("itemExtendedStyles")]]; - } - u16 x; - u16 y; - u16 width; - u16 height; - if (parent.parent.parent.parent.parent.id == 0x409 || parent.parent.parent.parent.parent.id == 0x411) { - u32 controlID; - VariableLengthStructure windowClass; - VariableLengthStructure dialogItemTitle; - u16 extraCount; - } else { - u16 controlID; - } - if (($+2)%16 == 0 || 4 || 8) - u16 alignment; + if (parent.parent.parent.parent.parent.id == 0x411) { + u32 helpContextIdentifier; + u32 extendedStyles [[name("itemExtendedStyles")]]; + u32 style [[name("itemStyle")]]; + } + else { + u32 style [[name("itemStyle")]]; + u32 extendedStyles [[name("itemExtendedStyles")]]; + } + u16 x; + u16 y; + u16 width; + u16 height; + if (parent.parent.parent.parent.parent.id == 0x409 || parent.parent.parent.parent.parent.id == 0x411) { + u32 controlID; + VariableLengthStructure windowClass; + VariableLengthStructure dialogItemTitle; + u16 extraCount; + } else { + u16 controlID; + } + if (($+2)%16 == 0 || 4 || 8) + u16 alignment; }; struct DialogItem { - DialogItemTemplate template [[inline]]; - /*if (parent.parent.parent.parent.id == 0x409 || parent.parent.parent.parent.id == 0x411) { - u8 itemCreationData[template.extraCount]; - }*/ + DialogItemTemplate template [[inline]]; + /*if (parent.parent.parent.parent.id == 0x409 || parent.parent.parent.parent.id == 0x411) { + u8 itemCreationData[template.extraCount]; + }*/ }; struct Dialog { - DialogTemplate dialogTemplate; - DialogItem items[dialogTemplate.numberOfItems]; + DialogTemplate dialogTemplate; + DialogItem items[dialogTemplate.numberOfItems]; }; // * String Resource struct StringTableResource { - std::string::SizedString16 strings[while($ < (parent.dataRVA - relativeVirtualDifference()) + parent.size)]; + std::string::SizedString16 strings[while($ < (parent.dataRVA - relativeVirtualDifference()) + parent.size)]; }; // * GroupCursor Resource struct GroupCursor { - u16 assorteddata; - u16 colors; - u16 otherassorteddata; - u16 pixels; - u16 assorteddataarray[5]; - u16 ordinalName; + u16 assorteddata; + u16 colors; + u16 otherassorteddata; + u16 pixels; + u16 assorteddataarray[5]; + u16 ordinalName; }; // * GroupIcon Resource struct GroupIconHeader { - u16 reserved; - u16 type; - u16 count; + u16 reserved; + u16 type; + u16 count; }; struct GroupIconEntry { - u8 width; - u8 height; - u8 colorCount; - u8 reserved; - u16 planes; - u16 bitCount; - u32 bytesInResource; - u16 id; + u8 width; + u8 height; + u8 colorCount; + u8 reserved; + u16 planes; + u16 bitCount; + u32 bytesInResource; + u16 id; }; struct GroupIcon { - GroupIconHeader header; - GroupIconEntry entries[header.count]; + GroupIconHeader header; + GroupIconEntry entries[header.count]; }; // * Version Resource struct VersionEntryHeader { - u16 length; - u16 valueLength; - u16 type; - char16 key[] [[format("formatNullTerminatedString16")]]; - padding[while(std::mem::read_unsigned($, 1) == 0 && $ < addressof(key)+sizeof(key)+5)]; + u16 length; + u16 valueLength; + u16 type; + char16 key[] [[format("formatNullTerminatedString16")]]; + padding[while(!$[$] && $ < addressof(key)+sizeof(key)+5)]; }; struct StringInfo { - VersionEntryHeader stringInfoHeader; - if (stringInfoHeader.valueLength > 0) { - char16 string[] [[format("formatNullTerminatedString16")]]; - padding[while(std::mem::read_unsigned($, 1) == 0)]; - } + VersionEntryHeader stringInfoHeader; + if (stringInfoHeader.valueLength > 0) { + char16 string[] [[format("formatNullTerminatedString16")]]; + padding[while(!$[$])]; + } }; struct VersionEntry { - VersionEntryHeader header [[inline]]; - if (header.key == "StringFileInfo") { - VersionEntryHeader stringTableHeader; - StringInfo strings[while($ < addressof(stringTableHeader) + stringTableHeader.length)]; - } - else if (header.key == "VarFileInfo") { - VersionEntryHeader varHeader; - u16 translation[varHeader.valueLength / 2]; - } - else { - u8 value[header.valueLength]; - padding[while(std::mem::read_unsigned($, 1) == 0)]; - } + VersionEntryHeader header [[inline]]; + if (header.key == "StringFileInfo") { + VersionEntryHeader stringTableHeader; + StringInfo strings[while($ < addressof(stringTableHeader) + stringTableHeader.length)]; + } + else if (header.key == "VarFileInfo") { + VersionEntryHeader varHeader; + u16 translation[varHeader.valueLength / 2]; + } + else { + u8 value[header.valueLength]; + padding[while(!$[$])]; + } }; struct Version { - VersionEntryHeader header [[inline]]; - u32 signature; - u16 structVersion[2]; - u16 fileVersion[4]; - u16 productVersion[4]; - u32 fileFlagsMask[2]; - u32 fileFlags; - u32 fileOS; - u32 fileType; - u32 fileSubType; - u32 fileTimestamp; - VersionEntry children[while($ < (parent.dataRVA - relativeVirtualDifference()) + parent.size)]; + VersionEntryHeader header [[inline]]; + u32 signature; + u16 structVersion[2]; + u16 fileVersion[4]; + u16 productVersion[4]; + u32 fileFlagsMask[2]; + u32 fileFlags; + u32 fileOS; + u32 fileType; + u32 fileSubType; + u32 fileTimestamp; + VersionEntry children[while($ < (parent.dataRVA - relativeVirtualDifference()) + parent.size)]; }; // * Resources Using TrueChar fn displayTrueChar(u8 value) { - str notation = "0x"; - if (value < 0x10) - notation += "0"; - if (value == 0) - return "'␀' (" + std::format(notation + "{:X}", value) + ")"; - else - return "'" + char(value) + "' (" + std::format(notation + "{:X}", value) + ")"; + char chr = char(value); + str notation = "0x"; + if (value < 0x10) notation += "0"; + if (!value) chr = "␀"; + return "'" + chr + "' (" + std::format(notation + "{:X}", value) + ")"; }; +using TrueChar = u8 [[format("displayTrueChar")]]; + // Resource Table enum ResourceID : u32 { - Cursor = 0x01, - Bitmap, - Icon, - Menu, - Dialog, - String, - Accelerator = 0x09, - StringData, - GroupCursor = 0x0C, - GroupIcon = 0x0E, - Version = 0x10, - Manifest = 0x18 + Cursor = 0x01, + Bitmap, + Icon, + Menu, + Dialog, + String, + Accelerator = 0x09, + StringData, + GroupCursor = 0x0C, + GroupIcon = 0x0E, + Version = 0x10, + Manifest = 0x18 }; ResourceID resourceIDType; -using TrueChar = u8 [[format("displayTrueChar")]]; - struct DataEntry { - u32 dataRVA; - u32 size; - u32 codepage; - u32 reserved; - - if (resourceIDType == ResourceID::Cursor) { - Cursor cursor @ dataRVA - relativeVirtualDifference(); - } - else if (resourceIDType == ResourceID::Bitmap || (resourceIDType == ResourceID::Icon && std::mem::read_string((dataRVA - relativeVirtualDifference())+1, 3) != "PNG")) { - imageDataSize = size; - Bitmap bitmap @ dataRVA - relativeVirtualDifference(); - } - else if (resourceIDType == ResourceID::Dialog) { - Dialog dialog @ dataRVA - relativeVirtualDifference(); - } - else if (resourceIDType == ResourceID::String) { - StringTableResource stringTableResource @ dataRVA - relativeVirtualDifference(); - } - else if (resourceIDType == ResourceID::StringData) { - TrueChar stringData[size] @ dataRVA - relativeVirtualDifference(); - } - else if (resourceIDType == ResourceID::GroupCursor) { - GroupCursor groupCursor @ dataRVA - relativeVirtualDifference(); - } - else if (resourceIDType == ResourceID::GroupIcon) { - GroupIcon groupIcon @ dataRVA - relativeVirtualDifference(); - } - else if (resourceIDType == ResourceID::Version) { - Version version @ dataRVA - relativeVirtualDifference(); - } - else if (resourceIDType == ResourceID::Manifest) { - TrueChar manifest[size] @ dataRVA - relativeVirtualDifference(); - } - else { - u8 resource[size] @ dataRVA - relativeVirtualDifference(); - } + u32 dataRVA; + u32 size; + u32 codepage; + u32 reserved; + + if (resourceIDType == ResourceID::Cursor) { + Cursor cursor @ dataRVA - relativeVirtualDifference(); + } + else if (resourceIDType == ResourceID::Bitmap || (resourceIDType == ResourceID::Icon && std::mem::read_string((dataRVA - relativeVirtualDifference())+1, 3) != "PNG")) { + imageDataSize = size; + Bitmap bitmap @ dataRVA - relativeVirtualDifference(); + } + else if (resourceIDType == ResourceID::Dialog) { + Dialog dialog @ dataRVA - relativeVirtualDifference(); + } + else if (resourceIDType == ResourceID::String) { + StringTableResource stringTableResource @ dataRVA - relativeVirtualDifference(); + } + else if (resourceIDType == ResourceID::StringData) { + TrueChar stringData[size] @ dataRVA - relativeVirtualDifference(); + } + else if (resourceIDType == ResourceID::GroupCursor) { + GroupCursor groupCursor @ dataRVA - relativeVirtualDifference(); + } + else if (resourceIDType == ResourceID::GroupIcon) { + GroupIcon groupIcon @ dataRVA - relativeVirtualDifference(); + } + else if (resourceIDType == ResourceID::Version) { + Version version @ dataRVA - relativeVirtualDifference(); + } + else if (resourceIDType == ResourceID::Manifest) { + TrueChar manifest[size] @ dataRVA - relativeVirtualDifference(); + } + else { + u8 resource[size] @ dataRVA - relativeVirtualDifference(); + } }; using ResourceDirectory; bitfield OffsetField { - offset : 31; - pointingToDirectory : 1; + offset : 31; + pointingToDirectory : 1; }; struct DataField { - if (std::mem::read_unsigned($+3, 1) >= 0x80) { - OffsetField offsetToData; - ResourceDirectory directory @ coffHeader.optionalHeader.directories[2].rva - relativeVirtualDifference() + offsetToData.offset; - } - else { - u32 offsetToData; - DataEntry dataEntry @ coffHeader.optionalHeader.directories[2].rva - relativeVirtualDifference() + offsetToData; - } + if ($[$+3] >= 0x80) { + OffsetField offsetToData; + ResourceDirectory directory @ coffHeader.optionalHeader.directories[2].rva - relativeVirtualDifference() + offsetToData.offset; + } + else { + u32 offsetToData; + DataEntry dataEntry @ coffHeader.optionalHeader.directories[2].rva - relativeVirtualDifference() + offsetToData; + } } [[inline]]; struct IdDirectoryEntry { - if ($ > coffHeader.optionalHeader.directories[2].rva - relativeVirtualDifference() + 0x10 + 8*(parent.directoryTable.nameEntriesAmount + parent.directoryTable.idEntriesAmount)) { - u32 id; - } - else { - ResourceID id; - resourceIDType = std::mem::read_unsigned(addressof(id), 4); - } + if ($ > coffHeader.optionalHeader.directories[2].rva - relativeVirtualDifference() + 0x10 + 8*(parent.directoryTable.nameEntriesAmount + parent.directoryTable.idEntriesAmount)) { + u32 id; + } + else { + ResourceID id; + resourceIDType = std::mem::read_unsigned(addressof(id), 4); + } - DataField datafield; + DataField datafield; }; struct NameDirectoryEntry { - OffsetField offsetToName; - std::string::SizedString16 name @ coffHeader.optionalHeader.directories[2].rva - relativeVirtualDifference() + offsetToName.offset; + OffsetField offsetToName; + std::string::SizedString16 name @ coffHeader.optionalHeader.directories[2].rva - relativeVirtualDifference() + offsetToName.offset; - DataField datafield; + DataField datafield; }; struct ResourceDirectoryTable { - u32 characteristics; - u32 timeDateStamp; - u16 majorVersion; - u16 minorVersion; - u16 nameEntriesAmount; - u16 idEntriesAmount; + u32 characteristics; + u32 timeDateStamp; + u16 majorVersion; + u16 minorVersion; + u16 nameEntriesAmount; + u16 idEntriesAmount; }; struct ResourceDirectory { - ResourceDirectoryTable directoryTable [[hex::spec_name("resourceDirectoryTable")]]; - NameDirectoryEntry nameEntries[directoryTable.nameEntriesAmount]; - IdDirectoryEntry idEntries[directoryTable.idEntriesAmount]; + ResourceDirectoryTable directoryTable [[hex::spec_name("resourceDirectoryTable")]]; + NameDirectoryEntry nameEntries[directoryTable.nameEntriesAmount]; + IdDirectoryEntry idEntries[directoryTable.idEntriesAmount]; }; struct ResourceTable { - ResourceDirectory rootDirectory; - $ = addressof(this)+coffHeader.optionalHeader.directories[2].size; + ResourceDirectory rootDirectory; + $ = addressof(this)+coffHeader.optionalHeader.directories[2].size; }; // Base Relocations Table enum BaseRelocationType : u8 { - Absolute, - High, - Low, - HighLow, - HighAdjacent, - Reserved = 6, - DIR64 = 10 + Absolute, + High, + Low, + HighLow, + HighAdjacent, + Reserved = 6, + DIR64 = 10 }; enum MIPSBaseRelocationType : u8 { - Absolute, - High, - Low, - HighLow, - HighAdjacent, - MIPSJMPAddress, - Reserved, - MIPSJMPAddress16 = 9, - DIR64 + Absolute, + High, + Low, + HighLow, + HighAdjacent, + MIPSJMPAddress, + Reserved, + MIPSJMPAddress16 = 9, + DIR64 }; enum ARMBaseRelocationType : u8 { - Absolute, - High, - Low, - HighLow, - HighAdjacent, - ARMMOV32, - Reserved, - DIR64 = 10 + Absolute, + High, + Low, + HighLow, + HighAdjacent, + ARMMOV32, + Reserved, + DIR64 = 10 }; enum RISCVBaseRelocationType : u8 { - Absolute, - High, - Low, - HighLow, - HighAdjacent, - RISCVHigh20, - Reserved, - RISCVLow12I, - RISCVLow12S, - DIR64 = 10 + Absolute, + High, + Low, + HighLow, + HighAdjacent, + RISCVHigh20, + Reserved, + RISCVLow12I, + RISCVLow12S, + DIR64 = 10 }; enum THUMBBaseRelocationType : u8 { - Absolute, - High, - Low, - HighLow, - HighAdjacent, - ARMMOV32, - Reserved, - ThumbMOV32, - DIR64 = 10 + Absolute, + High, + Low, + HighLow, + HighAdjacent, + ARMMOV32, + Reserved, + ThumbMOV32, + DIR64 = 10 }; enum LoongarchBaseRelocationType : u8 { - Absolute, - High, - Low, - HighLow, - HighAdjacent, - Reserved = 6, - MarkLA = 8, - DIR64 = 10 + Absolute, + High, + Low, + HighLow, + HighAdjacent, + Reserved = 6, + MarkLA = 8, + DIR64 = 10 }; fn formatBaseRelocationType(u8 value) { - if (coffHeader.architecture == ArchitectureType::MIPS16 || coffHeader.architecture == ArchitectureType::MIPSFPU || coffHeader.architecture == ArchitectureType::MIPSFPU16 || - coffHeader.architecture == ArchitectureType::R3000 || coffHeader.architecture == ArchitectureType::R4000 || coffHeader.architecture == ArchitectureType::R10000) { - MIPSBaseRelocationType mipsTypeBits = value; - return mipsTypeBits; - } - else if (coffHeader.architecture == ArchitectureType::ARM || coffHeader.architecture == ArchitectureType::ARM64 || coffHeader.architecture == ArchitectureType::ARMNT) { - ARMBaseRelocationType armTypeBits = value; - return armTypeBits; - } - else if (coffHeader.architecture == ArchitectureType::RISCV32 || coffHeader.architecture == ArchitectureType::RISCV64 || coffHeader.architecture == ArchitectureType::RISCV128) { - RISCVBaseRelocationType riscvTypeBits = value; - return riscvTypeBits; - } - else if (coffHeader.architecture == ArchitectureType::THUMB) { - THUMBBaseRelocationType thumbTypeBits = value; - return thumbTypeBits; - } - else if (coffHeader.architecture == ArchitectureType::LOONGARCH32 || coffHeader.architecture == ArchitectureType::LOONGARCH64) { - LoongarchBaseRelocationType loongarchTypeBits = value; - return loongarchTypeBits; - } - else { - BaseRelocationType genericTypeBits = value; - return genericTypeBits; - } + if (coffHeader.architecture == ArchitectureType::MIPS16 || coffHeader.architecture == ArchitectureType::MIPSFPU || coffHeader.architecture == ArchitectureType::MIPSFPU16 || + coffHeader.architecture == ArchitectureType::R3000 || coffHeader.architecture == ArchitectureType::R4000 || coffHeader.architecture == ArchitectureType::R10000) { + MIPSBaseRelocationType mipsTypeBits = value; + return mipsTypeBits; + } + else if (coffHeader.architecture == ArchitectureType::ARM || coffHeader.architecture == ArchitectureType::ARM64 || coffHeader.architecture == ArchitectureType::ARMNT) { + ARMBaseRelocationType armTypeBits = value; + return armTypeBits; + } + else if (coffHeader.architecture == ArchitectureType::RISCV32 || coffHeader.architecture == ArchitectureType::RISCV64 || coffHeader.architecture == ArchitectureType::RISCV128) { + RISCVBaseRelocationType riscvTypeBits = value; + return riscvTypeBits; + } + else if (coffHeader.architecture == ArchitectureType::THUMB) { + THUMBBaseRelocationType thumbTypeBits = value; + return thumbTypeBits; + } + else if (coffHeader.architecture == ArchitectureType::LOONGARCH32 || coffHeader.architecture == ArchitectureType::LOONGARCH64) { + LoongarchBaseRelocationType loongarchTypeBits = value; + return loongarchTypeBits; + } + else { + BaseRelocationType genericTypeBits = value; + return genericTypeBits; + } }; bitfield BaseRelocationWord { - offset : 12; - type : 4 [[format("formatBaseRelocationType")]]; + offset : 12; + type : 4 [[format("formatBaseRelocationType")]]; }; struct BaseRelocationBlock { - u32 pageRVA; - u32 blockSize; - BaseRelocationWord word[while($ < addressof(this) + this.blockSize)] [[inline]]; + u32 pageRVA; + u32 blockSize; + BaseRelocationWord word[while($ < addressof(this) + this.blockSize)] [[inline]]; }; struct BaseRelocationTable { - BaseRelocationBlock baseRelocationBlocks[while($ < addressof(this) + coffHeader.optionalHeader.directories[5].size)] [[inline]]; + BaseRelocationBlock baseRelocationBlocks[while($ < addressof(this) + coffHeader.optionalHeader.directories[5].size)] [[inline]]; }; // Debug Table enum DebugType : u32 { - Unknown, - COFF, - Codeview, - FPO, - Misc, - Exception, - Fixup, - OmapToSRC, - OmapFromSRC, - Borland, - Reserved10, - CLSID, - REPRO = 16, - ExtendedDLLCharacteristics = 20 + Unknown, + COFF, + Codeview, + FPO, + Misc, + Exception, + Fixup, + OmapToSRC, + OmapFromSRC, + Borland, + CLSID = 11, + REPRO = 16, + ExtendedDLLCharacteristics = 20 }; struct RSDS { - char signature[4]; - type::GUID guid; - u32 age; - char path[] [[format("formatNullTerminatedString")]]; + char signature[4]; + type::GUID guid; + u32 age; + char path[] [[format("formatNullTerminatedString")]]; }; struct DebugDirectory { - u32 characteristics; - type::time32_t timeDateStamp; - u16 majorVersion; - u16 minorVersion; - DebugType type; - u32 sizeOfData; - u32 virtualAddressOfRawData; - u32 pointerOfRawData; + u32 characteristics; + type::time32_t timeDateStamp; + u16 majorVersion; + u16 minorVersion; + DebugType type; + u32 sizeOfData; + u32 virtualAddressOfRawData; + u32 pointerOfRawData; }; struct DebugData { - DebugDirectory directory; - if (std::mem::read_string(directory.pointerOfRawData, 4) == "RSDS") { - RSDS rsds @ directory.pointerOfRawData; - } - else { - u8 data[directory.sizeOfData] @ directory.pointerOfRawData; - } - $ = addressof(this)+coffHeader.optionalHeader.directories[6].size; + DebugDirectory directory; + if (std::mem::read_string(directory.pointerOfRawData, 4) == "RSDS") { + RSDS rsds @ directory.pointerOfRawData; + } + else { + u8 data[directory.sizeOfData] @ directory.pointerOfRawData; + } + $ = addressof(this)+coffHeader.optionalHeader.directories[6].size; }; // TLS Table struct TLSTable { - if (coffHeader.optionalHeader.magic == PEFormat::PE32Plus) { - u64 rawDataStartVA; - u64 rawDataEndVA; - u64 indexAddress; - u64 callbacksAddress; - } - else { - u32 rawDataStartVA; - u32 rawDataEndVA; - u32 indexAddress; - u32 callbacksAddress; - } - u32 zeroFillSize; - u32 characteristics; - $ = addressof(this)+coffHeader.optionalHeader.directories[9].size; + if (coffHeader.optionalHeader.magic == PEFormat::PE32Plus) { + u64 rawDataStartVA; + u64 rawDataEndVA; + u64 indexAddress; + u64 callbacksAddress; + } + else { + u32 rawDataStartVA; + u32 rawDataEndVA; + u32 indexAddress; + u32 callbacksAddress; + } + u32 zeroFillSize; + u32 characteristics; + $ = addressof(this)+coffHeader.optionalHeader.directories[9].size; }; // CRT Section struct CRTSection { - u64 virtualAddresses[while($ < sectionsTable[currentSectionIndex].ptrRawData + sectionsTable[currentSectionIndex].sizeOfRawData)]; + u64 virtualAddresses[while($ < sectionsTable[currentSectionIndex].ptrRawData + sectionsTable[currentSectionIndex].sizeOfRawData)]; }; // Sections struct LineNumber { - if (std::mem::read_unsigned($+4) > 0) { - u32 virtualAddress; - } else { - u32 symbolTableIndex; - } - u32 lineNumber; + if (std::mem::read_unsigned($+4) > 0) { + u32 virtualAddress; + } else { + u32 symbolTableIndex; + } + u32 lineNumber; }; bool dataDirectoryInSection[coffHeader.optionalHeader.numberOfRVAsAndSizes]; fn checkForDataDirectory() { - for (u32 i = 0, i < coffHeader.optionalHeader.numberOfRVAsAndSizes, i += 1) - if (coffHeader.optionalHeader.directories[i].rva - relativeVirtualDifference() < sectionsTable[currentSectionIndex].ptrRawData+sectionsTable[currentSectionIndex].sizeOfRawData - && coffHeader.optionalHeader.directories[i].rva - relativeVirtualDifference() >= $) - dataDirectoryInSection[i] = true; + for (u32 i = 0, i < coffHeader.optionalHeader.numberOfRVAsAndSizes, i += 1) + if (coffHeader.optionalHeader.directories[i].rva - relativeVirtualDifference() < sectionsTable[currentSectionIndex].ptrRawData+sectionsTable[currentSectionIndex].sizeOfRawData + && coffHeader.optionalHeader.directories[i].rva - relativeVirtualDifference() >= $) + dataDirectoryInSection[i] = true; }; fn noDataDirectories() { - for (u32 i = 0, i < coffHeader.optionalHeader.numberOfRVAsAndSizes, i += 1) - if (dataDirectoryInSection[i]) return false; - return true; + for (u32 i = 0, i < coffHeader.optionalHeader.numberOfRVAsAndSizes, i += 1) + if (dataDirectoryInSection[i]) return false; + return true; }; -fn clearBoolArray() { - for (u32 i = 0, i < coffHeader.optionalHeader.numberOfRVAsAndSizes, i += 1) - dataDirectoryInSection[i] = false; +fn clearDirectoryChecks() { + for (u32 i = 0, i < coffHeader.optionalHeader.numberOfRVAsAndSizes, i += 1) + dataDirectoryInSection[i] = false; }; struct Section { - checkForDataDirectory(); - if (noDataDirectories()) { - if (std::string::starts_with(sectionsTable[currentSectionIndex].name, ".CRT")) // CRT section - CRTSection crtSection; - else - u8 freeformSection[sectionsTable[currentSectionIndex].sizeOfRawData]; // Freeform data section - } - else { - if (dataDirectoryInSection[0]) { - ExportsTable exportTable @ coffHeader.optionalHeader.directories[0].rva - relativeVirtualDifference(); - } - if (dataDirectoryInSection[1]) { - ImportsTable importTable @ coffHeader.optionalHeader.directories[1].rva - relativeVirtualDifference(); - } - if (dataDirectoryInSection[2]) { - ResourceTable resourceTable @ coffHeader.optionalHeader.directories[2].rva - relativeVirtualDifference(); - } - if (dataDirectoryInSection[3]) { - ExceptionTable exceptionTable @ coffHeader.optionalHeader.directories[3].rva - relativeVirtualDifference(); - } - if (dataDirectoryInSection[5]) { - BaseRelocationTable baseRelocationTable @ coffHeader.optionalHeader.directories[5].rva - relativeVirtualDifference(); - } - if (dataDirectoryInSection[6]) { - DebugData debugData @ coffHeader.optionalHeader.directories[6].rva - relativeVirtualDifference(); - } - if (dataDirectoryInSection[7]) { - char copyright[] @ coffHeader.optionalHeader.directories[7].rva - relativeVirtualDifference() [[format("formatNullTerminatedString")]]; - } - if (dataDirectoryInSection[9]) { - TLSTable tlsTable @ coffHeader.optionalHeader.directories[9].rva - relativeVirtualDifference(); - } - if (dataDirectoryInSection[13]) { - DelayedImportsTable delayedImportTable @ coffHeader.optionalHeader.directories[13].rva - relativeVirtualDifference(); - } - } - clearBoolArray(); - - LineNumber lineNumbers[sectionsTable[currentSectionIndex].numberOfLineNumbers] @ sectionsTable[currentSectionIndex].ptrLineNumbers; - - // Next section - if (sectionsTable[currentSectionIndex].sizeOfRawData > 0) // If the size of the next section is bigger than 0 - $ = addressof(this) + sectionsTable[currentSectionIndex].sizeOfRawData; // Put the current offset at the start of it to account for any bytes left - if (currentSectionIndex < coffHeader.numberOfSections-1) // If it's not the last section (to not make $ the address of an inexistent section) - $ = sectionsTable[currentSectionIndex+1].ptrRawData; // Put the current offset at the next section's address - currentSectionIndex += 1; // Make the current section index the next section's index + checkForDataDirectory(); + if (noDataDirectories()) { + if (std::string::starts_with(sectionsTable[currentSectionIndex].name, ".CRT")) // CRT section + CRTSection crtSection; + else + u8 freeformSection[sectionsTable[currentSectionIndex].sizeOfRawData]; // Freeform data section + } + else { + if (dataDirectoryInSection[0]) { + ExportsTable exportTable @ coffHeader.optionalHeader.directories[0].rva - relativeVirtualDifference(); + } + if (dataDirectoryInSection[1]) { + ImportsTable importTable @ coffHeader.optionalHeader.directories[1].rva - relativeVirtualDifference(); + } + if (dataDirectoryInSection[2]) { + ResourceTable resourceTable @ coffHeader.optionalHeader.directories[2].rva - relativeVirtualDifference(); + } + if (dataDirectoryInSection[3]) { + ExceptionTable exceptionTable @ coffHeader.optionalHeader.directories[3].rva - relativeVirtualDifference(); + } + if (dataDirectoryInSection[5]) { + BaseRelocationTable baseRelocationTable @ coffHeader.optionalHeader.directories[5].rva - relativeVirtualDifference(); + } + if (dataDirectoryInSection[6]) { + DebugData debugData @ coffHeader.optionalHeader.directories[6].rva - relativeVirtualDifference(); + } + if (dataDirectoryInSection[7]) { + char copyright[] @ coffHeader.optionalHeader.directories[7].rva - relativeVirtualDifference() [[format("formatNullTerminatedString")]]; + } + if (dataDirectoryInSection[9]) { + TLSTable tlsTable @ coffHeader.optionalHeader.directories[9].rva - relativeVirtualDifference(); + } + if (dataDirectoryInSection[13]) { + DelayedImportsTable delayedImportTable @ coffHeader.optionalHeader.directories[13].rva - relativeVirtualDifference(); + } + } + clearDirectoryChecks(); + + LineNumber lineNumbers[sectionsTable[currentSectionIndex].numberOfLineNumbers] @ sectionsTable[currentSectionIndex].ptrLineNumbers; + + // Next section + if (sectionsTable[currentSectionIndex].sizeOfRawData > 0) // If the size of the next section is bigger than 0 + $ = addressof(this) + sectionsTable[currentSectionIndex].sizeOfRawData; // Put the current offset at the start of it to account for any bytes left + if (currentSectionIndex < coffHeader.numberOfSections-1) // If it's not the last section (to not make $ the address of an inexistent section) + $ = sectionsTable[currentSectionIndex+1].ptrRawData; // Put the current offset at the next section's address + currentSectionIndex += 1; // Make the current section index the next section's index } [[name(sectionsTable[currentSectionIndex-1].name)]]; -Section sections[coffHeader.numberOfSections] @ sectionsTable[0].ptrRawData; +Section sections[coffHeader.numberOfSections] @ coffHeader.optionalHeader.sizeOfHeaders * !sectionsTable[0].sizeOfRawData + + sectionsTable[0].ptrRawData * sectionsTable[0].sizeOfRawData>0; // Symbol & String Tables enum SectionNumberType : s16 { - Undefined = 0, - Absolute = -1, - Debug = -2 + Undefined = 0, + Absolute = -1, + Debug = -2 }; enum SymbolTypeMSB : u8 { - Null = 0x00, - Pointer = 0x10, - Function = 0x20, - Array = 0x30 + 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 + 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 + 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 SymbolNameOffset { - padding[4]; - u32 offset [[name("nameOffset")]]; + padding[4]; + u32 offset [[name("nameOffset")]]; } [[inline]]; struct SymbolType { - SymbolTypeMSB msb; - SymbolTypeLSB lsb; + SymbolTypeMSB msb; + SymbolTypeLSB lsb; }; fn formatSymbolType(SymbolType value) { - return "{ " + std::string::to_string(value.msb) + " | " + std::string::to_string(value.lsb) + " }"; + return "{ " + std::string::to_string(value.msb) + " | " + std::string::to_string(value.lsb) + " }"; }; struct Symbol { - if (std::mem::read_unsigned($, 4) == 0) - SymbolNameOffset nameOffset; - else char shortName[8]; - u32 value; - SectionNumberType sectionNumber; - SymbolType type [[format("formatSymbolType")]]; - StorageClassType storageClass; - u8 numberOfAuxSymbols; + if (std::mem::read_unsigned($, 4) == 0) + SymbolNameOffset nameOffset; + else char shortName[8]; + u32 value; + SectionNumberType sectionNumber; + SymbolType type [[format("formatSymbolType")]]; + StorageClassType storageClass; + u8 numberOfAuxSymbols; }; bool checkForSymbols in; @@ -1205,12 +1210,12 @@ bool checkForSymbols in; Symbol symbolTable[checkForSymbols * coffHeader.numberOfSymbols] @ coffHeader.pointerToSymbolTable; struct SymbolString { - char string[] [[format("formatNullTerminatedString")]]; + char string[] [[format("formatNullTerminatedString")]]; } [[inline]]; struct StringTable { - u32 size; - SymbolString strings[while($ < addressof(this) + size)]; + u32 size; + SymbolString strings[while($ < addressof(this) + size)]; } [[inline]]; StringTable stringTable[sizeof(symbolTable)>0] @ addressof(symbolTable) + sizeof(symbolTable); @@ -1220,87 +1225,87 @@ bool checkForRichHeader in; u16 richHeaderEndPosition; u32 richHeaderPosition; fn findRichHeader() { - if (checkForRichHeader) { - for (u16 richEndCursor = peHeader.dosHeader.coffHeaderPointer, richEndCursor > peHeader.dosHeader.headerSizeInParagraphs*16, richEndCursor -= 1) { - if (std::mem::read_string(richEndCursor, 4) == "Rich") { - richHeaderEndPosition = richEndCursor; - //0x18 is the size of a Rich Header body with one product - for (u16 richCursor = richHeaderEndPosition - 0x18, richCursor > peHeader.dosHeader.headerSizeInParagraphs*16, richCursor -= 1) { - if (str(std::mem::read_unsigned(richCursor, 4) ^ std::mem::read_unsigned(richHeaderEndPosition+4, 4)) == "DanS") { - richHeaderPosition = richCursor; - break; - } - } - break; - } - } - } + if (checkForRichHeader) { + for (u16 richEndCursor = peHeader.dosHeader.coffHeaderPointer, richEndCursor > peHeader.dosHeader.headerSizeInParagraphs*16, richEndCursor -= 1) { + if (std::mem::read_string(richEndCursor, 4) == "Rich") { + richHeaderEndPosition = richEndCursor; + //0x18 is the size of a Rich Header body with one product + for (u16 richCursor = richHeaderEndPosition - 0x18, richCursor > peHeader.dosHeader.headerSizeInParagraphs*16, richCursor -= 1) { + if (str(std::mem::read_unsigned(richCursor, 4) ^ std::mem::read_unsigned(richHeaderEndPosition+4, 4)) == "DanS") { + richHeaderPosition = richCursor; + break; + } + } + break; + } + } + } }; findRichHeader(); fn formatHexadecimally(auto value) { - return std::string::to_string(value) + " (" + std::format("0x{:X}", value) + ")"; + return std::string::to_string(value) + " (" + std::format("0x{:X}", value) + ")"; }; fn unmask(u32 value) { - return formatHexadecimally(value ^ std::mem::read_unsigned(richHeaderEndPosition+4, 4)); + return formatHexadecimally(value ^ std::mem::read_unsigned(richHeaderEndPosition+4, 4)); }; fn unmaskBuild(u32 value) { - return formatHexadecimally(value ^ std::mem::read_unsigned(richHeaderEndPosition+4, 2)); + return formatHexadecimally(value ^ std::mem::read_unsigned(richHeaderEndPosition+4, 2)); }; fn unmaskProduct(u16 type) { - str value = "Unknown"; - str notation = "0x"; - if (type ^ std::mem::read_unsigned(richHeaderEndPosition+6, 2) < 0x10) { notation += "0"; } - match(type ^ std::mem::read_unsigned(richHeaderEndPosition+6, 2)) { - (0x00): value = "Unmarked"; - (0x01): value = "Imports"; - (0x04): value = "STDLIBDLL"; - (0x06): value = "VS97CVTRes"; - (0x0A): value = "VS98CCompiler"; - (0x0B): value = "VS98CPPCompiler"; - (0x0C): value = "OldNames"; - (0x0E): value = "MASM613"; - (0x0F): value = "VS2003Assembler"; - (0x16): value = "VC6SP5"; - (0x19): value = "VS2002Linker"; - (0x1C): value = "VS2002CCompiler"; - (0x1D): value = "VS2002CPPCompiler"; - (0x5D): value = "VS2003SDKIMP"; - (0x60): value = "VS2003CPPCompiler"; - (0x6D): value = "VS2005CCompiler"; - (0x6E): value = "VS2005CPPCompiler"; - (0x7B): value = "VS2005Linker"; - (0x93): value = "VS2008Linker"; - (0x9D): value = "Linker12"; - (0x9E): value = "MASM10"; - (0xAA): value = "VS2010CCompiler"; - (0xAB): value = "VS2010CPPCompiler"; - (0xFF): value = "VS2015CVTRes"; - (0x101 | 0x102): value = "VS2015Linker"; - (0x103): value = "VS2015Assembler"; - (0x104): value = "VS2015CCompiler"; - (0x105): value = "VS2015CPPCompiler"; - } - return value + " (" + std::format(notation + "{:X}", type ^ std::mem::read_unsigned(richHeaderEndPosition+6, 2)) + ")"; + str value = "Unknown"; + str notation = "0x"; + if (type ^ std::mem::read_unsigned(richHeaderEndPosition+6, 2) < 0x10) { notation += "0"; } + match(type ^ std::mem::read_unsigned(richHeaderEndPosition+6, 2)) { + (0x00): value = "Unmarked"; + (0x01): value = "Imports"; + (0x04): value = "STDLIBDLL"; + (0x06): value = "VS97CVTRes"; + (0x0A): value = "VS98CCompiler"; + (0x0B): value = "VS98CPPCompiler"; + (0x0C): value = "OldNames"; + (0x0E): value = "MASM613"; + (0x0F): value = "VS2003Assembler"; + (0x16): value = "VC6SP5"; + (0x19): value = "VS2002Linker"; + (0x1C): value = "VS2002CCompiler"; + (0x1D): value = "VS2002CPPCompiler"; + (0x5D): value = "VS2003SDKIMP"; + (0x60): value = "VS2003CPPCompiler"; + (0x6D): value = "VS2005CCompiler"; + (0x6E): value = "VS2005CPPCompiler"; + (0x7B): value = "VS2005Linker"; + (0x93): value = "VS2008Linker"; + (0x9D): value = "Linker12"; + (0x9E): value = "MASM10"; + (0xAA): value = "VS2010CCompiler"; + (0xAB): value = "VS2010CPPCompiler"; + (0xFF): value = "VS2015CVTRes"; + (0x101 | 0x102): value = "VS2015Linker"; + (0x103): value = "VS2015Assembler"; + (0x104): value = "VS2015CCompiler"; + (0x105): value = "VS2015CPPCompiler"; + } + return value + " (" + std::format(notation + "{:X}", type ^ std::mem::read_unsigned(richHeaderEndPosition+6, 2)) + ")"; }; struct Product { - u16 buildNumber [[format("unmaskBuild")]]; - u16 productID [[format("unmaskProduct")]]; - u32 objectCount [[format("unmask")]]; + u16 buildNumber [[format("unmaskBuild")]]; + u16 productID [[format("unmaskProduct")]]; + u32 objectCount [[format("unmask")]]; }; fn formatSignature(auto value) { - return "\"DanS\""; + return "\"DanS\""; }; using NullPadding = u32 [[format("unmask")]]; struct RichHeader { - char maskedSignature[4] [[format("formatSignature")]]; - NullPadding nullPadding[3]; - Product products[while($ < richHeaderEndPosition)]; - char signature[4]; - u32 mask; + char maskedSignature[4] [[format("formatSignature")]]; + NullPadding nullPadding[3]; + Product products[while($ < richHeaderEndPosition)]; + char signature[4]; + u32 mask; }; struct RichHeaderContainer { if (checkForRichHeader) RichHeader richHeader; }; diff --git a/patterns/snes.hexpat b/patterns/snes.hexpat new file mode 100644 index 00000000..4a6b690f --- /dev/null +++ b/patterns/snes.hexpat @@ -0,0 +1,110 @@ +#pragma author gmestanley +#pragma description Super Nintendo Entertainment System ROM header +#pragma sources snes.nesdev.org/wiki/ROM_header CPU_vectors en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map + +import std.string; + +u24 headerPosition = 0x7FC0; + +fn calculateHeaderPosition() { + if (std::mem::size() > 0x20000 && std::mem::size() < 0x600000) headerPosition += 0x8000; + else if (std::mem::size() >= 0x600000) headerPosition += 0x400000; +}; +calculateHeaderPosition(); + +enum ChipsetSubtype : u8 { + SPC7110, + ST01x, + ST018, + CX4 +}; + +struct ExpandedHeader { + char makerID[2]; + char gameID[4]; + padding[6]; + u8 expansionFlashSize [[comment("1 << N")]]; + u8 expansionRAMSize [[comment("1 << N")]]; + u8 specialVersion; + ChipsetSubtype chipsetSubtype; +}; + +struct ConditionalStruct { + if ($[headerPosition+0x1A] == 0x33) ExpandedHeader expandedHeader @ headerPosition - 0x10; + else if (!$[headerPosition+0x14]) ChipsetSubtype chipsetSubtype @ headerPosition - 1; +} [[inline]]; + +ConditionalStruct conditionalStruct @ $; + +enum MappingMode : u8 { + LoROM, + HiROM, + ExHiROM = 5 +}; + +fn formatMappingMode(u8 value) { + MappingMode enumValue = value; + return enumValue; +}; + +bitfield ROMType { + mappingMode : 4 [[format("formatMappingMode")]]; + speed : 1; + unknown : 1; +}; + +enum CoprocessorType : u8 { + DSP, + GSU, + OBC1, + SA1, + SDD1, + SRTC, + Other = 0x0E, + Custom +}; + +fn formatExtraHardwareType(u8 value) { + str valueMeaning = " (ROM"; + if (!value) valueMeaning += " only"; + else if (value) { + if (value > 3) valueMeaning += " + coprocessor"; + if (value != 3 || value != 6) valueMeaning += " + RAM"; + if (value == 2 || value > 5) valueMeaning += " + battery"; + } + return std::string::to_string(value) + valueMeaning + ")"; +}; + +fn formatCoprocessorType(u8 value) { + CoprocessorType enumValue = value; + return enumValue; +}; + +bitfield ExtraHardware { + extraHardwareType : 4 [[format("formatExtraHardwareType")]]; + coprocessorType : 4 [[format("formatCoprocessorType")]]; +}; + +enum Country : u8 { + NTSC = 1, + PAL +}; + +struct Header { + char title[21]; + ROMType romType; + ExtraHardware extraHardware; + u8 romSize [[comment("1 << N, rounded up")]]; + u8 ramSize [[comment("1 << N")]]; + Country country; + u8 developerID; + u8 romVersion; + u16 checksumComplement; + u16 checksum; + padding[4]; + u16 vectors[6]; + padding[4]; + u16 emulationModeVectors[6]; +}; + +Header header @ headerPosition; From 0a09efdd2011781a4a2c148a493beacd0e3ace96 Mon Sep 17 00:00:00 2001 From: Stephen Hewitt Date: Sat, 6 Dec 2025 07:16:36 +1100 Subject: [PATCH 88/93] patterns: Pattern for DOS EXE files (#452) * Initial DOS file * Update README.md * Update README.md * More README * More README * Add DOS EXE From: https://clasqm.github.io/freedos-repo/Games.html Name: Champ Galagon --- README.md | 1 + patterns/dos.hexpat | 242 ++++++++++++++++++++++++ tests/patterns/test_data/dos.hexpat.exe | Bin 0 -> 227681 bytes 3 files changed, 243 insertions(+) create mode 100644 patterns/dos.hexpat create mode 100644 tests/patterns/test_data/dos.hexpat.exe diff --git a/README.md b/README.md index d7e15c39..3fa6a278 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | DICOM | `application/dicom` | [`patterns/dicom.hexpat`](patterns/dicom.hexpat) | DICOM image format | | DMG | | [`patterns/dmg.hexpat`](patterns/dmg.hexpat) | Apple Disk Image Trailer (DMG) | | DMP | | [`patterns/dmp64.hexpat`](patterns/dmp64.hexpat) | Windows Kernel Dump(DMP64) | +| DOS | `application/x-dosexec` | [`patterns/dos.hexpat`](patterns/dos.hexpat) | 16-bit real mode DOS EXE files | | DOTNET_BinaryFormatter | | [`patterns/dotnet_binaryformatter.hexpat`](patterns/dotnet_binaryformatter.hexpat) | .NET BinaryFormatter | | DPAPI_Blob | | [`patterns/dpapblob.hexpat`](patterns/dpapiblob.hexpat) | Data protection API Blob File Format | | DPAPI_MasterKey | | [`patterns/dpapimasterkey.hexpat`](patterns/dpapimasterkey.hexpat) | Data protection API MasterKey | diff --git a/patterns/dos.hexpat b/patterns/dos.hexpat new file mode 100644 index 00000000..73cb5d51 --- /dev/null +++ b/patterns/dos.hexpat @@ -0,0 +1,242 @@ +#pragma author Stephen Hewitt +#pragma description MSDOS executable file + +#pragma MIME application/x-dosexec +#pragma MIME application/x-msdownload +#pragma MIME application/x-dosexecapplication/zip +#pragma MIME application/vnd.microsoft.portable-executable + +import type.magic; +import std.io; +import std.mem; +import std.math; +import std.string; + +/* + * A DOS EXE file, at a high level, consists of three regions: + * + * Header + * As it's name suggests. Contains info the loader uses. + * + * Load module + * Contains the program data that is loaded into memory. + * + * Extra data + * Data appended to the file that isn't loaded into memory. + * + * We'll call the combined header and load module the + * "program image". It's what the DOS loader cares about. + */ + + /* + * Wikipedia: The New Executable (NE or NewEXE) is a 16-bit executable + * file format, a successor to the DOS MZ executable format. It was used + * in Windows 1.0–3.x, Windows 9x, multitasking MS-DOS 4.0,[1] OS/2 1.x, + * and the OS/2 subset of Windows NT up to version 5.0 (Windows 2000). + * + * Since it was used in DOS we'll support it. + * + * We'll make it optional since some programs increased + * 'headerSizeInParagraphs' and stashed all kind of stuff there. + */ +bool EnableNEHeaderExt in; + +/* + * DOS file offsets/sizes. DOS uses INT 21h for file I/O. File positions and + * lengths are tracked using 32-bit signed integers. DOS INT 21h functions + * treat the offset as signed, so the highest positive offset is 0x7FFFFFFF. + * Attempting to seek beyond that or read/write beyond that will fail. + * We'll use a u32. + */ +u32 g_loadModule; +u32 g_loadModuleSize; +u32 g_programImageSize; + +fn formatNumber(u32 num, str msg="") { + if (std::string::length(msg)==0) + return std::format("0x{:x} ({})", num, num); + else + return std::format("{} 0x{:x} ({})", msg, num, num); +}; + +fn inLoadModule(u32 off, u32 sz) { + return off>=g_loadModule && off+sz<=g_loadModule+g_loadModuleSize; +}; + +struct Relocation { + u16 offset [[color("9AE630")]]; + u16 segment [[color("FE9A37")]]; +}; + +struct RelocationAnnotated : Relocation { + u32 fileOffset = g_loadModule+offset+segment*16; + if (inLoadModule(fileOffset, 2)) { + u16 __goto__target @ fileOffset [[highlight_hidden]]; + } + else { + str __goto__target = formatNumber(fileOffset, "Not in load module") [[export, highlight_hidden]]; + } +}; + +struct Relocations { + if (parent.dosHeader.relocations>0) { + Relocation __goto__firstReloc @ $ [[highlight_hidden]]; + Relocation __goto__lastReloc @ $+(parent.dosHeader.relocations-1)*sizeof(Relocation) [[highlight_hidden]]; + } + RelocationAnnotated data[parent.dosHeader.relocations] [[inline]]; +}; + +struct DOSHeader { + type::Magic<"MZ"> signature [[hex::spec_name("e_magic")]]; + u16 extraPageSize [[hex::spec_name("e_cblp")]]; + u16 numberOfPages [[hex::spec_name("e_cp")]]; + g_programImageSize = (extraPageSize==0) ? + (numberOfPages*512) : + (numberOfPages-1)*512 + extraPageSize; + str __programImageSize = formatNumber(g_programImageSize) [[export, highlight_hidden]]; + u8 __goto__lastByteInProgramImage @ g_programImageSize-1 [[highlight_hidden]]; + u16 relocations [[name("stubRelocations"), hex::spec_name("e_crlc")]]; + u16 headerSizeInParagraphs [[hex::spec_name("e_cparhdr")]]; + u32 headerSize = headerSizeInParagraphs*16; + g_loadModule = headerSizeInParagraphs*16; + g_loadModuleSize = g_programImageSize - headerSize; + str __headerSize = formatNumber(headerSize) [[export, highlight_hidden]]; + u8 __goto__lastByteInHeader @ headerSize-1 [[highlight_hidden]]; + u16 minimumAllocatedParagraphs [[hex::spec_name("e_minalloc")]]; + u16 maximumAllocatedParagraphs [[hex::spec_name("e_maxalloc")]]; + u16 initialSSValue [[hex::spec_name("e_ss")]]; + u16 initialRelativeSPValue [[hex::spec_name("e_sp")]]; + u16 checksum [[name("stubChecksum"), hex::spec_name("e_csum")]]; + u16 initialRelativeIPValue [[hex::spec_name("e_ip")]]; + u16 initialCSValue [[hex::spec_name("e_cs")]]; + + u32 csAddrFirst = initialCSValue<<4; + u32 csAddrLast = (csAddrFirst+0xffff) & ((1<<20)-1); + + u32 csEndGap = 0; + if (csAddrFirst <= csAddrLast) { + u32 csOffsetFirst = headerSize+csAddrFirst; + u32 csOffsetLast = csOffsetFirst+std::math::min(0x10000, g_loadModuleSize)-1; + } + else { + u32 csOffsetFirst = headerSize; + csEndGap = (1<<20)-csAddrFirst; + u32 csOffsetLast = headerSize+(0x10000-csEndGap-1); + + std::warning("EXE has 'initialCSValue' set such that 20-bit address wraps."); + std::warning(" My guess would be to get the PSP into the CS."); + } + + + /* + * Adding `csEndGap` to the `initialIP` calculation below is required because the + * program is started by transferring execution to CS:IP. If `csEndGap` is non-zero + * CS and the start of the load-module value do not align; there’s some extra data + * the CPU can see before the data in the EXE. What confused me for a bit was why + * it’s not required in the relocation target locations I make. The reason, I think, + * is that when the loader loads the load-module into memory and then proceeds to + * apply the relocations, the offsets are relative to the segment the code is loaded + * in and not the execution environment (the CS register from `initialCSValue`). + */ + u32 initialIP = csOffsetFirst+initialRelativeIPValue-csEndGap; + + if (inLoadModule(initialIP, 1)) + u8 __goto__initiaIP @ initialIP [[highlight_hidden]]; + else + str __goto__initiaIP = formatNumber(initialIP, "Not in load module!") [[export, highlight_hidden]]; + + u32 csSize = csOffsetLast-csOffsetFirst+1; + if (inLoadModule(csOffsetFirst, csSize)) { + std::mem::Bytes __select__InitialCS @ csOffsetFirst [[highlight_hidden]]; + u8 __goto__InitialCS_first @ csOffsetFirst [[highlight_hidden]]; + u8 __goto__InitialCS_last @ csOffsetFirst+csSize-1 [[highlight_hidden]]; + } + else { + str __select__CS = formatNumber(csOffsetFirst, "Not in image!") [[export, highlight_hidden]]; + } + + u16 relocationsTablePointer [[hex::spec_name("e_lfarlc")]]; + u32 sizeofRelocations = relocations*sizeof(Relocation); + if (relocations>0 && relocationsTablePointer+sizeofRelocations __select__relocationsTable + @ relocationsTablePointer [[highlight_hidden]]; + } + else { + str __select__relocationsTable = + "Not in image or zero length" [[export, highlight_hidden]]; + } + u16 overlayNumber [[hex::spec_name("e_ovno")]]; +}; + +struct NEDOSHeaderExt { + u16 reservedWords[4] [[hex::spec_name("e_res")]]; + u16 oemIdentifier [[hex::spec_name("e_oemid")]]; + u16 oemInformation [[hex::spec_name("e_oeminfo")]]; + u16 otherReservedWords[10] [[hex::spec_name("e_res2")]]; + u32 newHeaderPointer [[hex::spec_name("e_lfanew")]]; +}; + +struct NEDOSHeaderExtAnnotated : NEDOSHeaderExt { + if (newHeaderPointer < std::mem::size()) + u8 __goto__newHeader @ newHeaderPointer [[highlight_hidden]]; + else + str __goto__newHeader + = formatNumber(newHeaderPointer, "Not in image!") [[export, highlight_hidden]]; +}; + +/* + * The header of a DOS EXE file consists of three regions. + * + * DOSHeader + * Present in all DOS EXEs. Used by the loader. + * + * NEDOSHeaderExt + * An extension to the header. Optional. + * + * Relocations + * An array of segment relocations to the apply to the load module. Optional. + * + * The header is followed by the load module. There can be gaps between + * DOSHeader (or NEDOSHeaderExt if present) and Relocations, and between the + * Relocations and the load module. It is not uncommon for EXEs to stash candy + * in these gaps. + */ + +struct Header { + DOSHeader dosHeader; + + if (EnableNEHeaderExt) { + if (dosHeader.relocationsTablePointer < $+sizeof(NEDOSHeaderExt)) { + std::warning("NEHeaderExt and Relocations overlap. Disabling NEHeaderExt."); + } + else { + NEDOSHeaderExtAnnotated extHeader; + } + } + + if (dosHeader.relocations > 0) { + if (dosHeader.relocationsTablePointer < $) { + std::warning("Relocation table overlaps previous header members"); + } + if (dosHeader.relocationsTablePointer+dosHeader.relocations*sizeof(Relocation) > g_loadModule) { + std::warning("Relocation table ends past header."); + } + } + + if (dosHeader.relocationsTablePointer > $) { + u8 header_reloc_gap[dosHeader.relocationsTablePointer-$] [[highlight_hidden]]; + } + Relocations relocations; + if (g_loadModule > $) { + u8 reloc_loadModule_gap[g_loadModule-$] [[highlight_hidden]]; + } +}; + +struct LoadModule { + u8 __goto__first @ $ [[highlight_hidden]]; + u8 __goto__last @ $+g_loadModuleSize-1 [[highlight_hidden]]; + u8 data[g_loadModuleSize]; +} [[color("7393B3")]]; + +Header header @0; +LoadModule loadModule @g_loadModule;; \ No newline at end of file diff --git a/tests/patterns/test_data/dos.hexpat.exe b/tests/patterns/test_data/dos.hexpat.exe new file mode 100644 index 0000000000000000000000000000000000000000..5e20b67cd9746ad22545abb773c275ec186b255d GIT binary patch literal 227681 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

`*v0e~$%sVE?7DA8;&g%J!^ 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 Date: Fri, 5 Dec 2025 15:17:01 -0500 Subject: [PATCH 89/93] patterns: Add support for KTX 1.0 textures (#451) * Commit patterns I've collected - AppleSingle/AppleDouble pattern, used for macOS resource forks. - MAME CHD file format, currently only supports v5. - KEX Engine proprietary TARC format, used by various Nightdive games. * Add to README * Add pattern for KTX 1.0 file * Use import, add reference ktx file * Add to README --- README.md | 1 + patterns/ktx.hexpat | 231 ++++++++++++++++++ .../ktx.hexpat/rgb-mipmap-reference.ktx | Bin 0 -> 16480 bytes 3 files changed, 232 insertions(+) create mode 100644 patterns/ktx.hexpat create mode 100644 tests/patterns/test_data/ktx.hexpat/rgb-mipmap-reference.ktx diff --git a/README.md b/README.md index 3fa6a278..671ac217 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ISO | `application/x-iso9660-image` | [`patterns/iso.hexpat`](patterns/iso.hexpat) | ISO 9660 file system | | Java Class | `application/x-java-applet` | [`patterns/java_class.hexpat`](patterns/java_class.hexpat) | Java Class files | | JPEG | `image/jpeg` | [`patterns/jpeg.hexpat`](patterns/jpeg.hexpat) | JPEG Image Format | +| KTX | `image/ktx` | [`patterns/ktx.hexpat`](patterns/ktx.hexpat) | Khronos TeXture 1.0 | | LOC | | [`patterns/loc.hexpat`](patterns/loc.hexpat) | Minecraft Legacy Console Edition Language file | | LUC | | [`patterns/popcap_luc.hexpat`](patterns/popcap_luc.hexpat) | PopCap's proprietary Lua bytecode | | Lua 5.1 | | [`patterns/lua51.hexpat`](patterns/lua51.hexpat) | Lua 5.1 bytecode | diff --git a/patterns/ktx.hexpat b/patterns/ktx.hexpat new file mode 100644 index 00000000..3cff6a54 --- /dev/null +++ b/patterns/ktx.hexpat @@ -0,0 +1,231 @@ +#pragma author Lexi Mayfield +#pragma description Khronos TeXture 1.0 +#pragma MIME image/ktx +#pragma magic [ AB 4B 54 58 20 31 31 BB 0D 0A 1A 0A ] @ 0x00 + +import std.io; +import std.mem; +import type.magic; + +enum GLType : u32 { + compressed = 0x0, + UNSIGNED_BYTE = 0x1401, + BYTE = 0x1400, + UNSIGNED_SHORT = 0x1403, + SHORT = 0x1402, + UNSIGNED_INT = 0x1405, + INT = 0x1404, + HALF_FLOAT = 0x140B, + FLOAT = 0x1406, + UNSIGNED_BYTE_3_3_2 = 0x8032, + UNSIGNED_BYTE_2_3_3_REV = 0x8362, + UNSIGNED_SHORT_5_6_5 = 0x8363, + UNSIGNED_SHORT_5_6_5_REV = 0x8364, + UNSIGNED_SHORT_4_4_4_4 = 0x8033, + UNSIGNED_SHORT_4_4_4_4_REV = 0x8365, + UNSIGNED_SHORT_5_5_5_1 = 0x8034, + UNSIGNED_SHORT_1_5_5_5_REV = 0x8366, + UNSIGNED_INT_8_8_8_8 = 0x8035, + UNSIGNED_INT_8_8_8_8_REV = 0x8367, + UNSIGNED_INT_10_10_10_2 = 0x8036, + UNSIGNED_INT_2_10_10_10_REV = 0x8368, + UNSIGNED_INT_24_8 = 0x84FA, + UNSIGNED_INT_10F_11F_11F_REV = 0x8C3B, + UNSIGNED_INT_5_9_9_9_REV = 0x8C3E, + FLOAT_32_UNSIGNED_INT_24_8_REV = 0x8DAD, +}; + +enum GLFormat : u32 { + compressed = 0x0, + RED = 0x1903, + RG = 0x8227, + RGB = 0x1907, + BGR = 0x80E0, + RGBA = 0x1908, + BGRA = 0x80E1, + RED_INTEGER = 0x8D94, + RG_INTEGER = 0x8228, + RGB_INTEGER = 0x8D98, + BGR_INTEGER = 0x8D9A, + RGBA_INTEGER = 0x8D99, + BGRA_INTEGER = 0x8D9B, + STENCIL_INDEX = 0x1901, + DEPTH_COMPONENT = 0x1902, + DEPTH_STENCIL = 0x84F9, +}; + +enum GLInternalFormat : u32 { + R8 = 0x8229, + R8_SNORM = 0x8F94, + R16 = 0x822A, + R16_SNORM = 0x8F98, + RG8 = 0x822B, + RG8_SNORM = 0x8F95, + RG16 = 0x822C, + RG16_SNORM = 0x8F99, + R3_G3_B2 = 0x2A10, + RGB4 = 0x804F, + RGB5 = 0x8050, + RGB8 = 0x8051, + RGB8_SNORM = 0x8F96, + RGB10 = 0x8052, + RGB12 = 0x8053, + RGB16_SNORM = 0x8F9A, + RGBA2 = 0x8055, + RGBA4 = 0x8056, + RGB5_A1 = 0x8057, + RGBA8 = 0x8058, + RGBA8_SNORM = 0x8F97, + RGB10_A2 = 0x8059, + RGB10_A2UI = 0x906F, + RGBA12 = 0x805A, + RGBA16 = 0x805B, + SRGB8 = 0x8C41, + SRGB8_ALPHA8 = 0x8C43, + R16F = 0x822D, + RG16F = 0x822F, + RGB16F = 0x881B, + RGBA16F = 0x881A, + R32F = 0x822E, + RG32F = 0x8230, + RGB32F = 0x8815, + RGBA32F = 0x8814, + R11F_G11F_B10F = 0x8C3A, + RGB9_E5 = 0x8C3D, + R8I = 0x8231, + R8UI = 0x8232, + R16I = 0x8233, + R16UI = 0x8234, + R32I = 0x8235, + R32UI = 0x8236, + RG8I = 0x8237, + RG8UI = 0x8238, + RG16I = 0x8239, + RG16UI = 0x823A, + RG32I = 0x823B, + RG32UI = 0x823C, + RGB8I = 0x8D8F, + RGB8UI = 0x8D7D, + RGB16I = 0x8D89, + RGB16UI = 0x8D77, + RGB32I = 0x8D83, + RGB32UI = 0x8D71, + RGBA8I = 0x8D8E, + RGBA8UI = 0x8D7C, + RGBA16I = 0x8D88, + RGBA16UI = 0x8D76, + RGBA32I = 0x8D82, + RGBA32UI = 0x8D70, + COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0, // BC1 + COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1, // BC1 + COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2, // BC2 + COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3, // BC3 + COMPRESSED_SRGB_S3TC_DXT1_EXT = 0x8C4C, // BC1 + COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT = 0x8C4D, // BC1 + COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT = 0x8C4E, // BC2 + COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT = 0x8C4F, // BC3 + COMPRESSED_RED_RGTC1 = 0x8DBB, // BC4 + COMPRESSED_SIGNED_RED_RGTC1 = 0x8DBC, // BC4 + COMPRESSED_RG_RGTC2 = 0x8DBD, // BC5 + COMPRESSED_SIGNED_RG_RGTC2 = 0x8DBE, // BC5 + COMPRESSED_RGBA_BPTC_UNORM = 0x8E8C, // BC7 + COMPRESSED_SRGB_ALPHA_BPTC_UNORM = 0x8E8D, // BC7 + COMPRESSED_RGB_BPTC_SIGNED_FLOAT = 0x8E8E, // BC6H + COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT = 0x8E8F, // BC6H + ETC1_RGB8_OES = 0x8D64, // ETC1 + COMPRESSED_R11_EAC = 0x9270, // EAC + COMPRESSED_SIGNED_R11_EAC = 0x9271, // EAC + COMPRESSED_RG11_EAC = 0x9272, // EAC + COMPRESSED_SIGNED_RG11_EAC = 0x9273, // EAC + COMPRESSED_RGB8_ETC2 = 0x9274, // ETC2 + COMPRESSED_SRGB8_ETC2 = 0x9275, // ETC2 + COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276, // ETC2 + COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277, // ETC2 + COMPRESSED_RGBA8_ETC2_EAC = 0x9278, // ETC2 + COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279, // ETC2 + COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0, // ASTC... + COMPRESSED_RGBA_ASTC_5x4_KHR = 0x93B1, + COMPRESSED_RGBA_ASTC_5x5_KHR = 0x93B2, + COMPRESSED_RGBA_ASTC_6x5_KHR = 0x93B3, + COMPRESSED_RGBA_ASTC_6x6_KHR = 0x93B4, + COMPRESSED_RGBA_ASTC_8x5_KHR = 0x93B5, + COMPRESSED_RGBA_ASTC_8x6_KHR = 0x93B6, + COMPRESSED_RGBA_ASTC_8x8_KHR = 0x93B7, + COMPRESSED_RGBA_ASTC_10x5_KHR = 0x93B8, + COMPRESSED_RGBA_ASTC_10x6_KHR = 0x93B9, + COMPRESSED_RGBA_ASTC_10x8_KHR = 0x93BA, + COMPRESSED_RGBA_ASTC_10x10_KHR = 0x93BB, + COMPRESSED_RGBA_ASTC_12x10_KHR = 0x93BC, + COMPRESSED_RGBA_ASTC_12x12_KHR = 0x93BD, + COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR = 0x93D0, + COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR = 0x93D1, + COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR = 0x93D2, + COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR = 0x93D3, + COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR = 0x93D4, + COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR = 0x93D5, + COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR = 0x93D6, + COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR = 0x93D7, + COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR = 0x93D8, + COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR = 0x93D9, + COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR = 0x93DA, + COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR = 0x93DB, + COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR = 0x93DC, + COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR = 0x93DD, +}; + +enum GLBaseInternalFormat : u32 { + RED = 0x1903, + RG = 0x8227, + RGB = 0x1907, + BGR = 0x80E0, + RGBA = 0x1908, + BGRA = 0x80E1, + RED_INTEGER = 0x8D94, + RG_INTEGER = 0x8228, + RGB_INTEGER = 0x8D98, + BGR_INTEGER = 0x8D9A, + RGBA_INTEGER = 0x8D99, + BGRA_INTEGER = 0x8D9B, + SRGB = 0x8C40, + SRGB8 = 0x8C41, + SRGB_ALPHA = 0x8C42, + SRGB8_ALPHA8 = 0x8C43, +}; + +struct KeyValue { + u32 keyAndValueByteSize; + char keyAndValue[keyAndValueByteSize]; + std::mem::AlignTo<4>; +}; + +struct Header { + u32 endianness; + GLType glType; + u32 glTypeSize; + GLFormat glFormat; + GLInternalFormat glInternalFormat; + GLBaseInternalFormat glBaseInternalFormat; + u32 pixelWidth; + u32 pixelHeight; + u32 pixelDepth; + u32 numberOfArrayElements; + u32 numberOfFaces; + u32 numberOfMipmapLevels; + u32 bytesOfKeyValueData; +}; + +struct MipLevel { + u32 imageSize; + u8 imageData[imageSize]; + std::mem::AlignTo<4>; +}; + +struct KhronosTexture { + type::Magic<"\xABKTX 11\xBB\x0D\x0A\x1A\x0A"> identifier; + Header header; + u32 keyValueEnd = $ + header.bytesOfKeyValueData; + KeyValue kv[while($ < keyValueEnd)]; + MipLevel mipLevels[header.numberOfMipmapLevels]; +}; + +KhronosTexture khronosTexture @ 0x00; \ No newline at end of file diff --git a/tests/patterns/test_data/ktx.hexpat/rgb-mipmap-reference.ktx b/tests/patterns/test_data/ktx.hexpat/rgb-mipmap-reference.ktx new file mode 100644 index 0000000000000000000000000000000000000000..622a9477f576953a7541528da59b7c7b75a6e875 GIT binary patch literal 16480 zcmeI(u?Ye(6adg4??5;dENrxL1aAYaZ7m$cG2FxvY}_p7a@+uR%DwPI2q7VPkU)z6 zyj*XGzJIN|eU}zZo3;@tB9^;|^J8pJr9afE)bXivjL3oj0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oU&rfmoF--UR^y1PBlyK!5-N o0&@|Fru+f2ATVEn_mC6+=gIITo&W1YZ0eMH`KL!9+PX@P4?po6ApigX literal 0 HcmV?d00001 From 28a297582b0ad69b334bf3c7c6a7233627f17da8 Mon Sep 17 00:00:00 2001 From: F01TECH <59177844+F01TECH@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:18:56 -0500 Subject: [PATCH 90/93] patterns: Added DFIR Patterns (#442) * Added /DFIR/ with patterns Added /DFIR/ sub-directory. Contains modified versions of built-in patterns for semi-automated Disk/Volume/Filesystem parsing geared towards Digital Forensics. Originals in /fs/ should remain in tact for spot placement. * DFIR_README.md * DFIR_README.md * DFIR_README.md * DISK_PARSER.hexpat * DISK_PARSER.hexpat * FAT32.hexpat * exFAT.hexpat * README.md Added DFIR related hexpats to table. * README.md --------- Co-authored-by: Xtreme-Liberty <59177844+Xtreme-Liberty@users.noreply.github.com> --- README.md | 4 + patterns/DFIR/DFIR_README.md | 81 ++ patterns/DFIR/DISK_PARSER.hexpat | 677 +++++++++++++ patterns/DFIR/FAT32.hexpat | 789 +++++++++++++++ patterns/DFIR/NTFS.hexpat | 1571 ++++++++++++++++++++++++++++++ patterns/DFIR/exFAT.hexpat | 616 ++++++++++++ 6 files changed, 3738 insertions(+) create mode 100644 patterns/DFIR/DFIR_README.md create mode 100644 patterns/DFIR/DISK_PARSER.hexpat create mode 100644 patterns/DFIR/FAT32.hexpat create mode 100644 patterns/DFIR/NTFS.hexpat create mode 100644 patterns/DFIR/exFAT.hexpat diff --git a/README.md b/README.md index 671ac217..ff51aa7a 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | DDS | `image/vnd-ms.dds` | [`patterns/dds.hexpat`](patterns/dds.hexpat) | DirectDraw Surface | | DEX | | [`patterns/dex.hexpat`](patterns/dex.hexpat) | Dalvik EXecutable Format | | DICOM | `application/dicom` | [`patterns/dicom.hexpat`](patterns/dicom.hexpat) | DICOM image format | +| DISK_PARSER (DFIR) | `application/x-ima` | [`patterns/DFIR/DISK_PARSER.hexpat`](patterns/DFIR/DISK_PARSER.hexpat) | Recursive Disk/Volume/Filesystem parsing | | DMG | | [`patterns/dmg.hexpat`](patterns/dmg.hexpat) | Apple Disk Image Trailer (DMG) | | DMP | | [`patterns/dmp64.hexpat`](patterns/dmp64.hexpat) | Windows Kernel Dump(DMP64) | | DOS | `application/x-dosexec` | [`patterns/dos.hexpat`](patterns/dos.hexpat) | 16-bit real mode DOS EXE files | @@ -74,9 +75,11 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ESP32 Image | | [`patterns/esp32_image.hexpat`](patterns/esp32_image.hexpat) | Firmware image format for the ESP32 chip family | | EVTX | `application/x-ms-evtx` | [`patterns/evtx.hexpat`](patterns/evtx.hexpat) | MS Windows Vista Event Log | | EXFAT | | [`patterns/fs/exfat.hexpat`](patterns/fs/exfat.hexpat) | Extensible File Allocation Table (exFAT) | +| EXFAT (DFIR) | | [`patterns/DFIR/exFAT.hexpat`](patterns/DFIR/exFAT.hexpat) | Imported by DISK_PARSER.hexpat | | EXT4 | | [`patterns/fs/ext4.hexpat`](patterns/fs/ext4.hexpat) | Ext4 File System | | FAS | | [`patterns/fas_oskasoftware.hexpat`](patterns/fas_oskasoftware.hexpat) [`patterns/fas_oskasoftware_old.hexpat`](patterns/fas_oskasoftware_old.hexpat) (Old versions of Oska DeskMate) | Oska Software DeskMates FAS (Frames and Sequences) file | | FAT32 | | [`patterns/fs/fat32.hexpat`](patterns/fs/fat32.hexpat) | FAT32 File System | +| FAT32 (DFIR) | | [`patterns/DFIR/FAT32.hexpat`](patterns/DFIR/FAT32.hexpat) | Imported by DISK_PARSER.hexpat | | FBX | | [`patterns/fbx.hexpat`](patterns/fbx.hexpat) | Kaydara FBX Binary | | FDT | | [`patterns/fdt.hexpat`](patterns/fdt.hexpat) | Flat Linux Device Tree blob | | FFX | | [`patterns/ffx/*`](https://gitlab.com/EvelynTSMG/imhex-ffx-pats) | Various Final Fantasy X files | @@ -134,6 +137,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | NRO | | [`patterns/nro.hexpat`](patterns/nro.hexpat) | Nintendo Switch NRO files | | NTAG | | [`patterns/ntag.hexpat`](patterns/ntag.hexpat) | NTAG213/NTAG215/NTAG216, NFC Forum Type 2 Tag compliant IC | | NTFS | | [`patterns/fs/ntfs.hexpat`](patterns/fs/ntfs.hexpat) | NTFS (NT File System) | +| NTFS (DFIR) | | [`patterns/DFIR/NTFS.hexpat`](patterns/DFIR/NTFS.hexpat) | Imported by DISK_PARSER.hexpat | | OGG | `audio/ogg` | [`patterns/ogg.hexpat`](patterns/ogg.hexpat) | OGG Audio format | | ORP / ORS | | [`patterns/orp.hexpat`](patterns/orp.hexpat) | OpenRGB profile format | | PACK | | [`patterns/roblox_pack.hexpat`](patterns/roblox_pack.hexpat) | Roblox shader archive format | diff --git a/patterns/DFIR/DFIR_README.md b/patterns/DFIR/DFIR_README.md new file mode 100644 index 00000000..6c518ddb --- /dev/null +++ b/patterns/DFIR/DFIR_README.md @@ -0,0 +1,81 @@ +ImHex Pattern Files - Digital Forensics: + + - [ImHex-DFIR-Patterns](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns) + +Enhanced features of the stock Disk/Filesystem pattern files for forensic review of disk content. + - [ImHex](https://github.com/WerWolv/ImHex) + - [ImHex Patterns](https://github.com/WerWolv/ImHex-Patterns) + +Use: + - Open a physical disk via Raw Provider (read-only) + - EXAMPLE: /dev/disk6 + - Import Pattern File + - EXAMPLE: DISK_PARSER.hexpat + - [Pattern_Selection (screenshot)](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns/blob/main/screenshots/2-DISK_PARSER-Pattern.png) + + - DISK_PARSER.hexpat + - Recognize MBR/GPT Disks and parse MPT/GPT + - Including Logical Volumes in an Extended Partition (container) + - Auto load file system patterns for FAT32, exFAT, NTFS formatted volumes + - Optional Disk Report + + - [DISK > MBR/GPT (screenshot)](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns/blob/main/screenshots/3-DISK-HYBRID.png) + - [DISK > MBR > MPT > 3 Primaries | 2 Logicals in an Extended (screenshot)](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns/blob/main/screenshots/3a-DISK-MBR.png) + + - FAT32.hexpat + - Auto loaded by DISK_PARSER.hexpat + - Parse VBR, FAT1, FAT2, Root Dir, and 1 level of SubDirs + - FAT1/FAT2 Cluster chaining with SFN resolution + - LFN/SFN Alias grouping in Root Dir + - Recognize deleted entries (xE5) + - File Content pointer + - D/T Conversions + - Optional FAT32 Volume Report + + - [VOLUME > FAT32 > FAT1 (screenshot)](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns/blob/main/screenshots/4-FAT32-1_SMALL_TXT.png) + - [VOLUME > FAT32 > Root Dir (screenshot)](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns/blob/main/screenshots/5-FAT32_ROOT_DIR.png) + - [VOLUME > FAT32 > Data Pointer (screenshot)](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns/blob/main/screenshots/6-FAT32_SFN_POINTER.png) + + - exFAT.hexpat + - Auto loaded by DISK_PARSER.hexpat + - Parse VBR/Boot Sector/Extended Sectors, FAT1, Root Dir + - Recognize active directory entries (x85, xC0, xC1) + - Recognize inactive directory entries (x05, x40, x41) + - xC0/x40 File Content pointer + - D/T Conversions + - Optional exFAT Volume Report + + - [VOLUME > exFAT (screenshot)](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns/blob/main/screenshots/7-exFAT-1.png) + - [VOLUME > exFAT > Root Dir > xC0 (screenshot)](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns/blob/main/screenshots/8-exFAT_xC0.png) + - [VOLUME > exFAT > Data Pointer (screenshot)](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns/blob/main/screenshots/9-exFAT-Data_Pointer.png) + + - NTFS.hexpat + - Auto loaded by DISK_PARSER.hexpat + - Parse VBR (Boot Sector), $MFT, Root Dir, and Indexes + - Recursively parse the $Metadata files, $Attributes, and user files/dirs + - Added file record | parent [MFT#] [SEQ#] indicators + - Parse x80/xB0 Data Runs + - File Content pointer + - D/T Conversions + - Optional NTFS Volume Report + + - [VOLUME > NTFS > $MFT > D/T Conversion (screenshot)](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns/blob/main/screenshots/10-NTFS-DT.png) + - [VOLUME > NTFS > $MFT > x80 Run List (screenshot)](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns/blob/main/screenshots/11-NTFS-DATA_RUN.png) + - [VOLUME > NTFS > Data Pointer (screenshot)](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns/blob/main/screenshots/12-NTFS-DATA_POINTER.png) + + - Optional Reports + - Simply copy the console output to a file... + + - To enable/disable the reports: + - Open each DFIR related .hexpat + - Find the report constant (near the top) + - "true" = enabled + - "false" = disabled + + Example Report: GPT > FAT32|exFAT + - [exFAT_Report](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns/blob/main/reports/exFAT_Report.txt) + + Example Report: MBR > 5 Logical Volumes (2 in an Extended) > All FAT32 Volumes + - [MBR_5_VOLs](https://github.com/Xtreme-Liberty/ImHex-DFIR-Patterns/blob/main/reports/MBR_5_VOLs.txt) + + diff --git a/patterns/DFIR/DISK_PARSER.hexpat b/patterns/DFIR/DISK_PARSER.hexpat new file mode 100644 index 00000000..aa2222fe --- /dev/null +++ b/patterns/DFIR/DISK_PARSER.hexpat @@ -0,0 +1,677 @@ +#pragma author Formula Zero One Technologies +#pragma description DFIR_DISK_PARSER_v2.0 +#pragma MIME application/x-ima +#pragma endian little + + +// ----------------------------------------------------------------------------- +// CREDIT +// ----------------------------------------------------------------------------- +// Based on /fs/pattern.hexpat by WerWolv +// ----------------------------------------------------------------------------- +// TODO +// ----------------------------------------------------------------------------- +// Refine File System Detection/Match +// ----------------------------------------------------------------------------- +// IMPORTS +// ----------------------------------------------------------------------------- +import std.core; +import std.io; +import std.time; +import type.guid; +import type.magic; +import type.time; +import type.base; +import hex.provider; + +// WORKING IMPORTS +import * from DFIR.FAT32 as FAT32Pat; +import * from DFIR.exFAT as EXFATPat; +import * from DFIR.NTFS as NTFSPat; + +// ------------------------------------ +// DISABLED IMPORTS +// REFS - UNTESTED +// EXT4 - GROUP DESC ERRORS +// APFS - PARTIALLY WORKS + // Comment out "using uuid_t = type::GUID" + // Replace all instances of "uuid_t" with "type::GUID" + // Comment out line 1456-EOF +// JPEG/PNG - OFFSET ERRORS +// ------------------------------------ +//import * from fs.apfs as APFSPat; +//import * from fs.ext4 as EXT4Pat; +//import * from fs.refs as REFSPat; +//import * from jpeg as JPEGPat; +//import * from png as PNGPat; + +// ----------------------------------------------------------------------------- +// FWD DECs - GLOBAL +// ----------------------------------------------------------------------------- +bool has_ext = false; +bool has_gpt = false; +u64 partitionOffset = 0; +u64 containerStartOffset = 0; + +u32 mptIndex = 0; +u32 extIndex = 0; +str entryName = ""; + +u32 MPT_Count = 0; +u32 EXT_VolCount = 0; +u32 GPT_Count = 0; + +u32 memory_size = std::mem::size(); +str disk_path = hex::prv::get_information("file_path",""); +u128 sector_size = hex::prv::get_information("sector_size",""); + +// ----------------------------------------------------------------------------- +// REPORT HEADER ** ATTENTION ** +// ----------------------------------------------------------------------------- + +// ---******---*******---vvvv--- | +const bool DISK_REPORT = true; +// ---******---*******---^^^^--- | + +if (DISK_REPORT) { + std::print(" # # # # # # "); + std::print(" # # # "); + std::print(" # # # "); + std::print(" # # # # # # # "); + std::print(" I m H e x "); + std::print(" "); + std::print("-----------------------------------------"); + std::print(" "); + std::print(" ENTITY: _____________________"); + std::print(" "); + std::print("EXAMINER: _____________________"); + std::print(" "); + u128 timestamp = std::time::epoch(); + std::time::Time local_ts = std::time::to_local(timestamp); + std::time::Time utc_ts = std::time::to_utc(timestamp); + std::print("-----------------------------------------"); + std::print(" LOCAL: {}", + std::format("{:02}/{:02}/{:04} @ {:02}:{:02}:{:02}", + local_ts.mon + 1, + local_ts.mday, + local_ts.year + 1900, + local_ts.hour, + local_ts.min, + local_ts.sec + )); + std::print(" UTC: {}", + std::format("{:02}/{:02}/{:04} @ {:02}:{:02}:{:02}", + utc_ts.mon + 1, + utc_ts.mday, + utc_ts.year + 1900, + utc_ts.hour, + utc_ts.min, + utc_ts.sec + )); + std::print("-----------------------------------------"); + std::print(" "); +} + +// ----------------------------------------------------------------------------- +// SIGNATURE HELPER +// ----------------------------------------------------------------------------- +enum MBRSignature : u16 { + MBR_SIG = 0xAA55 // 0x55AA -> Read LE +}; + +// ----------------------------------------------------------------------------- +// CHS HELPER +// ----------------------------------------------------------------------------- +bitfield CHS_Decoder { + head : 8; + sector : 6; + cylinder : 10; +} [[format("chs_formatter")]]; + +fn chs_formatter(CHS_Decoder CHS) { + return std::format("({:X}, {:X}, {:X}) | 0x{:X}", CHS.cylinder, CHS.head, CHS.sector, (CHS.cylinder * 16 + CHS.head) * 63 + (CHS.sector - 1)); +}; + +// ----------------------------------------------------------------------------- +// TIMESTAMP HELPER +// ----------------------------------------------------------------------------- +struct DiskTimeStamp { + u8 seconds, minutes, hours; +}; + +// ----------------------------------------------------------------------------- +// DISK PROTECTION HELPER +// ----------------------------------------------------------------------------- +enum DiskProtection : u16 { + NotProtected = 0x0000, + CopyProtected = 0x5A5A +}; + +// ----------------------------------------------------------------------------- +// PARTITION STATUS HELPER +// ----------------------------------------------------------------------------- +enum PartitionStatus : u8 { + Not_Active = 0x00, // not_bootable + Active = 0x80 // bootable +}; + +enum MPTPartLabel : u8 { + UNUSED_OR_HIDDEN_ENTRY = 0x00, + PRIMARY = 0x07, + PRIMARY_F32_SMALL = 0x0B, + PRIMARY_0C_BIG = 0x0C, + EXTENDED_CONT_SMALL = 0x05, + EXTENDED_CONT_BIG = 0x0F, + LEGACY_MBR = 0xEE +}; + +// ----------------------------------------------------------------------------- +// PARTITION TYPE HELPER +// ----------------------------------------------------------------------------- +enum PartitionTypeCode : u8 { + UNUSED_ENTRY = 0x00, + FAT12_HDD = 0x01, + FAT12_HIDDEN = 0x11, + FAT16_SMALL = 0x04, + FAT16_SMALL_HIDDEN = 0x14, + FAT16_BIG = 0x06, + FAT16_BIG_HIDDEN = 0x16, + FAT32_SMALL = 0x0B, + FAT32_SMALL_HIDDEN = 0x1B, + FAT32_BIG = 0x0C, + FAT32_BIG_HIDDEN = 0x1C, + EXT_PART_SMALL = 0x05, + EXT_PART_SMALL_HIDDEN = 0x15, + EXT_PART_BIG = 0x0F, + EXT_PART_BIG_HIDDEN = 0x1F, + NTFS_EXFAT = 0x07, + NTFS_EXFAT_HIDDEN = 0x17, + WINDOWS_RECOVERY = 0x27, + NTFS_VOL_SET_1 = 0x86, + NTFS_VOL_SET_2 = 0x87, + macOSX = 0xA8, + OS2_HIDDEN_CDRIVE = 0x84, + LINUX_EXT = 0x83, + LINUX_EXT2 = 0x85, + LINUX_LVM = 0x8E, + LINUX_PA_RISC = 0xF0, + LINUX_RAID = 0xFD, + FREE_BSD = 0xA5, + OPEN_BSD = 0xA6, + QNX_1 = 0x4D, + QNX_2 = 0x4E, + QNX_3 = 0x4F, + GPT_DISK_STD = 0xEE, + GPT_DISK_SYS = 0xEF, + UNKNOWN = 0xFF, +}; + +// ----------------------------------------------------------------------------- +// GUID PARTITION TABLE (GPT) PARTIONING SCHEME RELATED +// ----------------------------------------------------------------------------- +// V V V V V V V V V V +// ----------------------------------------------------------------------------- +// GPT PARTITION LABEL HELPER +// ----------------------------------------------------------------------------- +enum GUIDPartLabel : u128 { + // ---------------- COMMON ---------------- + UNUSED_ENTRY = 0x00000000000000000000000000000000, + EFI_SYSTEM_PART = 0x3BC93EC9A0004BBA11D2F81FC12A7328, + APPLE_APFS_CONT = 0xACEC4365300011AA11AA00007C3457EF, + APPLE_HFS_PLUS_PART = 0xACEC4365300011AA11AA000048465300, + MICROSOFT_RESERVED_PART = 0xAE1502F92DF97D81B84D5C0BE3E3C9E3, + WINDOWS_REC_ENVIRONMENT = 0xACD67901D5BF6AA1404DD106A4BB94DE, + BASIC_DATA_PART = 0xC79926B7B668C0874433B9E5EBD0A0A2, + + // ---------------- LINUX ---------------- + LINUX_FILE_SYSTEM = 0xE47D47D8693D798E477284830FC63DAF, + RAID_PART = 0x1E91840F3F7406A04D3B05FCA19D880F, + ROOT_PART_X86 = 0x8A45F0D531D1F79A41B2F29744479540, + ROOT_PART_X86_64 = 0x09B784F9CAFBE7964DB1E8CD4F68BCE3, + ROOT_PART_ARM = 0xD3BE9AD4A1216CB14E3C2CE469DAD710, + ROOT_PART_ARM_64 = 0xAE3F0D286F4C44AF41C31DF0B921B045, + BOOT_PART = 0x72716FFD75B252A3426259E6BC13C2FF, + SWAP_PART = 0x4F4F4BC83309E58443C4A4AB0657FD6D, + LOGICAL_VOLUME_MGR_PART = 0x28F93D2A8F233CA244C2F507E6D6D379, + HOME_PART = 0x15F9AEE2140E44B84F132EB4933AC7E1, + SRV_SERVER_DATA_PART = 0xE8986FA7251A7F904F3B20E03B8F8425, + PLAIN_DMCRYPT_PART = 0xB786550AA13E418949B72D007FFEC5C9, + LUKS_PART = 0xCC59605342171C864C5363EDCA7D7CCB, + + // ---------------- APPLE ---------------- + APPLE_UFS_CONT = 0xACEC4365300011AA11AA000055465300, + APPLE_ZFS = 0x316673200008A69911B21DD26A898CC3, + APPLE_RAID_PART = 0xACEC4365300011AA11AA000052414944, + APPLE_RAID_PART_OFFLINE = 0xACEC4365300011AA11AA5F4F52414944, + APPLE_BOOT_PART_REC_HD = 0xACEC4365300011AA11AA0000426F6F74, + APPLE_LABEL = 0xACEC4365300011AA11AA6C004C616265, + APPLE_TV_RECOVERY_PART = 0xACEC4365300011AA11AA76655265636F, + APPLE_CORE_STORAGE_CONT = 0xACEC4365300011AA11AA616753746F72, + HFS_FILEVAULT_VOLUME_CONT = 0xACEC4365300011AA11AA616753746F72, + APPLE_APFS_PREBOOT_PART = 0xACEC4365300011AA11AA006769646961, + APPLE_APFS_RECOVERY_PART = 0xACEC4365300011AA11AA007972637652, + + // ---------------- WINDOWS ---------------- + LOGICAL_DISK_MGR_META_PART = 0xB3CF34E104E1D28542E08F7EAAC80858, + LOGICAL_DISK_MGR_DATA_PART = 0xAD694A71113368BC4F621431A0609BAF, + IBM_GENERAL_PARALLEL_FILE_SYS_PART = 0x74B155E07A2DC3914E4EEF7D90FFAA37, + STORAGE_SPACES_PART = 0x2DECF6E501B0A3AFEE4CF6808FAF5CE7, + STORAGE_REPLICA_PART = 0xD123292BD147C8AAC043A1ACC58D4355, +}; +// ----------------------------------------------------------------------------- +// BASIC DATA PARTITION ATTRIBUTES +// ----------------------------------------------------------------------------- +bitfield GPT_BDP_Attributes { + bool platform_required : 1 [[comment("Bit 0: RequiredPartition - Volume must be preserved")]]; + bool io_ignore : 1 [[comment("Bit 1: NoBlockIOProtocol - EFI ignores this Volume, no FS Mapping")]]; + bool legacy_flag : 1 [[comment("Bit 2: LegacyBIOSBootable - Active/Bootable under BIOS")]]; + reserved_UEFI : 45 [[comment("Bits 3–47: Reserved for UEFI")]]; + reserved_MS : 12 [[comment("Bits 48–59: Reserved for Microsoft")]]; + bool read_only : 1 [[comment("Bit 60: BasicDataPart - Read-Only Volume")]]; + bool shadow_copy : 1 [[comment("Bit 61: BasicDataPart - Shadow Copy Volume")]]; + bool hidden : 1 [[comment("Bit 62: BasicDataPart - Hidden Volume")]]; + bool no_drive_letter : 1 [[comment("Bit 63: BasicDataPart - Do not Auto-Assign Drive Letter")]]; +} [[bitfield_order( + std::core::BitfieldOrder::LeastToMostSignificant, 64)]]; + +// ----------------------------------------------------------------------------- +// GPT ENTRIES PARSER +// LBA2-LBA33 +// EACH ENTRY IS 128 BYTES (DESCRIBES A VOLUME) +// ----------------------------------------------------------------------------- +union PartitionUnion { + le type::GUID PartTypeGUID; // HUMAN READABLE GUID + GUIDPartLabel PartTypeLabel [[name(std::format("PartTypeLabel (DERIVED)"))]]; // OBJECT LABEL +}; + +struct GPT_PartitionEntry { + PartitionUnion Type [[comment("Known Partition Type GUID: Global Identifier")]]; + le type::GUID Unique_GUID [[comment("Unique Partition GUID: Every Volume has its own Unique GUID")]]; + u64 Start_LBA [[comment("The first Sector of the Volume (Offset by 1)")]]; + u64 End_LBA [[comment("The last Sector of the Volume (Offset by 1)")]]; + GPT_BDP_Attributes ATTR [[comment("ATTRs for a Basic Data Partition may not be the same as a Microsoft Reserved Partition")]]; + char16 PartName[36] [[comment("Partition Name: Based on Known Partition Type GUID, except for Disk Images")]]; + + if (Type.PartTypeLabel != GUIDPartLabel::UNUSED_ENTRY) { + GPT_Count += 1; + } + + u64 GPTpartitionOffset = Start_LBA * sector_size + [[name(std::format("VOL_OFFSET {} | 0x{:02X} (DERIVED)", Start_LBA * sector_size, Start_LBA * sector_size)), + export]]; + + match (Type.PartTypeLabel) { + (GUIDPartLabel::UNUSED_ENTRY): + continue; + + (GUIDPartLabel::EFI_SYSTEM_PART): + FAT32Pat EFI_SYS_VOL @ GPTpartitionOffset; + + (GUIDPartLabel::BASIC_DATA_PART | + GUIDPartLabel::WINDOWS_REC_ENVIRONMENT): { + char gpt_fat32_magic[8] @ GPTpartitionOffset + 82 [[hidden]]; + char gpt_ntfs_magic[8] @ GPTpartitionOffset + 3 [[hidden]]; + char gpt_exfat_magic[8] @ GPTpartitionOffset + 3 [[hidden]]; + + if (gpt_fat32_magic == "FAT32 ") + FAT32Pat FAT32_VOL @ GPTpartitionOffset; + if (gpt_ntfs_magic == "NTFS ") + NTFSPat NTFS_VOL @ GPTpartitionOffset; + else if (gpt_exfat_magic == "EXFAT ") + EXFATPat EXFAT_VOL @ GPTpartitionOffset; + } + // --------- DISABLED ----------------- + // EXT4 PATTERN WAS INOP WHEN TESTED + //(GUIDPartLabel::LINUX_FILE_SYSTEM): + //EXT4Pat EXT4_VOL @ GPTpartitionOffset; + //(GUIDPartLabel::APPLE_APFS_CONT): + // APFSPat APFS_VOL @ GPTpartitionOffset; + } +} [[name(std::format("GPT_ENTRY [{}]", std::core::array_index()))]]; + +// ----------------------------------------------------------------------------- +// GPT HEADER PARSER +// LBA1 OFFSETS 0-91 (92 bytes of 512 bytes used) +// ----------------------------------------------------------------------------- +struct GPT_Header { + type::Magic<"EFI PART"> signature [[comment("Signature (EFI PART)")]]; + u32 revision [[comment("Header Revision Value")]]; + u32 header_size [[comment("Size of Header - 92 Bytes")]]; + type::Hex header_crc32 [[comment("GPT Header Checksum")]]; + u32 reserved [[comment("Zeros")]]; + u64 current_lba [[comment("Current LBA - GPT Header Location")]]; + u64 backup_lba [[comment("Location of Backup - Header & GPT")]]; + u64 first_usable_lba [[comment("1st Sector Available for Logical VOL")]]; + u64 last_usable_lba [[comment("Last Sector Available for Logical VOL")]]; + type::GUID disk_guid [[comment("Unique Disk GUID")]]; + u64 partition_entries_lba [[comment("1st Sector of GPT")]]; + u32 num_partition_entries [[comment("Total Number of Partition Entries Available - 128 on Windows")]]; + u32 size_of_partition_entry [[comment("Size in Bytes of each GPT Entry")]]; + type::Hex partition_entries_crc32 [[comment("GPT Array Checksum")]]; +}; + +// ----------------------------------------------------------------------------- +// MASTER BOOT RECORD (MBR) PARTIONING SCHEME RELATED +// ----------------------------------------------------------------------------- +// V V V V V V V V V V +// ----------------------------------------------------------------------------- +// MASTER PARTITION TABLE (MPT) +// LBA0 > 0FFSETS 446-509 +// Each Entry Describes a Logical Volume (type/start_loc/size) +// ----------------------------------------------------------------------------- +union MBRPartitionUnion { + PartitionTypeCode Part_Type; + MPTPartLabel PartTypeLabel; // overlay for 0x00 +}; + +struct PartitionTableEntry { + // partition table fields + PartitionStatus ActiveFlag; + CHS_Decoder Starting_CHS; + MBRPartitionUnion Type; + CHS_Decoder Ending_CHS; + u32 Start_LBA; + u32 Total_Sectors; + + if (Type.PartTypeLabel != MPTPartLabel::UNUSED_OR_HIDDEN_ENTRY) { + // Track Count of Logical Volumes in the Extended Container + //MPT_Count += 1; + if (containerStartOffset == 0) { + // top-level MBR entry + MPT_Count = MPT_Count + 1; + } else { + // a logical inside an extended container + EXT_VolCount = EXT_VolCount + 1; + } + } + + partitionOffset = containerStartOffset + (Start_LBA * sector_size); + + match (Type.PartTypeLabel) { + (PartitionTypeCode::UNUSED_ENTRY): continue; + (PartitionTypeCode::FAT32_SMALL | PartitionTypeCode::FAT32_BIG): { + FAT32Pat FAT32_VOL @ partitionOffset; + } + (PartitionTypeCode::NTFS_EXFAT): { + char magic[8] @ partitionOffset + 3; + if (magic == "NTFS ") + NTFSPat NTFS_VOL @ partitionOffset; + else + EXFATPat EXFAT_VOL @ partitionOffset; + } + (PartitionTypeCode::EXT_PART_SMALL | PartitionTypeCode::EXT_PART_BIG): { + // Save parent state + bool parent_has_ext = has_ext; + has_ext = true; + + containerStartOffset = partitionOffset; + + // Parse first two entries of the extended partition + PartitionTableEntry EXTENDED_PARTITION[2] @ partitionOffset + 446; + + has_ext = parent_has_ext; + } + (PartitionTypeCode::GPT_DISK_STD | PartitionTypeCode::GPT_DISK_SYS): + // Set global flag + has_gpt = true; + } + if (!has_ext) { + entryName = std::format("MPT_ENTRY [{}]", mptIndex); + mptIndex += 1; + } else { + if (std::core::array_index() <= 0) { + entryName = std::format("LOGICAL_VOL (EXT) [{}]", extIndex); + } else if (std::core::array_index() == 1) { + entryName = "NEXT VOL POINTER (EXT)"; + } else { + entryName = std::format("LOGICAL_VOL (EXT) [{}]", extIndex); + } + extIndex += 1; + } +} [[name(entryName)]]; + +// ----------------------------------------------------------------------------- +// MBR PARSER +// LBA0 > OFFSETS 0-511 (512 bytes) +// ----------------------------------------------------------------------------- +struct MasterBootRecord { + u8 bootstrapCodeArea1[218] [[comment("Boot Strapping Code")]]; + padding[2] [[comment("Zeros")]]; + u8 originalPhysicalDrive [[comment("???")]]; + DiskTimeStamp diskTimeStamp [[comment("Timestamp of Disk OG Partitioning")]]; + u8 bootstrapCodeArea2[216] [[comment("Boot Strapping Code")]]; + u32 diskSignature [[comment("Disk Signature")]]; + DiskProtection diskProtection [[comment("Disk Protection - 0x0000=Not | 0x5A5A=Prot")]]; + PartitionTableEntry PT[4] [[comment("Master Partition Table (MPT) Offset 446-509")]]; + MBRSignature MBR_SIG [[comment("End of MBR - 0x55AA")]]; +}; + +// ----------------------------------------------------------------------------- +// DISK PARSER +// ----------------------------------------------------------------------------- +struct DiskRoot { + // Master Boot Record at LBA 0 (1st physical sector) + MasterBootRecord MBR @ 0x00; + + if (has_gpt) { + // GPT Header at LBA 1 (2nd physical sector) + GPT_Header GPT_HEADER @ 0x200; + // The GPT (table) at LBA 2 (3rd physical sector) to LBA 33 (34th physical sector) + // 32 sectors total (Windows) that can define up to 128 - (primary) logical volumes + GPT_PartitionEntry GPT_ENTRIES[GPT_HEADER.num_partition_entries] @ (GPT_HEADER.partition_entries_lba * 512); + } +}; + +// ----------------------------------------------------------------------------- +// ROOT OBJECT +// ----------------------------------------------------------------------------- +// --- +DiskRoot DISK @ 0x0; +// --- + + +// ------------------------------ +// DISK REPORT +// ------------------------------ + +if (DISK_REPORT) { + std::print("-----------------------------------------"); + std::print("-------------- DISK_REPORT --------------"); + std::print("-----------------------------------------"); + + // Disk Basics + std::print("DISK_PATH = {}", disk_path); + std::print("SECTOR_SIZE = {} BYTES", sector_size); + std::print("DISK_SIZE = {} SECTORS", memory_size / sector_size); + std::print("DISK_SIZE = {:.4f} GB @ 1000", memory_size / 1000.0 / 1000.0 / 1000.0); + std::print("DISK_SIZE = {:.4f} GiB @ 1024", memory_size / 1024.0 / 1024.0 / 1024.0); + + // Disk Protection + str diskProtectionStr; + if (DISK.MBR.diskProtection == DiskProtection::NotProtected) { + diskProtectionStr = "NOT_COPY_PROTECTED"; + } else if (DISK.MBR.diskProtection == DiskProtection::CopyProtected) { + diskProtectionStr = "COPY_PROTECTED"; + } else { + diskProtectionStr = "UNKNOWN"; + } + std::print("DISK_PROTECT = {}", diskProtectionStr); + + // Partition Scheme + if (MPT_Count >= 1 && GPT_Count == 0) { + std::print("PART_SCHEME = MBR"); + } else if (GPT_Count >= 1 && MPT_Count == 0) { + std::print("PART_SCHEME = GPT"); + } else if (GPT_Count >= 1 && MPT_Count >= 1) { + std::print("PART_SCHEME = HYBRID (MBR + GPT)"); + } else { + std::print("PART_SCHEME = UNKNOWN"); + } + + // MBR MPT Partitions + for (u32 i = 0, i < MPT_Count, i = i + 1) { + std::print("-----------------------------------------"); + std::print("-------------- MBR_MPT [{}] --------------", i); + std::print("-----------------------------------------"); + + // STATUS + str statusStr; + if (DISK.MBR.PT[i].ActiveFlag == PartitionStatus::Active) { + statusStr = "ACTIVE/BOOTABLE"; + } else if (DISK.MBR.PT[i].ActiveFlag == PartitionStatus::Not_Active) { + statusStr = "INACTIVE/NOT_BOOTABLE"; + } else { + statusStr = "UNKNOWN"; + } + std::print(" STATUS = {}", statusStr); + + // TYPE_CODE + str typeStr; + if (DISK.MBR.PT[i].Type.Part_Type == PartitionTypeCode::FAT32_SMALL) { + typeStr = "FAT32 (CHS) (0x0B)"; + } else if (DISK.MBR.PT[i].Type.Part_Type == PartitionTypeCode::FAT32_BIG) { + typeStr = "FAT32 (LBA) (0x0C)"; + } else if (DISK.MBR.PT[i].Type.Part_Type == PartitionTypeCode::NTFS_EXFAT) { + typeStr = "NTFS/EXFAT (0x07)"; + } else if (DISK.MBR.PT[i].Type.Part_Type == PartitionTypeCode::GPT_DISK_STD) { + typeStr = "GPT_PROTECTIVE (0xEE)"; + } else if (DISK.MBR.PT[i].Type.Part_Type == PartitionTypeCode::EXT_PART_BIG) { + typeStr = "EXTENDED (0x0F)"; + } else { + typeStr = "OTHER/UNKNOWN"; + } + std::print(" TYPE_CODE = {}", typeStr); + + // LBA and size + std::print(" FIRST_LBA = {:02}", DISK.MBR.PT[i].Start_LBA); + std::print(" LAST_LBA = {:02}", DISK.MBR.PT[i].Start_LBA + DISK.MBR.PT[i].Total_Sectors - 1); + std::print(" VOL_SIZE = {:02} SECTORS", DISK.MBR.PT[i].Total_Sectors); + std::print(" VOL_SIZE = {:.4f} GB", (DISK.MBR.PT[i].Total_Sectors * sector_size) / 1000.0 / 1000.0 / 1000.0); + std::print(" VOL_SIZE = {:.4f} GiB", (DISK.MBR.PT[i].Total_Sectors * sector_size) / 1024.0 / 1024.0 / 1024.0); + + if (DISK.MBR.PT[i].Type.PartTypeLabel == MPTPartLabel::EXTENDED_CONT_SMALL || + DISK.MBR.PT[i].Type.PartTypeLabel == MPTPartLabel::EXTENDED_CONT_BIG) { + + u32 logicalCount = std::core::member_count(DISK.MBR.PT[i].EXTENDED_PARTITION); + //u32 logicalCount = std::mem::size(DISK.MBR.PT[i].EXTENDED_PARTITION); + + for (u32 e = 0, e < logicalCount, e = e + 1) { + if (DISK.MBR.PT[i].EXTENDED_PARTITION[e].Type.PartTypeLabel == MPTPartLabel::UNUSED_OR_HIDDEN_ENTRY) + continue; + + std::print("-----------------------------------------"); + std::print("---------- LOGICAL (EXT) [{}] ------------", e); + std::print("-----------------------------------------"); + + // STATUS + str EXTstatusStr; + if (DISK.MBR.PT[i].EXTENDED_PARTITION[e].ActiveFlag == PartitionStatus::Active) { + EXTstatusStr = "ACTIVE/BOOTABLE"; + } else if (DISK.MBR.PT[i].EXTENDED_PARTITION[e].ActiveFlag == PartitionStatus::Not_Active) { + EXTstatusStr = "INACTIVE/NOT_BOOTABLE"; + } else { + EXTstatusStr = "UNKNOWN"; + } + std::print(" STATUS = {}", EXTstatusStr); + + // TYPE_CODE + str EXTtypeStr; + if (DISK.MBR.PT[i].EXTENDED_PARTITION[e].Type.Part_Type == PartitionTypeCode::FAT32_SMALL) { + EXTtypeStr = "FAT32 (CHS) (0x0B)"; + } else if (DISK.MBR.PT[i].EXTENDED_PARTITION[e].Type.Part_Type == PartitionTypeCode::FAT32_BIG) { + EXTtypeStr = "FAT32 (LBA) (0x0C)"; + } else if (DISK.MBR.PT[i].EXTENDED_PARTITION[e].Type.Part_Type == PartitionTypeCode::NTFS_EXFAT) { + EXTtypeStr = "NTFS/EXFAT (0x07)"; + } else if (DISK.MBR.PT[i].EXTENDED_PARTITION[e].Type.Part_Type == PartitionTypeCode::GPT_DISK_STD) { + EXTtypeStr = "GPT_PROTECTIVE (0xEE)"; + } else if (DISK.MBR.PT[i].EXTENDED_PARTITION[e].Type.Part_Type == PartitionTypeCode::EXT_PART_BIG) { + EXTtypeStr = "EXTENDED (0x0F)"; + } else { + EXTtypeStr = "OTHER/UNKNOWN"; + } + std::print(" TYPE_CODE = {}", EXTtypeStr); + + std::print(" FIRST_LBA = {}", DISK.MBR.PT[i].EXTENDED_PARTITION[e].Start_LBA); + std::print(" LAST_LBA = {}", DISK.MBR.PT[i].EXTENDED_PARTITION[e].Start_LBA + + DISK.MBR.PT[i].EXTENDED_PARTITION[e].Total_Sectors - 1); + std::print(" VOL_SIZE = {} SECTORS", DISK.MBR.PT[i].EXTENDED_PARTITION[e].Total_Sectors); + std::print(" VOL_SIZE = {:.4f} GB", + (DISK.MBR.PT[i].EXTENDED_PARTITION[e].Total_Sectors * sector_size) / 1000.0 / 1000.0 / 1000.0); + std::print(" VOL_SIZE = {:.4f} GiB", + (DISK.MBR.PT[i].EXTENDED_PARTITION[e].Total_Sectors * sector_size) / 1024.0 / 1024.0 / 1024.0); + } + } + } + + // GPT Header + if (GPT_Count >= 1) { + std::print("-----------------------------------------"); + std::print("-------------- GPT_HEADER ---------------"); + std::print("-----------------------------------------"); + std::print("SIGNATURE = {}", DISK.GPT_HEADER.signature); + std::print("REVISION = 0x{:02X}", DISK.GPT_HEADER.revision); + std::print("GPT_HDR_CRC = 0x{:02X}", DISK.GPT_HEADER.header_crc32); + std::print("GPT_HDR_BACKUP_LBA = {}", DISK.GPT_HEADER.backup_lba); + std::print("DISK_GUID = {}", DISK.GPT_HEADER.disk_guid); + std::print("FIRST_USABLE_LBA = {}", DISK.GPT_HEADER.first_usable_lba); + std::print("LAST_USABLE_LBA = {}", DISK.GPT_HEADER.last_usable_lba); + std::print("MAX_GPT_ENTRIES = {:02}", DISK.GPT_HEADER.num_partition_entries); + std::print("GPT_ENTRY_SIZE = {:02} BYTES", DISK.GPT_HEADER.size_of_partition_entry); + std::print("GPT_ARRAY_CRC = 0x{:02X}", DISK.GPT_HEADER.partition_entries_crc32); + + // GPT Partitions + for (u32 j = 0, j < GPT_Count, j = j + 1) { + std::print("-----------------------------------------"); + std::print("------------- GPT_PART [{}] --------------", j); + std::print("-----------------------------------------"); + std::print(" PART_TYPE_LABEL = {}", DISK.GPT_ENTRIES[j].Type.PartTypeLabel); + std::print(" PART_TYPE_GUID = {}", DISK.GPT_ENTRIES[j].Type.PartTypeGUID); + std::print(" UNIQUE_PART_GUID = {}", DISK.GPT_ENTRIES[j].Unique_GUID); + std::print(" FIRST_LBA = {:02}", DISK.GPT_ENTRIES[j].Start_LBA); + std::print(" LAST_LBA = {:02}", DISK.GPT_ENTRIES[j].End_LBA); + + bool _any = false; + std::print(" ATTR_FLAGS |"); + if(DISK.GPT_ENTRIES[j].ATTR.platform_required) { + std::print(" |- - - - > PlatformRequired"); + _any = true; + } + if(DISK.GPT_ENTRIES[j].ATTR.io_ignore) { + std::print(" |- - - - > NO_FS_MAP"); + _any = true; + } + if(DISK.GPT_ENTRIES[j].ATTR.legacy_flag) { + std::print(" |- - - - > LEGACY_BOOT"); + _any = true; + } + if(DISK.GPT_ENTRIES[j].Type.PartTypeLabel == GUIDPartLabel::BASIC_DATA_PART) { + if(DISK.GPT_ENTRIES[j].ATTR.read_only) { + std::print(" |- - - - > READ_ONLY"); + _any = true; + } + if(DISK.GPT_ENTRIES[j].ATTR.shadow_copy) { + std::print(" |- - - - > SHADOW_COPY"); + _any = true; + } + if(DISK.GPT_ENTRIES[j].ATTR.hidden) { + std::print(" |- - - - > HIDDEN"); + _any = true; + } + if(DISK.GPT_ENTRIES[j].ATTR.no_drive_letter) { + std::print(" |- - - - > NO_AUTO_MOUNT"); + _any = true; + } + } + // if nothing was printed, say "NONE" + if (!_any) { + //std::print(" |> NONE"); + std::print(" |- - - - > NONE"); + } + std::print(" PART_TYPE_NAME = {}", DISK.GPT_ENTRIES[j].PartName); + } + } + std::print("-----------------------------------------"); + std::print("------------------ END ------------------"); + std::print("-----------------------------------------"); + std::print(" "); +} diff --git a/patterns/DFIR/FAT32.hexpat b/patterns/DFIR/FAT32.hexpat new file mode 100644 index 00000000..c43b2797 --- /dev/null +++ b/patterns/DFIR/FAT32.hexpat @@ -0,0 +1,789 @@ +#pragma author Formula Zero One Technologies +#pragma description FAT32 File System (FAT32_v2.0) +#pragma MIME application/x-ima +#pragma endian little + + +// ----------------------------------------------------------------------------- +// CREDIT +// ----------------------------------------------------------------------------- +// OG AUTHOR: WerWolv +// OG DESC: fs/fat32.hexpat_v1.0 +// ----------------------------------------------------------------------------- +// NOTES FOR v2.0 ** GLOBALS NEED YOUR INPUT ** +// ----------------------------------------------------------------------------- +// Imported by DISK_PARSER.hexpat +// Added section separators for organization +// Added recursive parsing for Root Dir and a next level +// Added D/T conversions +// Show filenames on hover +// Added comments to DFIR fields of interest +// Changed pattern output naming/structure. +// Parse FAT1/FAT2 +// Show SFN <-> Starting Cluster Relation Overlay +// ----------------------------------------------------------------------------- +// TODO +// ----------------------------------------------------------------------------- +// Parse all SFN/LFN entries, not just Root + 1 +// ----------------------------------------------------------------------------- +// IMPORTS +// ----------------------------------------------------------------------------- +import std.core; +import std.io; +import std.mem; +import std.time; +import std.string; +import type.time; + +// ----------------------------------------------------------------------------- +// FORWARD DECS/GLOBALS +// ----------------------------------------------------------------------------- +// *** ATTENTION *** +// SET MAXIMUM NUMBER OF 4 BYTE CHUNKS TO PARSE FROM FAT1 +// DEFAULT IS 4096 +// Choose a value greater than 1 and less than 65536 OR increase the Array size limit with "#define... " + +// -------**************---vvvv--- | +const u64 MAX_FAT_CHUNKS = 4096; +// -------**************---^^^^--- | + +// *** ATTENTION *** +// SET MAXIMUM NUMBER OF SFN = STARTING CLUSTER TO PROCESS +// DEFAULT IS 100 (2 LEVELS DEEP | ROOT DIR + 1) +// Choose a value greater than 1 and less than 65536 OR increase the Array size limit with "#define... " + +// ---**************---************---vvv--- | +const u64 MAX_SFN_CLUSTER_RELATIONS = 100; +// ---**************---************---^^^--- | + +// ---*******---*******----vvvv--- | +const bool VOLUME_REPORT = true; +// ---*******---*******----^^^^--- | + +u64 bytesPerCluster = 0; +u64 rootDirSectors = 0; +u64 firstDataSector = 0; +u64 dataRegionStart = 0; +u64 sfn_count = 0; +u64 sfn_del_count = 0; +u64 lfn_count = 0; +u64 lfn_del_count = 0; +u64 start_index = 0; +u64 root_dir_start = 0; +u64 allocated_file_count = 0; + +u64 VBR_OFFSET = 0; +u64 FAT1_start_offset = 0; +u64 FAT2_start_offset = 0; +u64 FAT_ClusterHeap_Count = 0; + +u64 abs_FAT1_start_offset = 0; +u64 abs_FAT2_start_offset = 0; +u64 abs_rootDirStart_offset = 0; + +// ----------------------------------------------------------------------------- +// FILE ALLOCATION TABLE RELATED +// ----------------------------------------------------------------------------- +// V V V V V V V V V V +// ----------------------------------------------------------------------------- + +// ------------------------------ +// SFN <-> CLUSTER OVERLAY +// ------------------------------ +struct INFO_Overlay { + u64 index = std::core::array_index(); + u64 start_location = FAT1_start_offset + 8 + (index * 4); + u32 current_cluster = 2 + index; + str filename = overlay_func_name(current_cluster); + + if (filename != "") { + char hover_label[4] @ start_location [[ + name(std::format( + "SFN: {} | CLUSTER {}", + filename, + current_cluster + ))]]; + } +} [[inline]]; + +fn overlay_func_name(u32 cluster_num) { + str fname = ""; + str ext = ""; + str combo = ""; + + // Loop through all ROOT_DIR_ENTRIES + for (u32 i = 0, i < std::core::member_count(ROOT_DIR_ENTRIES), i = i + 1) { + + // Check SFN_ALIAS and SFN_ENTRY in root entries + if (std::core::has_member(ROOT_DIR_ENTRIES[i], "SFN_ALIAS")) { + if (ROOT_DIR_ENTRIES[i].SFN_ALIAS.first_cluster == cluster_num) { + combo = std::format("{}.{}", + ROOT_DIR_ENTRIES[i].SFN_ALIAS.fileName, + ROOT_DIR_ENTRIES[i].SFN_ALIAS.extension); + return combo; + } + } else if (std::core::has_member(ROOT_DIR_ENTRIES[i], "SFN_ENTRY")) { + if (ROOT_DIR_ENTRIES[i].SFN_ENTRY.first_cluster == cluster_num) { + combo = std::format("{}.{}", + ROOT_DIR_ENTRIES[i].SFN_ENTRY.fileName, + ROOT_DIR_ENTRIES[i].SFN_ENTRY.extension); + return combo; + } + } + + // Loop through all SUB_DIR_INDEX arrays for this root entry + if (std::core::has_member(ROOT_DIR_ENTRIES[i], "SUB_DIR_INDEX")) { + for (u32 j = 0, j < std::core::member_count(ROOT_DIR_ENTRIES[i].SUB_DIR_INDEX), j = j + 1) { + + if (std::core::has_member(ROOT_DIR_ENTRIES[i].SUB_DIR_INDEX[j], "SFN_ALIAS")) { + if (ROOT_DIR_ENTRIES[i].SUB_DIR_INDEX[j].SFN_ALIAS.first_cluster == cluster_num) { + combo = std::format("{}.{}", + ROOT_DIR_ENTRIES[i].SUB_DIR_INDEX[j].SFN_ALIAS.fileName, + ROOT_DIR_ENTRIES[i].SUB_DIR_INDEX[j].SFN_ALIAS.extension); + return combo; + } + + } else if (std::core::has_member(ROOT_DIR_ENTRIES[i].SUB_DIR_INDEX[j], "SFN_ENTRY")) { + if (ROOT_DIR_ENTRIES[i].SUB_DIR_INDEX[j].SFN_ENTRY.first_cluster == cluster_num) { + combo = std::format("{}.{}", + ROOT_DIR_ENTRIES[i].SUB_DIR_INDEX[j].SFN_ENTRY.fileName, + ROOT_DIR_ENTRIES[i].SUB_DIR_INDEX[j].SFN_ENTRY.extension); + return combo; + } + } + } + } + } + return ""; // no match found +}; + +// ----------------------------------------------------------------------------- +// FAT32 FILE ALLOCATION TABLE (FAT) PARSER +// ----------------------------------------------------------------------------- + +const u32 CLUSTER_SIZE_BYTES = 4; // Each FAT32 entry = 4 bytes +const u32 FAT32_EOF = 0x0FFFFFFF; // End-of-file marker +const u32 FAT32_BAD = 0x0FFFFFF7; // Bad cluster marker +const u32 FIRST_ALLOC_CLUSTER = 2; // First usable cluster after reserved + +enum FAT_Flags : u32 { + UNALLOCATED = 0x00000000, + END_OF_FILE = 0x0FFFFFFF, // L.END + BAD_CLUSTER = 0xFFFFFFF7, // L.END + //POINTER = Num >= 1 // INFO +}; + +union FAT_Union { + u32 DECIMAL [[hidden]]; + FAT_Flags FAT_FLAG; +}; + +// ------------------------------ +// Helper function for pointer label +// ------------------------------ +fn cluster_label(u32 val) { + if (val == FAT_Flags::UNALLOCATED) + return "UNALLOCATED"; + if (val == FAT_Flags::BAD_CLUSTER) + return "BAD"; + if (val >= 0x0FFFFFF8) + return "EOF"; + return std::format("{}", val); +}; + +// ------------------------------ +// FAT1/FAT2 HEAPS/CHAINS +// ------------------------------ +struct FAT_Entry { + FAT_Union FAT [[inline]]; + + u32 cluster_num = (FIRST_ALLOC_CLUSTER) + (std::core::array_index()); + + u32 next_cluster = FAT.DECIMAL & 0x0FFFFFFF; + + char hover_label[4] @ $ - 4 [[ + name(std::format( + "Cluster: {} → {}", + cluster_num, + cluster_label(next_cluster) + )) + ]]; + + bool is_eof = next_cluster >= 0x0FFFFFF8; + bool is_bad = next_cluster == FAT32_BAD; + bool is_free = next_cluster == 0; + + if (is_eof) { + allocated_file_count += 1; + } +} [[name(format_fat_entry(FAT.DECIMAL, std::core::array_index(), FIRST_ALLOC_CLUSTER))]]; + +// ------------------------------ +// FAT FORMATTER FUNC +// ------------------------------ +fn format_fat_entry(u32 raw_value, u32 cluster_index, u32 first_alloc_cluster) { + u32 next_cluster = raw_value & 0x0FFFFFFF; + + str next_label; + + if (next_cluster == 0) + next_label = "UNALLOCATED"; + + else if (next_cluster == FAT32_BAD) + next_label = "BAD"; + + else if (next_cluster == 0x0FFFFFFF) + next_label = "EOF"; + + else + next_label = std::format("{}", next_cluster); + + u32 logical_cluster = first_alloc_cluster + cluster_index; + + if (next_label == "UNALLOCATED" || next_label == "BAD" || next_label == "EOF") + return std::format("Cluster {}: {}", logical_cluster, next_label); + else + return std::format("Cluster {} → {}", logical_cluster, next_label); +}; + +// ------------------------------ +// MEDIA DESCRIPTOR HELPER +// ------------------------------ +enum Media_Descriptor : u8 { + SINGLE_SIDE_FLOPPY = 0xF0, + DOUBLE_SIDE_FLOPPY = 0xF9, + HARD_DISK_DRIVE = 0xF8, +}; + +// ------------------------------ +// FAT1/FAT2 HEADER PARSER +// ------------------------------ +struct FAT_Header { + Media_Descriptor mediaDescriptor [[comment("0xF8=FIXED DISK | 0xF0=REMOVABLE")]];; + u8 FAT32_FAT_HEADER[7] [[comment("8 BYTES TOTAL: 4 BYTES REPRESENT PSUEDO CLUSTER 0 (SYSTEM) | 4 BYTES REPRESENT PSUEDO CLUSTER 1 (SYSTEM)(EOF)")]]; + char root_dir_label[4] @ $ [[ + name(std::format( + "ROOT_DIRECTORY" + )) + ]]; + // WHICH IS WHY THE ROOT DIRECTORY (FIRST DATA AREA ITEM) STARTS IN CLUSTER 2 +}; + +// ----------------------------------------------------------------------------- +// ROOT DIRECTORY RELATED +// ----------------------------------------------------------------------------- +// V V V V V V V V V V +// ----------------------------------------------------------------------------- +// ------------------------------ +// ACTIVE LFN SEQUENCE NUMBER BITFIELD +// * EXCEPT DELETED ENTRIES - 0xE5 * +// ------------------------------ +bitfield LFN_Sequence { + padding : 1; + IS_LAST_ENTRY : 1 [[name("IS_LAST_ENTRY: [0=NO | 1=YES] ==")]]; + padding : 1; + LFN_SEQ_NUM : 5; +} [[bitfield_order( + std::core::BitfieldOrder::MostToLeastSignificant, 8)]]; + +// ------------------------------ +// DIRECTORY ENTRY STATUS/SEQUENCE HELPERS +// ------------------------------ +enum Entry_Status : u8 { + EMPTY_ENTRY = 0x00, + DOT_ENTRY = 0x2E, + DELETED_ENTRY = 0xE5, + + ACTIVE_1ST_ENTRY = 0x01, + ACTIVE_2ND_ENTRY = 0x02, + ACTIVE_3RD_ENTRY = 0x03, + ACTIVE_4TH_ENTRY = 0x04, + ACTIVE_5TH_ENTRY = 0x05, + ACTIVE_6TH_ENTRY = 0x06, + ACTIVE_7TH_ENTRY = 0x07, + ACTIVE_8TH_ENTRY = 0x08, + ACTIVE_9TH_ENTRY = 0x09, + ACTIVE_10TH_ENTRY = 0x0A, + ACTIVE_11TH_ENTRY = 0x0B, + ACTIVE_12TH_ENTRY = 0x0C, + ACTIVE_13TH_ENTRY = 0x0D, + ACTIVE_14TH_ENTRY = 0x0E, + ACTIVE_15TH_ENTRY = 0x0F, + ACTIVE_16TH_ENTRY = 0x10, + ACTIVE_17TH_ENTRY = 0x11, + ACTIVE_18TH_ENTRY = 0x12, + ACTIVE_19TH_ENTRY = 0x13, + ACTIVE_20TH_ENTRY = 0x14, + ACTIVE_1ST_ENTRY_LAST = 0x41, + ACTIVE_2ND_ENTRY_LAST = 0x42, + ACTIVE_3RD_ENTRY_LAST = 0x43, + ACTIVE_4TH_ENTRY_LAST = 0x44, + ACTIVE_5TH_ENTRY_LAST = 0x45, + ACTIVE_6TH_ENTRY_LAST = 0x46, + ACTIVE_7TH_ENTRY_LAST = 0x47, + ACTIVE_8TH_ENTRY_LAST = 0x48, + ACTIVE_9TH_ENTRY_LAST = 0x49, + ACTIVE_10TH_ENTRY_LAST = 0x4A, + ACTIVE_11TH_ENTRY_LAST = 0x4B, + ACTIVE_12TH_ENTRY_LAST = 0x4C, + ACTIVE_13TH_ENTRY_LAST = 0x4D, + ACTIVE_14TH_ENTRY_LAST = 0x4E, + ACTIVE_15TH_ENTRY_LAST = 0x4F, + ACTIVE_16TH_ENTRY_LAST = 0x50, + ACTIVE_17TH_ENTRY_LAST = 0x51, + ACTIVE_18TH_ENTRY_LAST = 0x52, + ACTIVE_19TH_ENTRY_LAST = 0x53, + ACTIVE_20TH_ENTRY_LAST = 0x54, +}; + +// ------------------------------ +// HELPER FOR LFN FIRST BYTE +// ------------------------------ +union LFNEntry_FirstByte { + Entry_Status status; + LFN_Sequence seq_num; +}; + +// ------------------------------ +// SFN ATTRIBUTE HELPER +// ------------------------------ +bitfield Attributes { + readOnly : 1; + hidden : 1; + systemFile : 1; + volumeLabel : 1; + subDirectory : 1; + archive : 1; + padding : 2; +} [[bitfield_order( + std::core::BitfieldOrder::LeastToMostSignificant, 8)]]; + +// ------------------------------ +// ROOT DIRECTORY ENTRY FUNC +// ------------------------------ +fn dir_entry_marker(u64 abs_off) { + u8 first @ abs_off; + return first; +}; + +// ------------------------------ +// ROOT DIRECTORY ENTRY FUNC +// ------------------------------ +fn dir_entry_attr(u64 abs_off) { + u8 attr @ abs_off + 0x0B; + return attr; +}; + +// ------------------------------ +// DATES AND TIMES FUNC +// ------------------------------ +fn format_dos_time_field(std::time::DOSTime t) { + return std::time::format_dos_time(t, "{:02}:{:02}:{:02}"); +}; + +fn format_dos_date_field(std::time::DOSDate d) { + return std::time::format_dos_date(d, "{1:02}-{0:02}-{2:04}"); +}; + +// ------------------------------ +// SHORT FILE NAME ALIAS PARSER +// ------------------------------ +struct SFN_Entry_Alias { + char fileName[8] [[name("SFN"), comment("Short File Name (8dot3)")]]; + char extension[3] [[name("EXT"), comment("File Extension (8dot3)")]]; + Attributes attributes [[name("RASH ATTR"), comment("Read-Only | Archive | System | Hidden | SubDir...")]]; + u8 reserved [[comment("Zeros")]]; + u8 milliseconds [[comment("Add to Times for Refinement")]]; + std::time::DOSTime Created_Time [[format("format_dos_time_field")]]; + std::time::DOSDate Created_Date [[format("format_dos_date_field")]]; + std::time::DOSDate Accessed_Date [[format("format_dos_date_field")]]; + u16 Cluster_Hi [[comment("High Cluster if Needed")]]; + std::time::DOSTime Modified_Time [[format("format_dos_time_field")]]; + std::time::DOSDate Modified_Date [[format("format_dos_date_field")]]; + u16 Cluster_Lo [[comment("Starting Cluster or Combine with Cluster_Hi")]]; + u32 fileSize [[name("FILE_SIZE"), comment("File Size in Bytes")]]; + + u32 first_cluster = (Cluster_Hi << 16) | Cluster_Lo; + + u8 FILE_DATA[fileSize] @ dataRegionStart + (first_cluster -2) * bytesPerCluster [[comment("Pointer to the Files Content")]]; + + sfn_count += 1; + + if (fileName[0] == 0xE5) { + sfn_del_count += 1; + } +}; + +// ------------------------------ +// LOOOONG FILE NAME PARSER +// ------------------------------ +struct LFN_Entry { + u64 curr_first_byte = $; + u8 curr_attr = dir_entry_attr(curr_first_byte); + + LFNEntry_FirstByte SeqByte [[name("SEQUENCE_NUM"), comment("0x01-0x20 | Add 0x40 to Last LFN Entry")]]; + char16 NAME_1[5] [[comment("First 5 Characters of LFN")]]; + Attributes attributes [[name("LFN_ATTR"), comment("0x0F = LFN")]]; + padding[1] [[comment("Zeros")]]; + u8 nameChecksum [[name("Checksum"), comment("Checksum Calculated on SFN_ALIAS")]]; + char16 NAME_2[6] [[comment("Next 6 Characters of LFN")]]; + padding[2] [[comment("Zeros")]]; + char16 NAME_3[2] [[comment("Next 2 Characters of LFN")]]; + + // ATTEMPT TO CLEANUP UNICODE LFN... DOES NOT ACCOUNT FOR MULTI LFN ENTRIES + if (curr_attr == 0x0F) { + char display_name[32] @ $ - 32 [[ + name( + (NAME_1[0] >= 0x20 && NAME_1[0] <= 0x7E ? std::string::to_string(NAME_1[0]) : "") + + (NAME_1[1] >= 0x20 && NAME_1[1] <= 0x7E ? std::string::to_string(NAME_1[1]) : "") + + (NAME_1[2] >= 0x20 && NAME_1[2] <= 0x7E ? std::string::to_string(NAME_1[2]) : "") + + (NAME_1[3] >= 0x20 && NAME_1[3] <= 0x7E ? std::string::to_string(NAME_1[3]) : "") + + (NAME_1[4] >= 0x20 && NAME_1[4] <= 0x7E ? std::string::to_string(NAME_1[4]) : "") + + + (NAME_2[0] >= 0x20 && NAME_2[0] <= 0x7E ? std::string::to_string(NAME_2[0]) : "") + + (NAME_2[1] >= 0x20 && NAME_2[1] <= 0x7E ? std::string::to_string(NAME_2[1]) : "") + + (NAME_2[2] >= 0x20 && NAME_2[2] <= 0x7E ? std::string::to_string(NAME_2[2]) : "") + + (NAME_2[3] >= 0x20 && NAME_2[3] <= 0x7E ? std::string::to_string(NAME_2[3]) : "") + + (NAME_2[4] >= 0x20 && NAME_2[4] <= 0x7E ? std::string::to_string(NAME_2[4]) : "") + + (NAME_2[5] >= 0x20 && NAME_2[5] <= 0x7E ? std::string::to_string(NAME_2[5]) : "") + + + (NAME_3[0] >= 0x20 && NAME_3[0] <= 0x7E ? std::string::to_string(NAME_3[0]) : "") + + (NAME_3[1] >= 0x20 && NAME_3[1] <= 0x7E ? std::string::to_string(NAME_3[1]) : "") + ) + ]]; + } + lfn_count += 1; + + if (SeqByte.status == Entry_Status::DELETED_ENTRY) { + lfn_del_count += 1; + } +}; + +// ------------------------------ +// SHORT FILE NAME PARSER +// ------------------------------ +struct SFN_Entry { + char fileName[8] [[name("SFN"), comment("Short File Name (8dot3)")]]; + char extension[3] [[name("EXT"), comment("File Extension (8dot3)")]]; + Attributes attributes [[name("RASH ATTR"), comment("Read-Only | Archive | System | Hidden | SubDir...")]]; + u8 reserved [[comment("Zeros")]]; + u8 milliseconds [[comment("Add to Times for Refinement")]]; + std::time::DOSTime Created_Time [[format("format_dos_time_field")]]; + std::time::DOSDate Created_Date [[format("format_dos_date_field")]]; + std::time::DOSDate Accessed_Date [[format("format_dos_date_field")]]; + u16 Cluster_Hi [[comment("High Cluster if Needed")]]; + std::time::DOSTime Modified_Time [[format("format_dos_time_field")]]; + std::time::DOSDate Modified_Date [[format("format_dos_date_field")]]; + u16 Cluster_Lo [[comment("Starting Cluster or Combine with Cluster_Hi")]]; + u32 fileSize [[name("FILE_SIZE"), comment("File Size in Bytes")]]; + + u32 first_cluster = (Cluster_Hi << 16) | Cluster_Lo; + + u8 FILE_DATA[fileSize] @ dataRegionStart + (first_cluster -2) * bytesPerCluster [[comment("Pointer to the File Content")]]; + + sfn_count += 1; + + if (fileName[0] == 0xE5) { + sfn_del_count += 1; + } +}; + +// ------------------------------ +// SUBDIRECTORY PARSER | LEVEL 2 +// ------------------------------ +struct SubDirParser { + u8 first = dir_entry_marker($); + u8 attr = dir_entry_attr($); + + u64 next_first_byte = $ + 32; + u8 next_attr = dir_entry_attr(next_first_byte); // current offset plus 12 bytes (offset 0x0B of entry) + + if (first != 0x00 && attr == 0x0F) { + LFN_Entry LFN_ENTRY; + + if (next_first_byte != 0x00 && next_first_byte != 0xE5 && next_attr == 0x0F) { + LFN_Entry next_LFN_ENTRY; + SFN_Entry_Alias SFN_ALIAS; + } + + } else if (first != 0x00 && attr != 0x0F) { + SFN_Entry SFN_ENTRY; + } +}; + +// ------------------------------ +// ROOT DIRECTORY ENTRY PARSER +// ROUGH METHOD OF PARSING SFN/LFN/SFN_ALIAS/SUBDIR TWO LEVELS DEEP +// IF THE PATTERN CRASHES - THIS IS LIKELY WHY +// ------------------------------ +struct RootDirParser { + u64 curr_first_byte = $; + u8 curr_attr = dir_entry_attr(curr_first_byte); // current offset plus 12 bytes (offset 0x0B of entry) + + u64 next_first_byte = $ + 32; + u8 next_attr = dir_entry_attr(next_first_byte); // current offset plus 12 bytes (offset 0x0B of entry) + + bool is_subdir = false; + + if (curr_first_byte != 0x00 && curr_first_byte != 0xE5 && curr_attr == 0x0F) { + LFN_Entry LFN_ENTRY; + + if (next_first_byte != 0x00 && next_first_byte != 0xE5 && next_attr == 0x0F) { + LFN_Entry next_LFN_ENTRY; + SFN_Entry_Alias SFN_ALIAS; + + is_subdir = SFN_ALIAS.attributes.subDirectory; + + if (SFN_ALIAS.attributes.subDirectory && next_first_byte != 0x00 && next_attr != 0xE5) { + is_subdir = SFN_ALIAS.attributes.subDirectory; + + u64 dir_start_addr = dataRegionStart + (SFN_ALIAS.first_cluster - 2) * bytesPerCluster; + SubDirParser SUB_DIR_INDEX[while(std::mem::read_unsigned($, 1) != 0x00)] @ dir_start_addr; + } + } + + if (next_first_byte != 0x00 && next_first_byte != 0xE5 && next_attr != 0x0F) { + SFN_Entry_Alias SFN_ALIAS; + + is_subdir = SFN_ALIAS.attributes.subDirectory; + + if (SFN_ALIAS.attributes.subDirectory && next_first_byte != 0x00 && next_attr != 0xE5) { + is_subdir = SFN_ALIAS.attributes.subDirectory; + + u64 dir_start_addr = dataRegionStart + (SFN_ALIAS.first_cluster - 2) * bytesPerCluster; + SubDirParser SUB_DIR_INDEX[while(std::mem::read_unsigned($, 1) != 0x00)] @ dir_start_addr; + } + + } + + } else if (curr_first_byte != 0x00 && curr_first_byte != 0xE5 && curr_attr != 0x0F) { + SFN_Entry SFN_ENTRY; + is_subdir = SFN_ENTRY.attributes.subDirectory; + + if (SFN_ENTRY.attributes.subDirectory && next_first_byte != 0x00 && next_attr != 0xE5) { + is_subdir = SFN_ENTRY.attributes.subDirectory; + + u64 dir_start_addr = dataRegionStart + (SFN_ENTRY.first_cluster - 2) * bytesPerCluster; + SubDirParser SUB_DIR_INDEX[while(std::mem::read_unsigned($, 1) != 0x00)] @ dir_start_addr; + } + + } else if (curr_first_byte != 0x00 && current_first_byte == 0xE5) { + + if (next_first_byte != 0x00 && next_attr == 0x0F) { + LFN_Entry LFN_ENTRY; + + if (next_first_byte != 0x00 && next_first_byte != 0xE5) { + + if (next_attr != 0x0F) { + SFN_Entry_Alias SFN_ALIAS; + + is_subdir = SFN_ALIAS.attributes.subDirectory; + + if (next_first_byte != 0x00 && next_first_byte != 0xE5 && next_attr != 0x0F) { + SFN_Entry_Alias SFN_ALIAS2; // otherwise switch to SFN + + is_subdir = SFN_ALIAS.attributes.subDirectory; + + if (SFN_ALIAS.attributes.subDirectory && next_first_byte != 0x00 && next_attr != 0xE5) { + is_subdir = SFN_ALIAS.attributes.subDirectory; + + u64 dir_start_addr = dataRegionStart + (SFN_ALIAS.first_cluster - 2) * bytesPerCluster; + SubDirParser SUB_DIR_INDEX[while(std::mem::read_unsigned($, 1) != 0x00)] @ dir_start_addr; + } + } + } + } + } else { + SFN_Entry SFN_ENTRY; + is_subdir = SFN_ENTRY.attributes.subDirectory; + } + } +} [[name(format_element($, start_index, is_subdir)), comment("FILE/DIR [INDX #]")]]; + +// ------------------------------ +// NAME FORMATTER +// ------------------------------ +fn format_element(auto v, u64 offset, bool subdir) { + if (subdir) { + return std::format("SUB_DIR [{:02}]", std::core::array_index() + offset); + } else { + return std::format("FILE [{:02}]", std::core::array_index() + offset); + } +}; + +// ------------------------------ +// ROOT DIRECTORY HEADER PARSER +// ------------------------------ +struct RootDirHeader { + char VolumeName[11] [[comment("User Defined Name of the VOL")]]; + u8 VolumeLabelFlag [[comment("Indicates the Preceding VOL LABEL")]]; + padding[10] [[comment("Zeros")]]; + std::time::DOSTime Created_Time [[format("format_dos_time_field"), comment("Last Write Time - Typically when Created/Formatted, but NOT ALWAYS...(DISK IMAGE/FAT DRIVERS)")]]; + std::time::DOSDate Created_Date [[format("format_dos_date_field"), comment("Last Write Date - Typically when Created/Formatted, but NOT ALWAYS...(DISK IMAGE/FAT DRIVERS)")]]; + padding[6] [[comment("Zeros")]]; +}; + +// ------------------------------ +// VBR SIGNATURE HELPER +// ------------------------------ +enum VBRSignature : u16 { + VBR_SIG = 0xAA55 +}; + +// ------------------------------ +// FILE SYSTEM INFO BLOCK +// ------------------------------ +struct FSInfo { + u32 leadSignature [[comment("RRaA")]]; + padding[480] [[comment("Zeros")]]; + u32 structSignature [[comment("FSINFO Signature")]]; + u32 freeClusterCount [[comment("Approximate Free Cluster Count")]]; + u32 nextFreeCluster [[comment("FAT1: Suggested Starting Point")]]; + padding[14] [[comment("Zeros")]]; + VBRSignature VBR_SIG [[comment("0x55AA")]]; +}; + +// ------------------------------ +// FAT12/16/32 BIOS PARAMETER BLOCK (BPB) +// ------------------------------ +struct BPB_Common { + u8 jmp_boot[3] [[comment("Assembly Instructions to Jump to Boot Code")]]; + char oem_name[8] [[comment("MSDOS/BSD")]]; + u16 bytes_per_sector [[comment("512,1024,2048,4096")]]; + u8 sectors_per_cluster [[comment("Under 32K - Must be a power of 2")]]; + u16 reserved_sectors [[comment("Size of Reserved Area in Sectors")]]; + u8 num_fats [[comment("Typically 2, but can be 1 for Small Volumes")]]; + u16 root_entry_count [[comment("Max Num of Entries -- 0 for FAT32| 512 for FAT16")]]; + u16 total_sectors16 [[comment("if 0, use total_sectors32")]]; + u8 media_type [[comment("0xF8=FIXED DISK | 0xF0=REMOVABLE")]]; + u16 fat_size16 [[comment("Size of each FAT in Sectors for FAT12/16; 0 for FAT32")]]; + u16 sectors_per_track [[comment("Legacy")]]; + u16 num_heads [[comment("Legacy")]]; + u32 hidden_sectors [[comment("Num of Sectors before the Volume")]]; + u32 total_sectors32 [[comment("32bit Value of Total Num of Sectors in Volume")]]; +// ----------------------vvv----- +// FAT32 EXTENDED +// ----------------------vvv----- + u32 FAT_Sector_Count [[comment("Total Sectors per FAT")]]; + u16 ext_flags [[comment("16bit Value: BIT_7 = 1 == 1 FAT USED | Otherwise both FATs USED")]]; + u16 fs_version [[comment("Major and Minor | None")]]; + u32 root_cluster [[comment("Cluster Num of Root Dir")]]; + u16 fs_info_sector [[comment("FS_INFO Location")]]; + u16 backup_boot_sector [[comment("VBR Backup Location")]]; + u8 reserved[12] [[comment("Zeros")]]; + u8 drive_number [[comment("BIOS INT13h Drive Num")]]; + u8 reserved1 [[comment("Zeros")]]; + u8 boot_signature [[comment("Extended Boot Sig = 0x29")]]; + u32 volume_id [[comment("Volume Serial Number - Based on Created Date/Time")]]; + char volume_label[11] [[comment("No Name | User Defined Name | Check Root Dir")]]; + char fs_type[8] [[comment("FAT32 ")]]; + u8 bootstrap[420] [[comment("Until Signature")]]; + VBRSignature VBR_SIG [[comment("0x55AA")]]; +// ----------------------vvv----- +// UPDATE CONSTANTS/GLOBALS +// ----------------------vvv----- + bytesPerCluster = sectors_per_cluster * bytes_per_sector; + rootDirSectors = ((root_entry_count * 32) + (bytes_per_sector - 1)) / bytes_per_sector; + firstDataSector = reserved_sectors + (num_fats * FAT_Sector_Count) + rootDirSectors; + dataRegionStart = firstDataSector * bytes_per_sector; +}; + +// ----------------------------------------------------------------------------- +// FAT32 MAIN RELATED +// ----------------------------------------------------------------------------- +// V V V V V V V V V V +// ----------------------------------------------------------------------------- +// ------------------------------ +// FAT32 VOLUME BOOT RECORD +// ------------------------------ +BPB_Common F32_VBR @ $; +VBR_OFFSET = F32_VBR.hidden_sectors * F32_VBR.bytes_per_sector; + +/// ------------------------------ +// FILE SYSTEM INFO BLOCK +// ------------------------------ +FSInfo FS_INFO @ F32_VBR.fs_info_sector * F32_VBR.bytes_per_sector; +root_dir_start = dataRegionStart + ((F32_VBR.root_cluster - 2) * bytesPerCluster) + 32; + +// ------------------------------ +// FILE ALLOCATION TABLE +// *** HAS GLOBAL AT TOP *** +// ------------------------------ +FAT1_start_offset = F32_VBR.reserved_sectors * F32_VBR.bytes_per_sector; +FAT2_start_offset = FAT1_start_offset + (F32_VBR.FAT_Sector_Count * F32_VBR.bytes_per_sector); +FAT_ClusterHeap_Count = F32_VBR.FAT_Sector_Count * F32_VBR.bytes_per_sector / CLUSTER_SIZE_BYTES; + +FAT_Header FAT1_HEADER @ FAT1_start_offset; +FAT_Entry FAT1[MAX_FAT_CHUNKS] @ FAT1_start_offset + 8; + +FAT_Header FAT2_HEADER @ FAT2_start_offset; +FAT_Entry FAT2[MAX_FAT_CHUNKS] @ FAT2_start_offset + 8; + +// ------------------------------ +// ROOT DIRECTORY HEADER +// ------------------------------ +RootDirHeader ROOT_DIR_HEADER @ dataRegionStart + ((F32_VBR.root_cluster - 2) * bytesPerCluster); + +// ----*-----*------*------*----- +// * * ROOT DIRECTORY PARSER * * +// ----*-----*------*------*----- +RootDirParser ROOT_DIR_ENTRIES[while(std::mem::read_unsigned($, 1) != 0x00)] @ root_dir_start; + +// ------------------------------ +// SFN <-> CLUSTER RELATION OVERLAY +// *** HAS GLOBAL AT TOP *** +// ------------------------------ +INFO_Overlay SFN_CLUSTER_LIST[MAX_SFN_CLUSTER_RELATIONS] @ FAT1_start_offset [[name("SFN <-> CLUSTER (DERIVED)")]]; + +// ------------------------------ +// FAT32 VOLUME REPORT +// *** HAS GLOBAL AT TOP *** +// ------------------------------ +abs_FAT1_start_offset = VBR_OFFSET + (F32_VBR.reserved_sectors * F32_VBR.bytes_per_sector); +abs_FAT2_start_offset = abs_FAT1_start_offset + (F32_VBR.FAT_Sector_Count * F32_VBR.bytes_per_sector); +abs_rootDirStart_offset = VBR_OFFSET + dataRegionStart; + +if (VOLUME_REPORT) { + std::print(" "); + std::print("-----------------------------------------"); + std::print("---------- FAT32 VOLUME_REPORT ----------"); + std::print("-----------------------------------------"); + std::print("VOL_LABEL = {}", F32_VBR.volume_label); + std::print("FILE_SYSTEM = {}", F32_VBR.fs_type); + std::print("SERIAL_NUMBER = 0x{:X}", F32_VBR.volume_id); + + std::print("-----------------------------------------"); + std::print("BYTES/SECTOR = {:02}", F32_VBR.bytes_per_sector); + std::print("SECTORS/CLUSTER = {:02}", F32_VBR.sectors_per_cluster); + std::print("BYTES/CLUSTER = {:02}", bytesPerCluster); + std::print("ROOT_ENTRIES = {:02}", F32_VBR.root_entry_count); + std::print("CLUSTER_COUNT = {:02}", (F32_VBR.total_sectors32 - firstDataSector) / F32_VBR.sectors_per_cluster); + + std::print("-----------------------------------------"); + std::print("VOLUME_SIZE = {:02} SECTORS", F32_VBR.total_sectors32); + std::print("VOLUME_SIZE = {:.4f} GB @ 1000", (F32_VBR.total_sectors32 * F32_VBR.bytes_per_sector) / 1000.0 / 1000.0 / 1000.0); + std::print("VOLUME_SIZE = {:.4f} GiB @ 1024", (F32_VBR.total_sectors32 * F32_VBR.bytes_per_sector) / 1024.0 / 1024.0 / 1024.0); + + std::print("-----------------------------------------"); + std::print("RESERVED_SECTORS = {:02}", F32_VBR.reserved_sectors); + std::print("FAT_COUNT = {:02}", F32_VBR.num_fats); + std::print("FAT_SIZE = {:02} SECTORS", F32_VBR.FAT_Sector_Count); + std::print("FAT1_START_OFF = {} | 0x{:02X}", abs_FAT1_start_offset, abs_FAT1_start_offset); + std::print("FAT2_START_OFF = {} | 0x{:02X}", abs_FAT2_start_offset, abs_FAT2_start_offset); + std::print("ROOT_DIR_CLUSTER = {:02}", F32_VBR.root_cluster); + std::print("ROOT_DIR_OFFSET = {} | 0x{:02X}", abs_rootDirStart_offset, abs_rootDirStart_offset); + + std::print("-----------------------------------------"); + if (sfn_del_count >= 1) { + std::print("SFN_DEL(xE5) = DETECTED"); + } + if (lfn_del_count >= 1) { + std::print("LFN_DEL(xE5) = DETECTED"); + } + std::print("FAT1_EOF_COUNT = {:02}", allocated_file_count / 2); // divided by 2 (FAT1/FAT2) + + std::print("-----------------------------------------"); + std::print("------------------ END ------------------"); + std::print("-----------------------------------------"); + std::print(" "); +} diff --git a/patterns/DFIR/NTFS.hexpat b/patterns/DFIR/NTFS.hexpat new file mode 100644 index 00000000..7afee959 --- /dev/null +++ b/patterns/DFIR/NTFS.hexpat @@ -0,0 +1,1571 @@ +#pragma author MODIFIED BY: Formula Zero One Technologies +#pragma description NT File System (NTFS_v2.0) +#pragma endian little + +// ----------------------------------------------------------------------------- +// CREDIT +// ----------------------------------------------------------------------------- +// OG AUTHOR: Hrant Tadevosyan (Axcient, now ConnectWise) +// OG DESC: ntfs.hexpat_v1.0 +// So much work went into this pattern; NICE JOB!! +// ----------------------------------------------------------------------------- +// NOTES FOR v2.0 +// ----------------------------------------------------------------------------- +// Imported by DISK_PARSER.hexpat +// Added recursive parsing for all $MFT records +// Added D/T conversions +// Changed most ntfschar to char16 to show filenames on hover +// Added comments to DFIR fields of interest +// Changed pattern output naming/structure. Similar to RunTime's DiskExplorer -- FREntry > FRHeader > ATTRName > ATTRHeader > ATTRBody > ATTREnd +// Reconfigured x50 ATTR and ACL processing +// Modified RunList - File Content Pointer +// ----------------------------------------------------------------------------- +// NTFS MANTRAS FROM NW3C... +// ----------------------------------------------------------------------------- +// --- 1 --- EVERYTHING IN NTFS IS A FILE (RECORD) (INCLUDING DIRs) +// --- 2 --- EVERY FILE (& DIR) HAS AN ENTRY IN THE $MFT, INCLUDING THE $MFT ITSELF +// --- 3 --- EVERY FILE (RECORD) IS MADE UP OF ATTRIBUTES +// --------- FILES = x10/x30/x80 (minimum) +// --------- DIRs = x10/x30/x90 (minimum) +// ----------------------------------------------------------------------------- +// ------------------------------------------------------------------------- +// IMPORTS +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- +import std.core; +import std.array; +import std.ptr; +import std.io; +import std.mem; +import std.string; +import std.time; +import type.magic; +import type.time; +import type.guid; +import type.base; + +// ------------------------------------------------------------------------- +// FWD DECs - GLOBALS +// ------------------------------------------------------------------------- + +// *** ATTENTION *** + +// ---*******---*******----vvvv--- | +const bool VOLUME_REPORT = true; +// ---*******---*******----^^^^--- | + +u64 VBR_OFFSET; +u64 prev_lcn = 0; +u32 MFT_Count; +u32 Unused_Count; +u8 NT_Major; +u8 NT_Minor; +type::Hex mft_num; +type::Hex seq_num; +str Vol_Label; +u8 PRENT_MFT; +u8 PRENT_SEQ; + +// ------------------------------------------------------------------------- +// GENERAL HELPERS RELATED +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- +using ntfschar = u16; +using leVCN = u64; +using leLCN = u64; +using leLSN = u64; +using leMFT_REF = u64; + +bitfield RUNLIST_HEADER { + unsigned SE_Offset : 4; + unsigned Length : 4; +} [[bitfield_order(std::core::BitfieldOrder::MostToLeastSignificant, 8)]]; + +// ------------------------------------------------------------------------- +// RUNLIST +// ------------------------------------------------------------------------- +struct RUNLIST { + RUNLIST_HEADER RunList_Header + [[comment("Left Nibble = S.E. Bytes | Right Nibble = S.E. Length")]]; + + // Highlight the Stream Extent bytes + char run_extent[RunList_Header.SE_Offset + RunList_Header.Length] @ $ + [[name(std::format("Run_Extent: {} + {} = {} Bytes ", + RunList_Header.SE_Offset, + RunList_Header.Length, + RunList_Header.SE_Offset + RunList_Header.Length)), + comment("Start/Run Extent | Derived from Header")]]; + + // Get number of clusters as a string + str NoC_value = std::string::to_string( + std::mem::read_unsigned($, RunList_Header.Length)); + + // Highlight exactly RunList_Header.Length bytes + char Num_of_Clusters[RunList_Header.Length] + [[name(std::format("Num_of_Clusters = {} ", NoC_value)), + comment("Number of Clusters in a Run")]]; + + // Needed for mft_get_dat_attr_lcn function only + u8 Logical_Cluster_Num[RunList_Header.SE_Offset] [[hidden]]; + + if (RunList_Header.SE_Offset > 0) { + // Read run length (little-endian) + u64 num_clusters = std::mem::read_unsigned( + $ - (RunList_Header.SE_Offset + RunList_Header.Length), + RunList_Header.Length); + + // Read signed LCN delta + s64 lcn_delta = std::mem::read_signed( + $ - RunList_Header.SE_Offset, + RunList_Header.SE_Offset); + + u64 data_size = num_clusters * cluster_size; + u64 content_offset = VBR_OFFSET + (lcn_delta * cluster_size); + + // Show both decimal LCN and absolute byte offset in the label + char Start_LCN[RunList_Header.SE_Offset] @ $ - RunList_Header.SE_Offset + [[name(std::format("Start_LCN {} | Offset 0x{:X} ", + lcn_delta, + content_offset)), + comment("Starting Logical Cluster Number")]]; + + // Pointer to the file content + char MAGIC_BYTES[4] @ content_offset - VBR_OFFSET //u8 as needed... + [[comment("Pointer to Start Cluster"), static, sealed]]; + + // Select all allocated clusters for easy Section View + // May error - array grew past end of data.... + //u8 CLUSTER_CONTENT[data_size] @ content_offset - VBR_OFFSET + // [[comment("Pointer to Cluster Content"), static, sealed]]; + //if (MAGIC_BYTES == 0xFFD8FF) { + // IMPORT AND CALL jpeg.hexpat + //} + } + + // Look ahead at the next byte to detect another run + u8 next_byte @ $ [[hidden]]; + + if (next_byte != 0x00) { + RUNLIST Next_Run; // recursively parse next run + } +} [[inline]]; + +fn calc_lcn_str(u64 start_offset, u32 len) { + if (len == 0) + return "0"; + return std::mem::read_unsigned(start_offset, len); +}; + +// ------------------------------------------------------------------------- +// SIGNATURE HELPER +// ------------------------------------------------------------------------- +enum VBRSignature : u16 { + VBR_SIG = 0xAA55 +}; + +// ------------------------------------------------------------------------- +// WINDOWS FILETIME FUNC +// ------------------------------------------------------------------------- +fn parse_filetime(u64 raw_ft) { + // Convert raw FILETIME to Unix time + u64 unix_time = type::impl::format_filetime_as_unix(raw_ft); + + // Convert Unix time to structured UTC time + std::time::Time ts = std::time::to_utc(unix_time); + + // Format as string + str formatted = std::time::format(ts, "%Y-%m-%d %H:%M:%S"); + + return formatted; +}; + +// ------------------------------------------------------------------------- +// NTFS VOLUME BOOT RECORD RELATED +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- +struct BIOS_PARAMETER_BLOCK { + u16 bytes_per_sector [[comment("Bytes per Sector")]]; + u8 sectors_per_cluster [[comment("Sectors per Cluster")]]; + u16 reserved_sectors [[comment("Reserved")]];; + u8 fats [[comment("Zeros")]];; + u16 root_entries [[comment("Unused")]]; + u16 sectors [[comment("Unused")]]; + u8 media_descriptor [[comment("Always F8-2003+")]]; + u16 sectors_per_fat [[comment("Legacy")]]; + u16 sectors_per_track [[comment("Legacy")]]; + u16 heads [[comment("Legacy")]]; + u32 hidden_sectors [[comment("Sectors before Volume")]]; + u32 large_sectors [[comment("Legacy")]]; +}; + +// ------------------------------------------------------------------------- +// FILE RECORD ENTRY RELATED +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- +enum NTFS_RECORD_TYPES : u32 { + magic_FILE = 0x454c4946, + magic_INDX = 0x58444e49, + magic_HOLE = 0x454c4f48, + magic_RSTR = 0x52545352, + magic_RCRD = 0x44524352, + magic_CHKD = 0x444b4843, + magic_BAAD = 0x44414142, + magic_empty = 0xffffffff, +}; + +// For other records, such as Index +struct NTFS_RECORD { + NTFS_RECORD_TYPES magic [[comment("File Record Signature 'FILE'")]];; + u16 usa_ofs [[comment("Offset to Fix Up Array")]]; + u16 usa_count [[comment("# of 2 Byte Entries in FUA")]]; +}; + +enum MFT_RECORD_FLAGS : u16 { + MFT_RECORD_NOT_IN_USE = 0x0000, + MFT_RECORD_IN_USE = 0x0001, + MFT_RECORD_IS_DIRECTORY = 0x0002, + MFT_RECORD_IS_4 = 0x0004, + MFT_RECORD_IS_VIEW_INDEX = 0x0008, + MFT_REC_SPACE_FILLER = 0xffff, +}; + +// ------------------------------------------------------------------------- +// FILE RECORD ATTRIBUTE RELATED +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- +enum ATTR_TYPES : u32 { + AT_UNUSED = 0x00, + AT_STANDARD_INFORMATION = 0x10, + AT_ATTRIBUTE_LIST = 0x20, + AT_FILE_NAME = 0x30, + AT_OBJECT_ID = 0x40, + AT_SECURITY_DESCRIPTOR = 0x50, + AT_VOLUME_NAME = 0x60, + AT_VOLUME_INFORMATION = 0x70, + AT_DATA = 0x80, + AT_INDEX_ROOT = 0x90, + AT_INDEX_ALLOCATION = 0xa0, + AT_BITMAP = 0xb0, + AT_REPARSE_POINT = 0xc0, + AT_EA_INFORMATION = 0xd0, + AT_EA = 0xe0, + AT_PROPERTY_SET = 0xf0, + AT_LOGGED_UTILITY_STREAM = 0x100, + AT_FIRST_USER_DEFINED_ATTRIBUTE = 0x1000, + AT_END = 0xffffffff, +}; + +enum ATTR_DEF_FLAGS : u32 { + ATTR_DEF_INDEXABLE = 0x02, + ATTR_DEF_MULTIPLE = 0x04, + ATTR_DEF_NOT_ZERO = 0x08, + ATTR_DEF_INDEXED_UNIQUE = 0x10, + ATTR_DEF_NAMED_UNIQUE = 0x20, + ATTR_DEF_RESIDENT = 0x40, + ATTR_DEF_ALWAYS_LOG = 0x80, +}; + +enum COLLATION_RULES : u32 { + COLLATION_BINARY = 0, + COLLATION_FILE_NAME = 1, + COLLATION_UNICODE_STRING = 2, + COLLATION_NTOFS_ULONG = 16, + COLLATION_NTOFS_SID = 17, + COLLATION_NTOFS_SECURITY_HASH = 18, + COLLATION_NTOFS_ULONGS = 19, +}; + +struct ATTR_DEF { + //ntfschar name[0x40]; + char16 name[0x40]; + ATTR_TYPES type; + u32 display_rule; + COLLATION_RULES collation_rule; + ATTR_DEF_FLAGS flags; + u64 min_size; + u64 max_size; +}; + +enum ATTR_FLAGS : u16 { + ATTR_IS_COMPRESSED = 0x0001, + ATTR_COMPRESSION_MASK = 0x00ff, + ATTR_IS_ENCRYPTED = 0x4000, + ATTR_IS_SPARSE = 0x8000, +}; + +enum RESIDENT_ATTR_FLAGS : u8 { + RESIDENT_ATTR_IS_INDEXED = 0x01, + RESIDENT_ATTR_NOT_INDEXED = 0x00, +}; + +enum FILE_ATTR_FLAGS : u32 { + FILE_ATTR_READONLY = 0x00000001, + FILE_ATTR_HIDDEN = 0x00000002, + FILE_ATTR_SYSTEM = 0x00000004, + FILE_ATTR_DIRECTORY = 0x00000010, + FILE_ATTR_ARCHIVE = 0x00000020, + FILE_ATTR_DEVICE = 0x00000040, + FILE_ATTR_NORMAL = 0x00000080, + FILE_ATTR_TEMPORARY = 0x00000100, + FILE_ATTR_SPARSE_FILE = 0x00000200, + FILE_ATTR_REPARSE_POINT = 0x00000400, + FILE_ATTR_COMPRESSED = 0x00000800, + FILE_ATTR_OFFLINE = 0x00001000, + FILE_ATTR_NOT_CONTENT_INDEXED = 0x00002000, + FILE_ATTR_ENCRYPTED = 0x00004000, + FILE_ATTRIBUTE_RECALL_ON_OPEN = 0x00040000, + FILE_ATTR_VALID_FLAGS = 0x00047fb7, + FILE_ATTR_VALID_SET_FLAGS = 0x000031a7, + FILE_ATTR_I30_INDEX_PRESENT = 0x10000000, + FILE_ATTR_VIEW_INDEX_PRESENT = 0x20000000, +}; + +enum FILE_NAME_TYPE_FLAGS : u8 { + FILE_NAME_POSIX = 0x00, + FILE_NAME_WIN32 = 0x01, + FILE_NAME_DOS = 0x02, + FILE_NAME_WIN32_AND_DOS = 0x03, +}; + +// ------------------------------------------------------------------------- +// STANDARD INFO ATTRIBUTE RELATED +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- +struct STANDARD_INFORMATION_BODY_OLD { + u64 Created_DT_UTC [[format("parse_filetime"), comment("Created DT UTC")]]; + u64 Modified_DT_UTC [[format("parse_filetime"), comment("Modified DT UTC")]]; + u64 MFT_Mod_DT_UTC [[format("parse_filetime"), comment("$MFT Mod DT UTC")]]; + u64 Accessed_DT_UTC [[format("parse_filetime"), comment("Accessed DT UTC")]]; + FILE_ATTR_FLAGS file_attributes [[comment("RASH ATTR")]];; +} [[static]]; + +struct STANDARD_INFORMATION_OLD { + u64 Created_DT_UTC [[format("parse_filetime"), comment("Created DT UTC")]]; + u64 Modified_DT_UTC [[format("parse_filetime"), comment("Modified DT UTC")]]; + u64 MFT_Mod_DT_UTC [[format("parse_filetime"), comment("$MFT Mod DT UTC")]]; + u64 Accessed_DT_UTC [[format("parse_filetime"), comment("Accessed DT UTC")]]; + FILE_ATTR_FLAGS file_attributes [[comment("RASH ATTR")]];; + u8 reserved12[12]; +} [[static]]; + +struct STANDARD_INFORMATION { + u64 Created_DT_UTC [[format("parse_filetime"), comment("Created DT UTC")]]; + u64 Modified_DT_UTC [[format("parse_filetime"), comment("Modified DT UTC")]]; + u64 MFT_Mod_DT_UTC [[format("parse_filetime"), comment("$MFT Mod DT UTC")]]; + u64 Accessed_DT_UTC [[format("parse_filetime"), comment("Accessed DT UTC")]]; + FILE_ATTR_FLAGS file_attributes [[comment("RASH ATTR")]];; + + u32 maximum_versions [[comment("Maximum Versions Allowed: Disabled = 0x00")]];; + u32 version_number [[comment("File Version Number")]];; + u32 class_id [[comment("Class ID")]];; + u32 owner_id [[comment("Owner ID - $Quota")]];; + u32 security_id [[comment("Security Permission Settings")]];; + u64 quota_charged [[comment("Number of Bytes Towards User Quota")]];; + u64 usn [[comment("Update Sequence Number - $USNJournal Index")]];; +} [[static]]; + +struct FILE_NAME_ATTR_PACKED { + u16 packed_ea_size [[comment("Size of xE0 ATTR")]];; + u16 reserved [[comment("Reserved")]];; +} [[static]]; + +union FILE_NAME_ATTR_FORM { + FILE_NAME_ATTR_PACKED packed; // [[inline]]; + u32 reparse_point_tag [[comment("Reparse Point Tag")]];; +} [[static]]; + +struct FILE_NAME_ATTR { + PRENT_MFT = 0x00; + PRENT_SEQ = 0x00; + + u8 MFT_Parent_Dir[6] [[comment("Parent Directory $MFT File Record Number")]]; + u16 Parent_Seq_Num [[comment("Parent Directory $MFT Sequence Number")]]; + u64 Created_DT_UTC [[format("parse_filetime"), comment("Created DT in UTC")]]; + u64 Modified_DT_UTC [[format("parse_filetime"), comment("Modified DT in UTC")]]; + u64 MFT_Mod_DT_UTC [[format("parse_filetime"), comment("MFT Mod DT in UTC")]]; + u64 Accessed_DT_UTC [[format("parse_filetime"), comment("Accessed DT in UTC")]]; + u64 allocated_size [[comment("Size on Disk")]]; + u64 data_size [[comment("Logical File Size")]]; + FILE_ATTR_FLAGS file_attributes [[comment("RASH ATTR")]]; + FILE_NAME_ATTR_FORM form [[inline, comment("Reparse Value")]]; + u8 file_name_length [[comment("File Name Length")]]; + FILE_NAME_TYPE_FLAGS file_name_type [[comment("Namespace Type: POSIX = 0x00 | Win32 = 0x01 | DOS_SFN = 0x02 | Win32&DOS = 0x03")]]; + char16 file_name[file_name_length] [[comment("The Actual File Name")]]; + + PRENT_MFT = MFT_Parent_Dir; + PRENT_SEQ = Parent_Seq_Num; +}; + +// ------------------------------------------------------------------------- +// SECURITY DESC | OBJECT ID RELATED +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- +struct OBJECT_ID_ATTR_INFO { + type::GUID birth_volume_id; + type::GUID birth_object_id; + type::GUID domain_id; +}; + +union OBJECT_ID_ATTR_FORM { + OBJECT_ID_ATTR_INFO info [[inline]]; + u8 extended_info[48]; +}; + +struct OBJECT_ID_ATTR { + type::GUID object_id; + OBJECT_ID_ATTR_FORM form [[inline]]; +}; + +enum VOLUME_FLAGS : u16 { + VOLUME_IS_DIRTY = 0x0001, + VOLUME_RESIZE_LOG_FILE = 0x0002, + VOLUME_UPGRADE_ON_MOUNT = 0x0004, + VOLUME_MOUNTED_ON_NT4 = 0x0008, + VOLUME_DELETE_USN_UNDERWAY = 0x0010, + VOLUME_REPAIR_OBJECT_ID = 0x0020, + VOLUME_CHKDSK_UNDERWAY = 0x4000, + VOLUME_MODIFIED_BY_CHKDSK = 0x8000, + VOLUME_FLAGS_MASK = 0xc03f, +}; + +struct VOLUME_INFORMATION { + u64 reserved; + u8 major_ver; + u8 minor_ver; + VOLUME_FLAGS flags; + NT_Major = major_ver; + NT_Minor = minor_ver; +} [[static]]; + +enum SECURITY_DESCRIPTOR_CONTROL : u16 { + SE_OWNER_DEFAULTED = 0x0001, + SE_GROUP_DEFAULTED = 0x0002, + SE_DACL_PRESENT = 0x0004, + SE_DACL_DEFAULTED = 0x0008, + SE_SACL_PRESENT = 0x0010, + SE_SACL_DEFAULTED = 0x0020, + SE_DACL_AUTO_INHERIT_REQ = 0x0100, + SE_SACL_AUTO_INHERIT_REQ = 0x0200, + SE_DACL_AUTO_INHERITED = 0x0400, + SE_SACL_AUTO_INHERITED = 0x0800, + SE_DACL_PROTECTED = 0x1000, + SE_SACL_PROTECTED = 0x2000, + SE_RM_CONTROL_VALID = 0x4000, + SE_SELF_RELATIVE = 0x8000, +}; + + +// ------------------------------ +// Enums +// ------------------------------ +enum ACE_TYPES : u8 { + ACCESS_MIN_MS_ACE_TYPE = 0, + ACCESS_ALLOWED_ACE_TYPE = 0, + ACCESS_DENIED_ACE_TYPE = 1, + SYSTEM_AUDIT_ACE_TYPE = 2, + SYSTEM_ALARM_ACE_TYPE = 3, + ACCESS_MAX_MS_V2_ACE_TYPE = 3, + ACCESS_ALLOWED_COMPOUND_ACE_TYPE = 4, + ACCESS_MAX_MS_V3_ACE_TYPE = 4, + ACCESS_MIN_MS_OBJECT_ACE_TYPE = 5, + ACCESS_ALLOWED_OBJECT_ACE_TYPE = 5, + ACCESS_DENIED_OBJECT_ACE_TYPE = 6, + SYSTEM_AUDIT_OBJECT_ACE_TYPE = 7, + SYSTEM_ALARM_OBJECT_ACE_TYPE = 8, + ACCESS_MAX_MS_OBJECT_ACE_TYPE = 8, + ACCESS_MAX_MS_V4_ACE_TYPE = 8, + ACCESS_MAX_MS_ACE_TYPE = 8, + ACCESS_ALLOWED_CALLBACK_ACE_TYPE = 9, + ACCESS_DENIED_CALLBACK_ACE_TYPE = 10, + ACCESS_ALLOWED_CALLBACK_OBJECT_ACE_TYPE = 11, + ACCESS_DENIED_CALLBACK_OBJECT_ACE_TYPE = 12, + SYSTEM_AUDIT_CALLBACK_ACE_TYPE = 13, + SYSTEM_ALARM_CALLBACK_ACE_TYPE = 14, + SYSTEM_AUDIT_CALLBACK_OBJECT_ACE_TYPE = 15, + SYSTEM_ALARM_CALLBACK_OBJECT_ACE_TYPE = 16, + SYSTEM_MANDATORY_LABEL_ACE_TYPE = 17, + SYSTEM_RESOURCE_ATTRIBUTE_ACE_TYPE = 18, + SYSTEM_SCOPED_POLICY_ID_ACE_TYPE = 19, + SYSTEM_PROCESS_TRUST_LABEL_ACE_TYPE = 20, +}; + +enum ACE_FLAGS : u8 { + OBJECT_INHERIT_ACE = 0x01, + CONTAINER_INHERIT_ACE = 0x02, + NO_PROPAGATE_INHERIT_ACE = 0x04, + INHERIT_ONLY_ACE = 0x08, + INHERITED_ACE = 0x10, + VALID_INHERIT_FLAGS = 0x1f, + SUCCESSFUL_ACCESS_ACE_FLAG = 0x40, + FAILED_ACCESS_ACE_FLAG = 0x80, +}; + +enum ACCESS_MASK : u32 { + FILE_READ_DATA = 0x00000001, + FILE_LIST_DIRECTORY = 0x00000001, + FILE_WRITE_DATA = 0x00000002, + FILE_ADD_FILE = 0x00000002, + FILE_APPEND_DATA = 0x00000004, + FILE_ADD_SUBDIRECTORY = 0x00000004, + FILE_READ_EA = 0x00000008, + FILE_WRITE_EA = 0x00000010, + FILE_EXECUTE = 0x00000020, + FILE_TRAVERSE = 0x00000020, + FILE_DELETE_CHILD = 0x00000040, + FILE_READ_ATTRIBUTES = 0x00000080, + FILE_WRITE_ATTRIBUTES = 0x00000100, + DELETE = 0x00010000, + READ_CONTROL = 0x00020000, + WRITE_DAC = 0x00040000, + WRITE_OWNER = 0x00080000, + SYNCHRONIZE = 0x00100000, + STANDARD_RIGHTS_READ = 0x00020000, + STANDARD_RIGHTS_WRITE = 0x00020000, + STANDARD_RIGHTS_EXECUTE = 0x00020000, + STANDARD_RIGHTS_REQUIRED = 0x000f0000, + STANDARD_RIGHTS_ALL = 0x001f0000, + ACCESS_SYSTEM_SECURITY = 0x01000000, + MAXIMUM_ALLOWED = 0x02000000, + GENERIC_ALL = 0x10000000, + GENERIC_EXECUTE = 0x20000000, + GENERIC_WRITE = 0x40000000, + GENERIC_READ = 0x80000000, +}; + +enum OBJECT_ACE_FLAGS : u32 { + ACE_OBJECT_TYPE_PRESENT = 1, + ACE_INHERITED_OBJECT_TYPE_PRESENT = 2, +}; + +// ------------------------------ +// ACE/SID +// ------------------------------ +struct ACE_HEADER { + ACE_TYPES type; + ACE_FLAGS flags; + u16 size; +}; + +struct SID { + u8 revision; + u8 sub_authority_count; + u8 identifier_authority[6]; + u32 sub_authority[sub_authority_count]; +}; + +struct ACCESS_ALLOWED_ACE { + ACE_HEADER header [[inline]]; + ACCESS_MASK mask; + SID sid; +}; + +using ACCESS_DENIED_ACE = ACCESS_ALLOWED_ACE; +using SYSTEM_ALARM_ACE = ACCESS_ALLOWED_ACE; +using SYSTEM_AUDIT_ACE = ACCESS_ALLOWED_ACE; + +struct ACCESS_ALLOWED_OBJECT_ACE { + ACE_HEADER header [[inline]]; + ACCESS_MASK mask; + OBJECT_ACE_FLAGS object_flags; + type::GUID object_type; + type::GUID inherited_object_type; + SID sid; +}; + +using ACCESS_DENIED_OBJECT_ACE = ACCESS_ALLOWED_OBJECT_ACE; +using SYSTEM_AUDIT_OBJECT_ACE = ACCESS_ALLOWED_OBJECT_ACE; +using SYSTEM_ALARM_OBJECT_ACE = ACCESS_ALLOWED_OBJECT_ACE; + +// ------------------------------ +// ACE HELPER +// ------------------------------ +union ACE { + ACE_HEADER hdr [[hidden]]; + + match (hdr.type) { + (ACE_TYPES::ACCESS_ALLOWED_ACE_TYPE): ACCESS_ALLOWED_ACE allowed; + (ACE_TYPES::ACCESS_DENIED_ACE_TYPE): ACCESS_DENIED_ACE denied; + (ACE_TYPES::SYSTEM_AUDIT_ACE_TYPE): SYSTEM_AUDIT_ACE audit; + (ACE_TYPES::SYSTEM_ALARM_ACE_TYPE): SYSTEM_ALARM_ACE alarm; + + (ACE_TYPES::ACCESS_ALLOWED_OBJECT_ACE_TYPE): ACCESS_ALLOWED_OBJECT_ACE obj_allowed; + (ACE_TYPES::ACCESS_DENIED_OBJECT_ACE_TYPE): ACCESS_DENIED_OBJECT_ACE obj_denied; + (ACE_TYPES::SYSTEM_AUDIT_OBJECT_ACE_TYPE): SYSTEM_AUDIT_OBJECT_ACE obj_audit; + (ACE_TYPES::SYSTEM_ALARM_OBJECT_ACE_TYPE): SYSTEM_ALARM_OBJECT_ACE obj_alarm; + } + + // Consume remaining bytes as raw padding + padding[hdr.size - sizeof(ACE_HEADER)]; +}; + +// ------------------------------ +// ACL +// ------------------------------ +struct ACL { + u8 revision; + u8 alignment1; + u16 size; + u16 ace_count; + u16 alignment2; + + ACE aces[ace_count]; +}; + +// ------------------------------ +// SECURITY DESCRIPTOR +// ------------------------------ +#define SECURITY_DESCRIPTOR_RELATIVE_SIZE (20) +struct SECURITY_DESCRIPTOR_RELATIVE { + u32 struct_start = $; + u8 revision; + u8 alignment; + u16 control; // SECURITY_DESCRIPTOR_CONTROL + u32 owner; + u32 group; + u32 sacl; + u32 dacl; + + if (control & SECURITY_DESCRIPTOR_CONTROL::SE_DACL_PRESENT) { + if (dacl != 0) { + // Skip to the relative offset + if ($ - struct_start < dacl) { + padding[dacl - ($ - struct_start)]; + } + ACL dacl_acl; + } + } + if (control & SECURITY_DESCRIPTOR_CONTROL::SE_SACL_PRESENT) { + if (sacl != 0) { + if ($ - struct_start < sacl) { + padding[sacl - ($ - struct_start)]; + } + ACL sacl_acl; + } + } + + if (owner > 0) { + u32 owner_bytes = $ - struct_start; + if (owner > owner_bytes) { + padding[owner - owner_bytes]; + } + SID owner_sid; + } + + if (group > 0) { + u32 group_bytes = $ - struct_start; + if (group > group_bytes) { + padding[group - group_bytes]; + } + SID group_sid; + } +}; + +// ------------------------------------------------------------------------- +// INDEX RELATED +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- +// ------------------------------ +// INDEX HEADER HELPER +// ------------------------------ +enum INDEX_HEADER_FLAGS : u8 { + SMALL_INDEX = 0, + LARGE_INDEX = 1, + LEAF_NODE = 0, + INDEX_NODE = 1, + NODE_MASK = 1, +}; + +// ------------------------------ +// INDEX HEADER PARSER +// ------------------------------ +struct INDEX_HEADER { + u32 entries_offset; + u32 index_length; + u32 allocated_size; + INDEX_HEADER_FLAGS ih_flags; + u8 reserved[3]; +}; + +// ------------------------------ +// REPARSE INDEX KEY PARSER +// ------------------------------ +struct REPARSE_INDEX_KEY { + u32 reparse_tag; + leMFT_REF file_id; +}; + +// ------------------------------ +// SDS_ENTRY PARSER +// ------------------------------ +struct SDS_ENTRY { + u32 hash; + u32 security_id; + u64 offset; + u32 length; + SECURITY_DESCRIPTOR_RELATIVE sid; +}; + +using SII_INDEX_KEY = u32 ; + +// ------------------------------ +// SDH INDEX KEY PARSER +// ------------------------------ +struct SDH_INDEX_KEY { + u32 hash; + u32 security_id; +}; + +// ------------------------------ +// INDEX ENTRY FLAG HELPER +// ------------------------------ +enum INDEX_ENTRY_FLAGS : u16 { + INDEX_ENTRY_NODE = 1, + INDEX_ENTRY_END = 2, + INDEX_ENTRY_SPACE_FILLER = 0xffff, +}; + +// ------------------------------ +// INDEX HEADER POINTER +// ------------------------------ +struct INDEX_ENTR_HEADER_PTR { + u16 data_offset; + u16 data_length; + u32 reservedV; +}; + +// ------------------------------ +// INDEX HEADER HELPER +// ------------------------------ +union INDEX_ENTR_HEADER_REF { + leMFT_REF indexed_file; + INDEX_ENTR_HEADER_PTR ptr; +}; + +// ------------------------------ +// INDEX HEADER PARSER +// ------------------------------ +struct INDEX_ENTRY_HEADER { + INDEX_ENTR_HEADER_REF file_ref; + u16 length; + u16 key_length; + u16 flags; // INDEX_ENTRY_FLAGS + u16 reserved; +}; + +// ------------------------------ +// INDEX ENTRY FN ATTR +// Duplicated so that parent mft# and parent seq# tracking doesn't break. +// ------------------------------ +struct IDX_FILE_NAME_ATTR{ + u8 MFT_Parent_Dir[6] [[comment("Parent Directory $MFT File Record Number")]]; + u16 Parent_Seq_Num [[comment("Parent Directory $MFT Sequence Number")]]; + u64 Created_DT_UTC [[format("parse_filetime"), comment("Created DT in UTC")]]; + u64 Modified_DT_UTC [[format("parse_filetime"), comment("Modified DT in UTC")]]; + u64 MFT_Mod_DT_UTC [[format("parse_filetime"), comment("MFT Mod DT in UTC")]]; + u64 Accessed_DT_UTC [[format("parse_filetime"), comment("Accessed DT in UTC")]]; + u64 allocated_size [[comment("Size on Disk")]]; + u64 data_size [[comment("Logical File Size")]]; + FILE_ATTR_FLAGS file_attributes [[comment("RASH ATTR")]]; + FILE_NAME_ATTR_FORM form [[inline, comment("Reparse Value")]]; + u8 file_name_length [[comment("File Name Length")]]; + FILE_NAME_TYPE_FLAGS file_name_type [[comment("Namespace Type: POSIX = 0x00 | Win32 = 0x01 | DOS_SFN = 0x02 | Win32&DOS = 0x03")]]; + char16 file_name[file_name_length] [[comment("The Actual File Name")]]; +}; + +// ------------------------------ +// INDEX ENTRY FN PARSER +// ------------------------------ +struct INDEX_ENTRY_FILE_NAME { + INDEX_ENTRY_HEADER header; + + if (!(header.flags & INDEX_ENTRY_FLAGS::INDEX_ENTRY_END)) { + // Workaround -- Using new structure for IDX FNs -- Allows for setting parent info on non-IDX items + IDX_FILE_NAME_ATTR key; + } + + if (header.flags & INDEX_ENTRY_FLAGS::INDEX_ENTRY_NODE) { + leVCN vcn; + } + std::mem::AlignTo<8>; +}; + +// ------------------------------ +// INDEX ENTRY SII PARSER +// ------------------------------ +struct INDEX_ENTRY_SII { + INDEX_ENTRY_HEADER header; + + if (!(header.flags & INDEX_ENTRY_FLAGS::INDEX_ENTRY_END)) { + SII_INDEX_KEY key; + } + + if (header.flags & INDEX_ENTRY_FLAGS::INDEX_ENTRY_NODE) { + leVCN vcn; + } + + std::mem::AlignTo<8>; +}; + +// ------------------------------ +// INDEX ENTRY SDH PARSER +// ------------------------------ +struct INDEX_ENTRY_SDH { + INDEX_ENTRY_HEADER header; + + if (!(header.flags & INDEX_ENTRY_FLAGS::INDEX_ENTRY_END)) { + SDH_INDEX_KEY key; + } + + if (header.flags & INDEX_ENTRY_FLAGS::INDEX_ENTRY_NODE) { + leVCN vcn; + } + std::mem::AlignTo<8>; +}; + +// ------------------------------ +// INDEX ENTRY OBJECT ID PARSER +// ------------------------------ +struct INDEX_ENTRY_OBJ_ID { + INDEX_ENTRY_HEADER header; + + if (!(header.flags & INDEX_ENTRY_FLAGS::INDEX_ENTRY_END)) { + type::GUID key; + } + + if (header.flags & INDEX_ENTRY_FLAGS::INDEX_ENTRY_NODE) { + leVCN vcn; + } + std::mem::AlignTo<8>; +}; + +// ------------------------------ +// INDEX ENTRY REPARSE POINT PARSER +// ------------------------------ +struct INDEX_ENTRY_REPARSE { + INDEX_ENTRY_HEADER header; + + if (!(header.flags & INDEX_ENTRY_FLAGS::INDEX_ENTRY_END)) { + REPARSE_INDEX_KEY key; + } + + if (header.flags & INDEX_ENTRY_FLAGS::INDEX_ENTRY_NODE) { + leVCN vcn; + } + std::mem::AlignTo<8>; +}; + +// ------------------------------ +// INDEX ENTRY SID PARSER +// ------------------------------ +struct INDEX_ENTRY_SID { + INDEX_ENTRY_HEADER header; + + if (!(header.flags & INDEX_ENTRY_FLAGS::INDEX_ENTRY_END)) { + SID key; + } + + if (header.flags & INDEX_ENTRY_FLAGS::INDEX_ENTRY_NODE) { + leVCN vcn; + } + std::mem::AlignTo<8>; +}; + +// ------------------------------ +// INDEX ENTRY OWNER ID PARSER +// ------------------------------ +struct INDEX_ENTRY_OWNER_ID { + INDEX_ENTRY_HEADER header; + + if (!(header.flags & INDEX_ENTRY_FLAGS::INDEX_ENTRY_END)) { + u64 key; + } + + if (header.flags & INDEX_ENTRY_FLAGS::INDEX_ENTRY_NODE) { + leVCN vcn; + } + std::mem::AlignTo<8>; +}; + +// ------------------------------ +// INDEX FLAGS FUNC +// ------------------------------ +fn index_get_flags(u64 offset) { + INDEX_ENTRY_HEADER index_entry_header @ offset; + return index_entry_header.flags; +}; + +// ------------------------------ +// INDEX BLOCK PARSER +// ------------------------------ +struct INDEX_BLOCK { + NTFS_RECORD header [[inline]]; + leLSN lsn; + leVCN index_block_vcn; + + u32 index_head = $; + INDEX_HEADER index; + + if (index.entries_offset > sizeof (index)) { + padding[index.entries_offset - sizeof (index)]; + } + + INDEX_ENTRY_FILE_NAME ents[while(!(index_get_flags($) & INDEX_ENTRY_FLAGS::INDEX_ENTRY_END))]; + INDEX_ENTRY_FILE_NAME ent_end; + + u32 index_used = $ - index_head; + if (index.index_length > index_used) { + padding[index.index_length - index_used]; + } +}; + +// ------------------------------ +// INDEX ROOT PARSER +// ------------------------------ +struct INDEX_ROOT { + ATTR_TYPES type; + COLLATION_RULES collation_rule; + u32 index_block_size; + s8 clusters_per_index_block; + u8 reserved[3] [[static, sealed]]; + + u32 index_head = $; + INDEX_HEADER index; + + if (index.entries_offset > sizeof (index)) { + padding[index.entries_offset - sizeof (index)]; + } + + match (type) { + (ATTR_TYPES::AT_FILE_NAME): { + INDEX_ENTRY_FILE_NAME ents[while(!(index_get_flags($) & INDEX_ENTRY_FLAGS::INDEX_ENTRY_END))]; + INDEX_ENTRY_FILE_NAME ent_end; + } + } + + u32 index_used = $ - index_head; + if (index.index_length > index_used) { + padding[index.index_length - index_used]; + } +}; + +// ------------------------------------------------------------------------- +// RESTART AREA RELATED +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- +// ------------------------------ +// RESTART AREA HEADER PARSER +// ------------------------------ +struct RESTART_PAGE_HEADER { + NTFS_RECORD_TYPES magic; + u16 usa_ofs; + u16 usa_count; + leLSN chkdsk_lsn; + u32 system_page_size; + u32 log_page_size; + u16 restart_area_offset; + u16 minor_ver; + u16 major_ver; + u16 usn; +}; + +// ------------------------------ +// RESTART AREA FLAG PARSER +// ------------------------------ +enum RESTART_AREA_FLAGS : u16 { + RESTART_VOLUME_IS_CLEAN = 0x0002, + RESTART_SPACE_FILLER = 0xffff, +}; + +// ------------------------------ +// RESTART AREA PARSER +// ------------------------------ +struct RESTART_AREA { + leLSN current_lsn; + u16 log_clients; + u16 client_free_list; + u16 client_in_use_list; + RESTART_AREA_FLAGS flags; + u32 seq_number_bits; + u16 restart_area_length; + u16 client_array_offset; + u64 file_size; + u32 last_lsn_data_length; + u16 log_record_header_length; + u16 log_page_data_offset; + u32 restart_log_open_count; + u32 reserved; +}; + +// ------------------------------------------------------------------------- +// LOG CLIENT RELATED +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- +// ------------------------------ +// LOG CLIENT PARSER +// ------------------------------ +struct LOG_CLIENT_RECORD { + leLSN oldest_lsn; + leLSN client_restart_lsn; + u16 prev_client; + u16 next_client; + u16 seq_number; + u8 reserved[6]; + u32 client_name_length; + //ntfschar client_name[64]; + char16 client_name[64]; +}; + +// ------------------------------------------------------------------------- +// ATTRIBUTE RELATED +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- +// ------------------------------ +// xD0 ATTRIBUTE PARSER +// ------------------------------ +struct EA_INFO_ENTRY { + u16 EA_packed_size [[comment("Size of the Packed Extended Attributes")]]; + u16 EA_Count [[comment("Number of Extended Attributes which have NEED_EA set")]]; + u32 EA_unpacked_size [[comment("Size of the Unpacked Extended Attributes")]]; +}; + +// ------------------------------ +// xE0 ATTRIBUTE PARSER +// ------------------------------ +struct EA_ENTRY { + u32 entry_size [[comment("EA Size: Offset to Next Extended Attribute")]]; + u8 flags [[comment("Flags: 0x80 = Needs EA")]]; + u8 name_length [[comment("EA Name Size")]]; + u16 value_length [[comment("EA Value Size")]]; + char name[name_length] [[comment("EA Name: LXUID/LXGID/LXMOD/ETC")]]; + padding[2]; + char value[value_length] [[comment("EA Value: Related to HPFS+ Compatibility")]]; +}; + +// ------------------------------ +// ATTRIBUTE DEFS +// ------------------------------ +#define ATTR_RECORD_HEADER_SIZE (16) +#define ATTR_RECORD_RESIDENT_SIZE (16 + 8) +#define ATTR_RECORD_NONRESIDENT_SIZE (16 + 48) +#define ATTR_RECORD_NONRESIDENT_CMPR_SIZE (16 + 48 + 8) + +// ------------------------------ +// ATTRIBUTE HEADER +// ------------------------------ +struct ATTRHeader { + ATTR_TYPES type [[comment("Attribute Identifier")]]; + u32 length [[comment("Size of this ATTR")]]; + u8 non_resident [[comment("0 = Resident | 1 = Non-Resident")]]; + u8 name_length [[comment("Size of the ATTR Stream Name, if any")]]; + u16 name_offset [[comment("Offset to the ATTR Stream Name")]]; + u16 flags [[comment("0x0001 = Compressed | 0x4000 = Encrypted | 0x8000 = Sparse")]]; // ATTR_FLAGS + u16 instance [[comment("Sequential Order: 0x0000 = 1st")]]; + + u32 name_offset_delta = 0; + + if (!non_resident) { + u32 value_length [[comment("Size of Resident Data")]]; + u16 value_offset [[comment("Offset to Resident Data")]]; + RESIDENT_ATTR_FLAGS resident_flags [[comment("Indexed Flag: 0x01 = Indexed | 0x00 = Not Indexed")]]; + s8 reservedR [[comment("Reserved")]]; + + //if (name_offset > ATTR_RECORD_RESIDENT_SIZE) { + // name_offset_delta = name_offset - ATTR_RECORD_RESIDENT_SIZE; + // padding[name_offset_delta]; + //} + + } +}; + +// ------------------------------ +// ATTRIBUTE BODY NON_RESIDENT +// ------------------------------ +struct ATTRBody { + leVCN lowest_vcn [[comment("Start Virtual Cluster Number")]]; + leVCN highest_vcn [[comment("End Virtual Cluster Number")]]; + u16 mapping_pairs_offset [[comment("Offset to RunList")]]; + u8 compression_unit [[comment("Compression Unit Size")]]; + u8 reserved1[5] [[comment("Reserved")]]; + u64 allocated_size [[comment("Size on Disk")]]; + u64 data_size [[comment("Logical File Size")]]; + u64 initialized_size [[comment("Initialized Size: Typical for Downloads")]]; +}; + +// ------------------------------ +// ATTRIBUTE RECORD BODY RELATED +// ------------------------------ +struct ATTR_RECORD { + ATTRHeader ATTR_HEADER [[name("Header")]]; + + if (ATTR_HEADER.non_resident) { + ATTRBody x80_xB0_Body [[name(attr_type_name(u32(ATTR_HEADER.type)) + "_Body")]]; + + if (ATTR_HEADER.flags & ATTR_FLAGS::ATTR_IS_COMPRESSED) { + u64 compressed_size [[comment("Compressed Size")]]; + + if (ATTR_HEADER.name_offset > ATTR_RECORD_NONRESIDENT_CMPR_SIZE) { + name_offset_delta = ATTR_HEADER.name_offset - ATTR_RECORD_NONRESIDENT_CMPR_SIZE; + padding[name_offset_delta]; + } + } else { + if (ATTR_HEADER.name_offset > ATTR_RECORD_NONRESIDENT_SIZE) { + name_offset_delta = ATTR_HEADER.name_offset - ATTR_RECORD_NONRESIDENT_SIZE; + padding[name_offset_delta]; + } + } + } + + u32 name_length_bytes = ATTR_HEADER.name_length * sizeof (ntfschar); + + if (ATTR_HEADER.name_length > 0) { + char16 name[ATTR_HEADER.name_length]; + } + + if (ATTR_HEADER.non_resident) { + if (ATTR_HEADER.flags & ATTR_FLAGS::ATTR_IS_COMPRESSED) { + if (x80_xB0_Body.mapping_pairs_offset > (ATTR_RECORD_NONRESIDENT_CMPR_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes)) { + padding[x80_xB0_Body.mapping_pairs_offset - (ATTR_RECORD_NONRESIDENT_CMPR_SIZE + name_length_bytes)]; + } + } else { + if (x80_xB0_Body.mapping_pairs_offset > (ATTR_RECORD_NONRESIDENT_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes)) { + padding[x80_xB0_Body.mapping_pairs_offset - (ATTR_RECORD_NONRESIDENT_SIZE + name_length_bytes)]; + } + } + + RUNLIST x80_xB0_RUNLIST[] [[name(attr_type_name(u32(ATTR_HEADER.type)) + "_RUNLIST"), comment("$DATA or $BITMAP Run")]]; + } else { + + u32 value_offset_delta = 0; + if (ATTR_HEADER.value_offset > (ATTR_RECORD_RESIDENT_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes)) { + value_offset_delta = ATTR_HEADER.value_offset - (ATTR_RECORD_RESIDENT_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes); + padding[value_offset_delta]; + } + + match (ATTR_HEADER.type) { + (ATTR_TYPES::AT_UNUSED): { + u32 attr_sum = (ATTR_RECORD_RESIDENT_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes + value_offset_delta); + if (ATTR_HEADER.length > attr_sum) { + padding[ATTR_HEADER.length - attr_sum]; + } + } + (ATTR_TYPES::AT_STANDARD_INFORMATION): { + if (ATTR_HEADER.value_length > sizeof(STANDARD_INFORMATION_OLD)) { + STANDARD_INFORMATION x10_Body [[name(attr_type_name(u32(ATTR_HEADER.type)) + "_Body")]]; + + } else { + STANDARD_INFORMATION_OLD x10_Body [[name(attr_type_name(u32(ATTR_HEADER.type)) + "_Body")]]; + } + + u32 attr_sum = (ATTR_RECORD_RESIDENT_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes + value_offset_delta + sizeof (x10_Body)); + if (ATTR_HEADER.length > attr_sum) { + padding[ATTR_HEADER.length - attr_sum]; + } + } + (ATTR_TYPES::AT_ATTRIBUTE_LIST): { + u32 attr_sum = (ATTR_RECORD_RESIDENT_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes + value_offset_delta); + if (ATTR_HEADER.length > attr_sum) { + padding[ATTR_HEADER.length - attr_sum]; + } + } + (ATTR_TYPES::AT_FILE_NAME): FILE_NAME_ATTR x30_Body [[name(attr_type_name(u32(ATTR_HEADER.type)) + "_Body")]]; + (ATTR_TYPES::AT_OBJECT_ID): { + u32 attr_sum = (ATTR_RECORD_RESIDENT_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes + value_offset_delta); + if (ATTR_HEADER.length > attr_sum) { + padding[ATTR_HEADER.length - attr_sum]; + } + } + (ATTR_TYPES::AT_SECURITY_DESCRIPTOR): { + SECURITY_DESCRIPTOR_RELATIVE x50_Body [[name(attr_type_name(u32(ATTR_HEADER.type)) + "_Body")]]; + u32 attr_sum = (ATTR_RECORD_RESIDENT_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes + value_offset_delta + sizeof (x50_Body)); + if (ATTR_HEADER.length > attr_sum) { + padding[ATTR_HEADER.length - attr_sum]; + } + } + //(ATTR_TYPES::AT_VOLUME_NAME): char16 vol_name[ATTR_HEADER.value_length / sizeof (ntfschar)] [[name(attr_type_name(u32(ATTR_HEADER.type)) + "_Body"), comment("User Defined Volume Name")]]; + (ATTR_TYPES::AT_VOLUME_NAME): { char16 vol_name[ATTR_HEADER.value_length / sizeof (ntfschar)] [[name(attr_type_name(u32(ATTR_HEADER.type)) + "_Body"), comment("User Defined Volume Name")]]; + Vol_Label = str(vol_name); + } + + (ATTR_TYPES::AT_VOLUME_INFORMATION): VOLUME_INFORMATION vol_info; + (ATTR_TYPES::AT_DATA): { + char x80_xB0_Stream[ATTR_HEADER.value_length] [[name(attr_type_name(u32(ATTR_HEADER.type)) + "_Body")]]; + u32 attr_sum = (ATTR_RECORD_RESIDENT_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes + value_offset_delta + sizeof (x80_xB0_Stream)); + if (ATTR_HEADER.length > attr_sum) { + padding[ATTR_HEADER.length - attr_sum]; + } + } + (ATTR_TYPES::AT_INDEX_ROOT): { + INDEX_ROOT x90_Body [[name(attr_type_name(u32(ATTR_HEADER.type)) + "_Body")]]; + } + (ATTR_TYPES::AT_INDEX_ALLOCATION): { + u32 attr_sum = (ATTR_RECORD_RESIDENT_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes + value_offset_delta); + if (ATTR_HEADER.length > attr_sum) { + padding[ATTR_HEADER.length - attr_sum]; + } + } + (ATTR_TYPES::AT_BITMAP): u8 buffer[ATTR_HEADER.value_length] [[name(attr_type_name(u32(ATTR_HEADER.type)) + "_Body")]]; + (ATTR_TYPES::AT_REPARSE_POINT): { + u32 attr_sum = (ATTR_RECORD_RESIDENT_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes + value_offset_delta); + if (ATTR_HEADER.length > attr_sum) { + padding[ATTR_HEADER.length - attr_sum]; + } + } + (ATTR_TYPES::AT_EA_INFORMATION): { + u64 attr_start = $ - (ATTR_HEADER.non_resident ? ATTR_RECORD_NONRESIDENT_SIZE : ATTR_RECORD_RESIDENT_SIZE); + EA_INFO_ENTRY xD0_Body[while($ - (attr_start + ATTR_HEADER.value_offset) < ATTR_HEADER.value_length)] [[name(attr_type_name(u32(ATTR_HEADER.type)) + "_Body")]]; + } + (ATTR_TYPES::AT_EA): { + u64 attr_start = $ - (ATTR_HEADER.non_resident ? ATTR_RECORD_NONRESIDENT_SIZE : ATTR_RECORD_RESIDENT_SIZE); + EA_ENTRY xE0_Body[while($ - (attr_start + ATTR_HEADER.value_offset) < ATTR_HEADER.value_length)] [[name(attr_type_name(u32(ATTR_HEADER.type)) + "_Body")]]; + } + (ATTR_TYPES::AT_PROPERTY_SET): { + u32 attr_sum = (ATTR_RECORD_RESIDENT_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes + value_offset_delta); + if (ATTR_HEADER.length > attr_sum) { + padding[ATTR_HEADER.length - attr_sum]; + } + } + (ATTR_TYPES::AT_LOGGED_UTILITY_STREAM): { + u32 attr_sum = (ATTR_RECORD_RESIDENT_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes + value_offset_delta); + if (ATTR_HEADER.length > attr_sum) { + padding[ATTR_HEADER.length - attr_sum]; + } + } + (ATTR_TYPES::AT_FIRST_USER_DEFINED_ATTRIBUTE): { + u32 attr_sum = (ATTR_RECORD_RESIDENT_SIZE + ATTR_HEADER.name_offset_delta + name_length_bytes + value_offset_delta); + if (ATTR_HEADER.length > attr_sum) { + padding[ATTR_HEADER.length - attr_sum]; + } + } + (ATTR_TYPES::AT_END): { + //length is no longer valid + } + } + } + std::mem::AlignTo<8>; +}; + +// ------------------------------ +// ATTRIBUTE TYPE FUNC +// ------------------------------ +fn attr_get_type(u64 offset) { + ATTR_RECORD ATTR @ offset; + return ATTR.ATTR_HEADER.type; +}; + +// ------------------------------------------------------------------------- +// FILE RECORD ENTRY RELATED +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- + +// ------------------------------ +// PRIMARY FILE RECORD HEADER PARSER +// ------------------------------ +struct NTFS_RECORD_HDR { + NTFS_RECORD_TYPES magic [[comment("File Record Signature 'FILE'")]];; + u16 usa_ofs [[comment("Offset to Fix Up Array")]]; + u16 usa_count [[comment("# of 2 Byte Entries in FUA")]]; + leLSN lsn [[comment("$LogFile Sequence #")]]; + u16 sequence_number [[comment("Count of FR Entry Usage: 1=Once/Not Del")]]; + u16 link_count [[comment("(HARDLINK) Count of File Name ATTR")]]; + u16 attr_offset [[comment("Offset to 1st ATTR")]]; + MFT_RECORD_FLAGS flags [[comment("Allocation Status Flags: 0x00=Del File||0x01=Alloc.File||0x02=Del DIR||0x03=Alloc.DIR")]];; + u32 bytes_in_use [[comment("Logical Size of FR Entry")]]; + u32 bytes_allocated [[comment("Phys (Alloc.) Size of FR Entry")]]; + leMFT_REF base_mft_record [[comment("Ref to additional FR Entry, if any")]]; + u16 next_attr_instance [[comment("Count of ATTRs")]]; + u16 FU_Array [[comment("Fix Up Array and ATTR-NT3.0")]]; // 2 bytes NTFS 3.0 (LEGACY) + + if (FU_Array <= 0) { // FU_Array not in use -- NTFS 3.1+ + u32 mft_record_number [[comment("MFT File Record Num")]]; // 4 bytes + mft_num = mft_record_number; + seq_num = sequence_number; + + if (usa_count > 0) { + u16 update_sequence[usa_count] [[comment("Fix Up Array and ATTR-NT3.1+")]]; + } + } else { + padding[4]; + mft_num += 0x01; + } +}; + +// ------------------------------ +// QUICK ATTRIBUTE CHECK +// ------------------------------ +fn attr_type_name(u32 type) { + if (type == ATTR_TYPES::AT_STANDARD_INFORMATION) return "STANDARD_INFORMATION"; + if (type == ATTR_TYPES::AT_ATTRIBUTE_LIST) return "ATTRIBUTE_LIST"; + if (type == ATTR_TYPES::AT_FILE_NAME) return "FILE_NAME"; + if (type == ATTR_TYPES::AT_OBJECT_ID) return "OBJECT_ID"; + if (type == ATTR_TYPES::AT_SECURITY_DESCRIPTOR) return "SECURITY_DESCRIPTOR"; + if (type == ATTR_TYPES::AT_VOLUME_NAME) return "VOLUME_NAME"; + if (type == ATTR_TYPES::AT_VOLUME_INFORMATION) return "VOLUME_INFORMATION"; + if (type == ATTR_TYPES::AT_DATA) return "DATA"; + if (type == ATTR_TYPES::AT_INDEX_ROOT) return "INDEX_ROOT"; + if (type == ATTR_TYPES::AT_INDEX_ALLOCATION) return "INDEX_ALLOCATION"; + if (type == ATTR_TYPES::AT_BITMAP) return "BITMAP"; + if (type == ATTR_TYPES::AT_REPARSE_POINT) return "REPARSE_POINT"; + if (type == ATTR_TYPES::AT_EA_INFORMATION) return "EA_INFORMATION"; + if (type == ATTR_TYPES::AT_EA) return "EA"; + if (type == ATTR_TYPES::AT_PROPERTY_SET) return "PROPERTY_SET"; + if (type == ATTR_TYPES::AT_LOGGED_UTILITY_STREAM) return "LOGGED_UTILITY_STREAM"; + return "UNKNOWN"; +}; + +fn peek_attr_type(u64 offset) { + return std::mem::read_unsigned(offset, 4); // first 4 bytes of ATTRHeader +}; + +// ------------------------------ +// ATTRIBUTE WRAPPER +// ------------------------------ +struct ATTR_WRAPPER { + ATTR_RECORD ATTR [[name(attr_type_name(peek_attr_type($)))]]; +} [[inline, name(attr_type_name(peek_attr_type($)))]]; + +// ------------------------------ +// PATTERN NAMING FUNC +// ------------------------------ +fn get_file_name(auto ATTRS) { + str fname = ""; + + for (u32 i = 0, i < std::core::member_count(ATTRS), i = i + 1) { + + if (ATTRS[i].ATTR.ATTR_HEADER.type == ATTR_TYPES::AT_FILE_NAME) { + u8 len = ATTRS[i].ATTR.x30_Body.file_name_length; + + for (u32 j = 0, j < len, j = j + 1) { + char16 cu = ATTRS[i].ATTR.x30_Body.file_name[j]; + u8 c = u8(cu); // Cast to low byte + fname = std::format("{}{}", fname, char(c)); + } + break; + } + } + return fname; +}; + +// ------------------------------ +// PRIMARY FILE RECORD PARSER +// ------------------------------ +u64 start_index = 12; // First 12 are hard-coded - We're parsing the rest of the MFT Entries + +struct MFT_RECORD { + u64 record_start_offset = $; + NTFS_RECORD_HDR FR_HEADER; + + if (FR_HEADER.flags == 0x00 || FR_HEADER.flags == 0x02) { + Unused_Count += 1; + } + if (FR_HEADER.flags == 0x01 || FR_HEADER.flags == 0x03) { + MFT_Count += 1; + } + + std::mem::AlignTo<8>; + ATTR_WRAPPER ATTRS[while(attr_get_type($) != ATTR_TYPES::AT_END)] [[inline]]; + str filename = get_file_name(ATTRS); + + if (filename == "") { + PRENT_MFT = 0x00; + PRENT_SEQ = 0x00; + filename = "FILE_REC"; + } + + ATTR_RECORD ATTR_END [[static, name("ATTR_END")]] ; + std::mem::AlignTo; + +} [[name(format_element(filename, $, start_index)), + comment("DEC:[MFT#][SEQ#] | HEX:[MFT#][SEQ#] | PARENT_HEX:[MFT#][SEQ#]")]]; + +fn format_element(auto filename, auto v, u64 offset) { + return std::format("{} [{:02}][{:02}] | [x{:02X}][x{:02X}] | PRENT:[x{:02X}][x{:02X}]", filename, mft_num, seq_num, mft_num, seq_num, PRENT_MFT, PRENT_SEQ); +}; + + +// ------------------------------------------------------------------------- +// VOLUME BOOT RECORD RELATED +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- +// ------------------------------ +// NTFS VBR +// ------------------------------ +struct NTFS_BOOT_SECTOR { + u8 jmp_boot[3] [[comment("Jump Instructions")]]; + char oem_id[8] [[comment("FS IDENTIFIER")]]; + BIOS_PARAMETER_BLOCK bpb; + u8 physical_drive [[comment("Legacy")]]; + u8 current_head [[comment("Legacy")]]; + u8 extended_boot_signature [[comment("Legacy")]]; + u8 reserved1 [[comment("Legacy")]]; + u64 total_sectors [[comment("Volume Size in Sectors")]]; + u64 mft_lcn [[comment("MFT Start Cluster")]]; + u64 mftmirr_lcn [[comment("MFTMirror Start Cluster")]]; + u8 clusters_per_mft_record [[comment("Size of MFT/Mirror in Clusters")]]; + u8 reserved2[3] [[comment("Unused")]]; + u8 clusters_per_index_buffer [[comment("Size of Index Buffer")]]; + u8 reserved3[3] [[comment("Unused")]]; + u64 volume_serial [[comment("Volume Serial #")]]; + u32 checksum [[comment("VBR Sector CRC32")]]; + u8 bootstrap[426] [[comment("Boot-strapping Instructions")]]; + VBRSignature VBR_SIG [[comment("End of VBR - 0x55AA")]]; +}; + +// ------------------------------ +// $DATA FUNC HELPER FOR INDEXES +// ------------------------------ +fn mft_get_dat_attr_lcn(MFT_RECORD record, ATTR_TYPES type = ATTR_TYPES::AT_DATA) { + u64 lcn = 0; + + for (u64 i = 0, i < record.FR_HEADER.next_attr_instance, i += 1) { + if (record.ATTRS[i].ATTR.ATTR_HEADER.type == type) { + for (s64 j = record.ATTRS[i].ATTR.x80_xB0_RUNLIST[0].RunList_Header.SE_Offset - 1, j >= 0, j -= 1) { + lcn |= record.ATTRS[i].ATTR.x80_xB0_RUNLIST[0].Logical_Cluster_Num[j] << (8 * j); + } + break; + } + } + return lcn; +}; + +// ------------------------------------------------------------------------- +// MAIN ENTRY RELATED +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- + +// ============= VBR ============================================================================================= +NTFS_BOOT_SECTOR nbs @ $ [[name("NTFS_VBR")]]; +u32 cluster_size = nbs.bpb.bytes_per_sector * nbs.bpb.sectors_per_cluster; +u32 mft_rec_size = 1 << -nbs.clusters_per_mft_record; + +VBR_OFFSET = $ + (nbs.bpb.hidden_sectors - 1) * nbs.bpb.bytes_per_sector; + +// ============= $MFT ============================================================================================ +MFT_RECORD mft @ nbs.mft_lcn * cluster_size + 0 * mft_rec_size + [[name(std::format("$MFT [{:02}][{:02}] | [x{:02X}][x{:02X}] | PRENT:[x{:02X}][x{:02X}]", + mft_num, seq_num, mft_num, seq_num, PRENT_MFT, PRENT_SEQ))]]; + +MFT_RECORD mftmirr @ nbs.mftmirr_lcn * cluster_size + [[name(std::format("$MFT COPY (DERIVED FROM $MFTMirr)"))]]; + +// ============= $MFTMirr ======================================================================================== +MFT_RECORD mft_mirr @ nbs.mft_lcn * cluster_size + 1 * mft_rec_size + [[name(std::format("$MFT Mirror [{:02}][{:02}] | [x{:02X}][x{:02X}] | PRENT:[x{:02X}][x{:02X}]", + mft_num, seq_num, mft_num, seq_num, PRENT_MFT, PRENT_SEQ))]]; + +// ============= $LogFile ======================================================================================== +MFT_RECORD mft_log @ nbs.mft_lcn * cluster_size + 2 * mft_rec_size + [[name(std::format("$LogFile [{:02}][{:02}] | [x{:02X}][x{:02X}] | PRENT:[x{:02X}][x{:02X}]", + mft_num, seq_num, mft_num, seq_num, PRENT_MFT, PRENT_SEQ))]]; + +// ============= $Volume ========================================================================================= +MFT_RECORD mft_vol @ nbs.mft_lcn * cluster_size + 3 * mft_rec_size + [[name(std::format("$Volume [{:02}][{:02}] | [x{:02X}][x{:02X}] | PRENT:[x{:02X}][x{:02X}]", + mft_num, seq_num, mft_num, seq_num, PRENT_MFT, PRENT_SEQ))]]; + +// ============= $AttrDef ======================================================================================== +MFT_RECORD mft_adef @ nbs.mft_lcn * cluster_size + 4 * mft_rec_size + [[name(std::format("$AttrDef [{:02}][{:02}] | [x{:02X}][x{:02X}] | PRENT:[x{:02X}][x{:02X}]", + mft_num, seq_num, mft_num, seq_num, PRENT_MFT, PRENT_SEQ))]]; + +ATTR_DEF adef_dat_buf[16] @ mft_get_dat_attr_lcn(mft_adef) * cluster_size + [[name("$AttrDef ARRAY (DERIVED)")]]; + +// ============= $I30 (Root) ===================================================================================== +MFT_RECORD mft_root @ nbs.mft_lcn * cluster_size + 5 * mft_rec_size + [[name(std::format("$I30 (Root) [{:02}][{:02}] | [x{:02X}][x{:02X}] | PRENT:[x{:02X}][x{:02X}]", + mft_num, seq_num, mft_num, seq_num, PRENT_MFT, PRENT_SEQ))]]; +INDEX_BLOCK root_index_block @ mft_get_dat_attr_lcn(mft_root, ATTR_TYPES::AT_INDEX_ALLOCATION) * cluster_size + [[name(std::format("$I30 (Root) INDEX_BLOCK (DERIVED)"))]]; + +// ============= $Bitmap ========================================================================================= +MFT_RECORD mft_bm @ nbs.mft_lcn * cluster_size + 6 * mft_rec_size + [[name(std::format("$Bitmap [{:02}][{:02}] | [x{:02X}][x{:02X}] | PRENT:[x{:02X}][x{:02X}]", + mft_num, seq_num, mft_num, seq_num, PRENT_MFT, PRENT_SEQ))]]; + +// ============= $Boot =========================================================================================== +MFT_RECORD mft_boot @ nbs.mft_lcn * cluster_size + 7 * mft_rec_size + [[name(std::format("$Boot [{:02}][{:02}] | [x{:02X}][x{:02X}] | PRENT:[x{:02X}][x{:02X}]", + mft_num, seq_num, mft_num, seq_num, PRENT_MFT, PRENT_SEQ))]]; + +// ============= $BadClus ======================================================================================== +MFT_RECORD mft_badclus @ nbs.mft_lcn * cluster_size + 8 * mft_rec_size + [[name(std::format("$BadClus [{:02}][{:02}] | [x{:02X}][x{:02X}] | PRENT:[x{:02X}][x{:02X}]", + mft_num, seq_num, mft_num, seq_num, PRENT_MFT, PRENT_SEQ))]]; + +// ============= $Secure ($Quota for NTFS 1.2) =================================================================== +MFT_RECORD mft_sec @ nbs.mft_lcn * cluster_size + 9 * mft_rec_size + [[name(std::format("$Secure [{:02}][{:02}] | [x{:02X}][x{:02X}] | PRENT:[x{:02X}][x{:02X}]", + mft_num, seq_num, mft_num, seq_num, PRENT_MFT, PRENT_SEQ))]]; + +// ============= $UpCase ========================================================================================= +MFT_RECORD mft_uc @ nbs.mft_lcn * cluster_size + 10 * mft_rec_size + [[name(std::format("$UpCase [{:02}][{:02}] | [x{:02X}][x{:02X}] | PRENT:[x{:02X}][x{:02X}]", + mft_num, seq_num, mft_num, seq_num, PRENT_MFT, PRENT_SEQ))]]; +u8 uc_tbl[1] @ mft_get_dat_attr_lcn(mft_uc) * cluster_size [[name("$UpCase TABLE (DERIVED)")]]; + +// ============= $Extend ========================================================================================= +MFT_RECORD mft_ext @ nbs.mft_lcn * cluster_size + 11 * mft_rec_size + [[name(std::format("$Extend [{:02}][{:02}] | [x{:02X}][x{:02X}] | PRENT:[x{:02X}][x{:02X}]", + mft_num, seq_num, mft_num, seq_num, PRENT_MFT, PRENT_SEQ))]]; + +//============== RESERVED / UNUSED================================================================================ +// RESERVED/UNUSED FILE RECORD ENTRIES 12-23 | 0x0C-0x17 (13th-24th) + +// ============= NEXT RECORDS ==================================================================================== +// ---- FUNC TO CHECK FILE RECORD MAGIC +fn fr_magic(u64 abs_off) { + NTFS_RECORD_TYPES magic @ abs_off; + return magic; +}; + +// ---- CALL THE NEXT RECORDS ARRAY +MFT_RECORD NEXT_RECORDS[while(fr_magic($) == NTFS_RECORD_TYPES::magic_FILE)] @ (nbs.mft_lcn * cluster_size + 12 * mft_rec_size) [[inline, static]]; + +// ============= REPORT ========================================================================================== +// ------------------------------ +// NTFS VOLUME REPORT +// *** HAS GLOBAL AT TOP *** +// ------------------------------ +u64 mft_offset = VBR_OFFSET + nbs.mft_lcn * cluster_size + 0 * mft_rec_size; +u64 mft_mrr_offset = VBR_OFFSET + nbs.mftmirr_lcn * cluster_size; +u64 rootDir_offset = VBR_OFFSET + mft_get_dat_attr_lcn(mft_root, ATTR_TYPES::AT_INDEX_ALLOCATION) * cluster_size; + +if (VOLUME_REPORT) { + std::print("-----------------------------------------"); + std::print("------------- VOLUME_REPORT -------------"); + std::print("-----------------------------------------"); + std::print("VOL_LABEL = {}", Vol_Label); + std::print("NTFS_VERS = {}.{}", NT_Major, NT_Minor); + std::print("SECTOR_SIZE = {:02} BYTES", nbs.bpb.bytes_per_sector); + std::print("CLUSTER_SIZE = {:02} BYTES", cluster_size); + std::print("VOLUME_SIZE = {:02} SECTORS", nbs.total_sectors); + std::print("VOLUME_SIZE = {:.4f} GB @ 1000", (nbs.total_sectors * nbs.bpb.bytes_per_sector) / 1000.0 / 1000.0 / 1000.0); + std::print("VOLUME_SIZE = {:.4f} GiB @ 1024", (nbs.total_sectors * nbs.bpb.bytes_per_sector) / 1024.0 / 1024.0 / 1024.0); + + std::print("-----------------------------------------"); + std::print("MFT_OFFSET = {} | 0x{:02X}", mft_offset, mft_offset); + std::print("MFT_RECORDS = ~{:02} IN USE", MFT_Count); + std::print("MFT_RECORDS = ~{:02} NOT IN USE", Unused_Count); + + std::print("-----------------------------------------"); + std::print("MFT_COPY_OFFSET = {} | 0x{:02X}", mft_mrr_offset, mft_mrr_offset); + std::print("ROOT_DIR_OFFSET = {} | 0x{:02X}", rootDir_offset, rootDir_offset); + std::print("-----------------------------------------"); + std::print("------------------ END ------------------"); + std::print("-----------------------------------------"); + std::print(" "); +} \ No newline at end of file diff --git a/patterns/DFIR/exFAT.hexpat b/patterns/DFIR/exFAT.hexpat new file mode 100644 index 00000000..3f8975a1 --- /dev/null +++ b/patterns/DFIR/exFAT.hexpat @@ -0,0 +1,616 @@ +#pragma author Formula Zero One Technologies +#pragma description exFAT Filesystem (exFAT_v2.0) +#pragma MIME application/x-ima +#pragma endian little + +// ----------------------------------------------------------------------------- +// CREDIT +// ----------------------------------------------------------------------------- +// Based on /fs/exfat.hexpat by WerWolv +// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// NOTES +// ----------------------------------------------------------------------------- +// Imported by DISK_PARSER.hexpat +// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// TODO +// ----------------------------------------------------------------------------- +// Recursive parsing of Root Directory / SubDirs +// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// IMPORTS +// ----------------------------------------------------------------------------- +import std.core; +import std.io; +import std.time; +import std.mem; +import type.guid; +import type.magic; +import type.base; + +// ------------------------------ +// FORWARD DECS/GLOBALS +// ------------------------------ + +// *** ATTENTION *** +// SET MAXIMUM NUMBER OF 4 BYTE CHUNKS TO PARSE FROM FAT1 +// SET MAXIMUM NUMBER OF DIRECTORY ENTRIES TO PARSE FROM ROOT DIRECTORY +// DEFAULTS ARE 4096 | 2500 +// Choose a value greater than 1 and less than 65536 OR increase the Array size limit with "#define... " + +// -------**************---vvvv--- | +const u64 MAX_FAT_CHUNKS = 4096; +// -------**************---^^^^--- | + +// -------**************---vvvv--- | +const u64 MAX_DIR_ENTRIES = 2500; +// -------**************---^^^^--- | + +// *** ATTENTION *** +// ---*******---*******----vvvv--- | +const bool VOLUME_REPORT = true; +// ---*******---*******----^^^^--- | + +u64 allocated_file_count; +u64 rdc; + +// -------------------------- +// exFAT DIRECTORY ENTRY HELPER +// -------------------------- +enum EntryType : u8 { + UNUSED_ENTRY = 0x00, + ACTIVE_VOLUME_GUID_ENTRY = 0xA0, + INACTIVE_VOLUME_GUID_ENTRY = 0x20, + ACTIVE_TEXFAT_ENTRY = 0xA1, + INACTIVE_TEXFAT_ENTRY = 0x21, + ACTIVE_ACCESS_CONTROL_ENTRY = 0xA2, + INACTIVE_ACCESS_CONTROL_ENTRY = 0x22, + ACTIVE_VOLUME_LABEL_ENTRY = 0x83, + INACTIVE_VOLUME_LABEL_ENTRY = 0x03, + ACTIVE_ALLOCATION_BITMAP_ENTRY = 0x81, + INACTIVE_ALLOCATION_BITMAP_ENTRY = 0x01, + ACTIVE_UPCASE_TABLE_ENTRY = 0x82, + INACTIVE_UPCASE_TABLE_ENTRY = 0x02, + ACTIVE_FILE_INFO_ENTRY = 0x85, + INACTIVE_FILE_INFO_ENTRY = 0x05, + ACTIVE_STREAM_ENTRY = 0xC0, + INACTIVE_STREAM_ENTRY = 0x40, + ACTIVE_FILENAME_ENTRY = 0xC1, + INACTIVE_FILENAME_ENTRY = 0x41, +}; + +// ------------------------------ +// DATES AND TIMES FUNC +// ------------------------------ +fn format_dos_time_field(std::time::DOSTime t) { + return std::time::format_dos_time(t, "{:02}:{:02}:{:02}"); +}; + +fn format_dos_date_field(std::time::DOSDate d) { + return std::time::format_dos_date(d, "{1:02}-{0:02}-{2:04}"); +}; + +// ------------------------------ +// BITFIELD HELPERS +// ------------------------------ +bitfield Entry_Flags { + unsigned TypeCode : 5; + unsigned Importance : 1; + unsigned Category : 1; + unsigned InUse : 1; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 8)]]; + +bitfield Bitmap_Flags { + unsigned Bitmap_1 : 1; + unsigned Bitmap_2 : 1; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 8)]]; + +bitfield General_Primary_Flags { + unsigned Allocation_Possible : 1; + unsigned No_FAT_Chain : 1; + unsigned Reserved : 6; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 8)]]; + +bitfield General_Secondary_Flags { + unsigned Allocation_Possible : 1; + unsigned No_FAT_Chain : 1; + unsigned Reserved : 6; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 8)]]; + +bitfield File_Attr_Flags { + unsigned Read_Only : 1; + unsigned Hidden : 1; + unsigned System : 1; + unsigned Directory : 1; + unsigned Archive : 1; + Reserved : 11; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 16)]]; + +// -------------------------- +// exFAT DIRECTORY ENTRY STRUCTURES +// -------------------------- +// xA0 / x20 = Volume GUID Entry +struct VolumeGUID_Entry { + Entry_Flags EntryFlags [[comment("ENTRY TYPE IDENTIFIER")]]; + u8 SecondaryCount[3] [[comment("COUNT OF SUBSEQUENT ENTRIES")]]; + type::Hex SetChecksum [[comment("16bit CHECKSUM")]]; + General_Primary_Flags PrimaryFlags; + type::GUID GUID; + u8 Reserved_1[9]; +}; + +// xA1 / x21 = TexFAT / Padding Entry +struct TexFATPadding_Entry { + Entry_Flags Flags [[comment("ENTRY TYPE IDENTIFIER")]]; + u8 Reserved_1[31]; +}; + +// xA2 / x22 = Access Control Entry +struct AccessControl_Entry { + Entry_Flags Flags [[comment("ENTRY TYPE IDENTIFIER")]]; + u8 Reserved[31]; +}; + +// x83 / x03 = Volume Label Entry +struct VolumeLabel_Entry { + Entry_Flags Flags [[comment("ENTRY TYPE IDENTIFIER")]]; + u8 LabelLength [[comment("NUMBER OF UTF-16 CHARACTERS")]]; + char16 Label[LabelLength] [[comment("VOLUME LABEL: UTF-16")]]; + u8 Reserved[32-2-(LabelLength * 2)]; +}; + +// x81 / x01 = Allocation Bitmap Entry +struct AllocationBitmap_Entry { + Entry_Flags Flags [[comment("ENTRY TYPE IDENTIFIER")]]; + Bitmap_Flags BitmapFlags; + u8 Reserved_1[18]; + u32 FirstCluster [[comment("FIRST LOGICAL CLUSTER NUMBER")]]; + u64 DataLength [[comment("DATA SIZE")]]; + u8 FILE_DATA[DataLength] @ temp_root_location + (FirstCluster - exFAT_VBR.root_dir_cluster) * bytesPerCluster [[comment("POINTER TO THE CLUSTER CONTENT")]]; +}; + +// x82 / x02 = UpCase Table Entry +struct UpCaseTable_Entry { + Entry_Flags Flags [[comment("ENTRY TYPE IDENTIFIER")]]; + u8 Reserved_1[3]; + type::Hex TableChecksum [[comment("16bit CHECKSUM")]]; + u8 Reserved_2[12]; + u32 FirstCluster [[comment("FIRST LOGICAL CLUSTER NUMBER")]]; + u64 DataLength [[comment("DATA SIZE")]]; + u8 FILE_DATA[DataLength] @ temp_root_location + (FirstCluster - exFAT_VBR.root_dir_cluster) * bytesPerCluster [[comment("POINTER TO THE CLUSTER CONTENT")]]; +}; + +// x85 / x05 = File Info Entry +struct FileInfo_Entry { + Entry_Flags EntryFlags [[comment("ENTRY TYPE IDENTIFIER")]]; + u8 SecondaryCount [[comment("COUNT OF SUBSEQUENT ENTRIES")]]; + type::Hex SetChecksum [[comment("16bit CHECKSUM")]]; + File_Attr_Flags AttrFlags [[comment("FILE ATTRS: RASH")]]; + u16 Reserved_1; + std::time::DOSTime Created_Time [[format("format_dos_time_field")]]; + std::time::DOSDate Created_Date [[format("format_dos_date_field")]]; + std::time::DOSTime Accessed_Time [[format("format_dos_time_field")]]; + std::time::DOSDate Accessed_Date [[format("format_dos_date_field")]]; + std::time::DOSTime Modified_Time [[format("format_dos_time_field")]]; + std::time::DOSDate Modified_Date [[format("format_dos_date_field")]]; + u8 Created_10ms_Increments [[comment("Add to Times for Refinement")]]; + u8 Modified_10ms_Increments [[comment("Add to Times for Refinement")]]; + s8 Created_UTC_Diff [[comment("Add to Times for Refinement")]]; + s8 Modified_UTC_Diff [[comment("Add to Times for Refinement")]]; + s8 Accessed_UTC_Diff [[comment("Add to Times for Refinement")]]; + u8 Reserved[7]; +}; + +// xC1 / x41 = File Name Entry +struct FileName_Entry { + Entry_Flags EntryFlags [[comment("ENTRY TYPE IDENTIFIER")]]; + General_Secondary_Flags SecondaryFlags [[comment("COUNT OF SUBSEQUENT ENTRIES")]]; + char16 FileName[15] [[comment("FILE NAME: UTF-16")]]; +}; + +// xC0 / x40 = Stream Extension Entry +struct Stream_Entry { + Entry_Flags EntryFlags [[comment("ENTRY TYPE IDENTIFIER")]];; + General_Secondary_Flags SecondaryFlags [[comment("COUNT OF SUBSEQUENT ENTRIES")]]; + u8 Reserved_1; + u8 NameLength [[comment("STREAM LENGTH")]]; + type::Hex NameHash [[comment("16bit QUICK HASH: USED FOR FILE SEARCHING")]]; + u16 Reserved_2; + u64 InitSize [[comment("INITIALIZED SIZE")]]; + u32 Reserved_3; + u32 FirstCluster [[comment("FIRST LOGICAL CLUSTER NUMBER")]]; + u64 ActualSize [[comment("PHYSICAL DATA SIZE")]];; + u8 FILE_DATA[InitSize] @ temp_root_location + (FirstCluster - exFAT_VBR.root_dir_cluster) * bytesPerCluster [[comment("POINTER TO THE CLUSTER CONTENT")]]; +}; + +// -------------------------- +// exFAT ROOT DIRECTORY +// -------------------------- +struct RootDir { + EntryType Type; + padding[31]; + + match (Type) { + (EntryType::UNUSED_ENTRY): { + continue; + } + (EntryType::ACTIVE_VOLUME_GUID_ENTRY | EntryType::INACTIVE_VOLUME_GUID_ENTRY):{ + VolumeGUID_Entry VOLUME_GUID_ENTRY @ $ - 32; + } + (EntryType::ACTIVE_TEXFAT_ENTRY | EntryType::INACTIVE_TEXFAT_ENTRY):{ + TexFATPadding_Entry TEXFAT_PADDING_ENTRY @ $ - 32; + } + (EntryType::ACTIVE_ACCESS_CONTROL_ENTRY | EntryType::INACTIVE_ACCESS_CONTROL_ENTRY):{ + AccessControl_Entry ACCESS_CONTROL_ENTRY @ $ - 32; + } + (EntryType::ACTIVE_VOLUME_LABEL_ENTRY | EntryType::INACTIVE_VOLUME_LABEL_ENTRY):{ + VolumeLabel_Entry VOLUME_LABEL_ENTRY @ $ - 32; + } + (EntryType::ACTIVE_ALLOCATION_BITMAP_ENTRY | EntryType::INACTIVE_ALLOCATION_BITMAP_ENTRY):{ + AllocationBitmap_Entry ALLOCATION_BITMAP_ENTRY @ $ - 32; + u64 bitmap_cluster = ALLOCATION_BITMAP_ENTRY.FirstCluster; + char dolla_BITMAP_label[4] @ FAT1_start_offset + (bitmap_cluster * 4) [[name(std::format("$Bitmap"))]]; + } + (EntryType::ACTIVE_UPCASE_TABLE_ENTRY | EntryType::INACTIVE_UPCASE_TABLE_ENTRY):{ + UpCaseTable_Entry UPCASE_TABLE_ENTRY @ $ - 32; + u64 upcase_cluster = UPCASE_TABLE_ENTRY.FirstCluster; + char dolla_UPCASE_label[4] @ FAT1_start_offset + (upcase_cluster * 4) [[name(std::format("$UpCase"))]]; + } + (EntryType::ACTIVE_FILE_INFO_ENTRY | EntryType::INACTIVE_FILE_INFO_ENTRY):{ + FileInfo_Entry FILE_INFO_ENTRY @ $ - 32; + } + (EntryType::ACTIVE_STREAM_ENTRY | EntryType::INACTIVE_STREAM_ENTRY):{ + Stream_Entry STREAM_EXT_ENTRY @ $ - 32; + } + (EntryType::ACTIVE_FILENAME_ENTRY | EntryType::INACTIVE_FILENAME_ENTRY):{ + FileName_Entry FILE_NAME_ENTRY @ $ - 32; + } + } +} [[name(format_entry_name(std::mem::read_unsigned($-32, 1), std::core::array_index()))]]; + +fn format_entry_name(auto entry_type, u64 idx) { + return std::format("{}[{}]", type_name(entry_type), idx); +}; + +// ------------------------------ +// TYPE RE-NAMER +// ------------------------------ +fn type_name(u32 type) { + if (type == EntryType::UNUSED_ENTRY) return "UNUSED_ENTRY"; + if (type == EntryType::ACTIVE_VOLUME_GUID_ENTRY) return "ACTIVE_VOLUME_GUID_ENTRY"; + if (type == EntryType::INACTIVE_VOLUME_GUID_ENTRY) return "INACTIVE_VOLUME_GUID_ENTRY"; + if (type == EntryType::ACTIVE_TEXFAT_ENTRY) return "ACTIVE_TEXFAT_ENTRY"; + if (type == EntryType::INACTIVE_TEXFAT_ENTRY) return "INACTIVE_TEXFAT_ENTRY"; + if (type == EntryType::ACTIVE_ACCESS_CONTROL_ENTRY) return "ACTIVE_ACCESS_CONTROL_ENTRY"; + if (type == EntryType::INACTIVE_ACCESS_CONTROL_ENTRY) return "INACTIVE_ACCESS_CONTROL_ENTRY"; + if (type == EntryType::ACTIVE_VOLUME_LABEL_ENTRY) return "ACTIVE_VOLUME_LABEL_ENTRY"; + if (type == EntryType::INACTIVE_VOLUME_LABEL_ENTRY) return "INACTIVE_VOLUME_LABEL_ENTRY"; + if (type == EntryType::ACTIVE_ALLOCATION_BITMAP_ENTRY) return "ACTIVE_ALLOCATION_BITMAP_ENTRY"; + if (type == EntryType::INACTIVE_ALLOCATION_BITMAP_ENTRY) return "INACTIVE_ALLOCATION_BITMAP_ENTRY"; + if (type == EntryType::ACTIVE_UPCASE_TABLE_ENTRY) return "ACTIVE_UPCASE_TABLE_ENTRY"; + if (type == EntryType::INACTIVE_UPCASE_TABLE_ENTRY) return "INACTIVE_UPCASE_TABLE_ENTRY"; + if (type == EntryType::ACTIVE_FILE_INFO_ENTRY) return "ACTIVE_FILE_INFO_ENTRY"; + if (type == EntryType::INACTIVE_FILE_INFO_ENTRY) return "INACTIVE_FILE_INFO_ENTRY"; + if (type == EntryType::ACTIVE_STREAM_ENTRY) return "ACTIVE_STREAM_ENTRY"; + if (type == EntryType::INACTIVE_STREAM_ENTRY) return "INACTIVE_STREAM_ENTRY"; + if (type == EntryType::ACTIVE_FILENAME_ENTRY) return "ACTIVE_FILENAME_ENTRY"; + if (type == EntryType::INACTIVE_FILENAME_ENTRY) return "INACTIVE_FILENAME_ENTRY"; + return "UNKNOWN"; +}; + +// ----------------------------------------------------------------------------- +// exFAT FILE ALLOCATION TABLE (FAT1) PARSER +// ----------------------------------------------------------------------------- + +const u32 CLUSTER_SIZE_BYTES = 4; // Each FAT32 entry = 4 bytes +const u32 FAT_EOF = 0x0FFFFFFF; // End-of-file marker +const u32 FAT_BAD = 0x0FFFFFF7; // Bad cluster marker +const u32 FIRST_ALLOC_CLUSTER = 2; // First usable cluster after reserved + +enum FAT_Flags : u32 { + UNALLOCATED = 0x00000000, + END_OF_FILE = 0xFFFFFFFF, // L.END + BAD_CLUSTER = 0xFFFFFFF7, // L.END +}; + +union FAT_Union { + u32 DECIMAL [[hidden]]; + FAT_Flags FAT_FLAG; +}; + +// ------------------------------ +// Helper function for pointer label +// ------------------------------ +fn cluster_label(u32 val) { + if (val == FAT_Flags::UNALLOCATED) + return "UNALLOCATED"; + if (val == FAT_Flags::BAD_CLUSTER) + return "BAD"; + if (val >= 0x0FFFFFF8) + return "EOF"; + return std::format("{}", val); +}; + +// ------------------------------ +// FAT1 HEAPS/CHAINS +// ------------------------------ +struct FAT_Entry { + FAT_Union FAT [[inline]]; + + u32 cluster_num = (FIRST_ALLOC_CLUSTER) + (std::core::array_index()); + + u32 next_cluster = FAT.DECIMAL & 0x0FFFFFFF; + + char hover_label[4] @ $ - 4 [[ + name(std::format( + "Cluster: {} → {}", + cluster_num, + cluster_label(next_cluster) + )) + ]]; + + bool is_eof = next_cluster >= 0x0FFFFFF8; + bool is_bad = next_cluster == FAT_BAD; + bool is_free = next_cluster == 0; + + if (is_eof) { + allocated_file_count += 1; + } +} [[name(format_fat_entry(FAT.DECIMAL, std::core::array_index(), FIRST_ALLOC_CLUSTER))]]; + +// ------------------------------ +// FAT FORMATTER FUNC +// ------------------------------ +fn format_fat_entry(u32 raw_value, u32 cluster_index, u32 first_alloc_cluster) { + u32 next_cluster = raw_value & 0x0FFFFFFF; + + str next_label; + + if (next_cluster == 0) + next_label = "UNALLOCATED"; + + else if (next_cluster == FAT_BAD) + next_label = "BAD"; + + else if (next_cluster == 0x0FFFFFFF) + next_label = "EOF"; + + else + next_label = std::format("{}", next_cluster); + + u32 logical_cluster = first_alloc_cluster + cluster_index; + + if (next_label == "UNALLOCATED" || next_label == "BAD" || next_label == "EOF") + return std::format("Cluster {}: {}", logical_cluster, next_label); + else + return std::format("Cluster {} → {}", logical_cluster, next_label); +}; + +// ------------------------------ +// MEDIA DESCRIPTOR HELPER +// ------------------------------ +enum Media_Descriptor : u8 { + SINGLE_SIDE_FLOPPY = 0xF0, + DOUBLE_SIDE_FLOPPY = 0xF9, + HARD_DISK_DRIVE = 0xF8, +}; + +// ------------------------------ +// FAT HEADER PARSER +// ------------------------------ +struct FAT_Header { + Media_Descriptor mediaDescriptor [[comment("0xF8=FIXED DISK | 0xF0=REMOVABLE")]];; + u8 exFAT_FAT_HEADER[7] [[comment("8 BYTES TOTAL: 4 BYTES REPRESENT PSUEDO CLUSTER 0 (SYSTEM) | 4 BYTES REPRESENT PSUEDO CLUSTER 1 (SYSTEM)(EOF)")]]; + + //Bitmap and UpCase overlays handled in RootDir parser + + char root_dir_label[4] @ $ + ((rdc - 2) * 4) [[ + name(std::format( + "ROOT_DIRECTORY" + )) + ]]; +}; + +// ------------------------------ +//SIGNATURE HELPER +// ------------------------------ +enum VBRSignature : u16 { + VBR_SIG = 0xAA55 +}; + +// ------------------------------ +// EXTENDED BOOT REGION +// ------------------------------ +struct ExtendedBoot { + u8 Extended_Boot_Sector[1 * bytesPerSector]; + VBRSignature VBR_SIG @ $ - 2; +}; + +// ------------------------------ +// BOOT SECTOR BITFIELD FLAGS +// ------------------------------ +bitfield VolumeFlags { + unsigned Active : 1; + unsigned VolumeDirty : 1; + unsigned Media_Failure : 1; + unsigned Clear_to_Zero : 1; + Rserved : 12; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 16)]]; + +// ------------------------------ +// EXFAT VOLUME BOOT RECORD +// ------------------------------ +struct exFAT_BootSector { + u8 jmp_boot[3]; + char fs_name[8]; // "EXFAT " + u8 must_be_zero[53]; + u64 partition_offset; // in sectors + u64 volume_length; // in sectors + u32 fat_offset; // in sectors + u32 fat_length; // in sectors + u32 cluster_heap_offset; // in sectors + u32 cluster_count; + u32 root_dir_cluster; + u32 volume_serial; + u16 fs_revision; + VolumeFlags volume_flags; + u8 bytes_per_sector_shift; // 2^n + u8 sectors_per_cluster_shift; // 2^n + u8 number_of_fats; + u8 drive_select; + u8 percent_in_use; + u8 reserved[7]; + u8 bootstrap[390]; + VBRSignature VBR_SIG; // 0x55AA + + rdc = root_dir_cluster; +}; + +// ------------------------------------------------------------------------- +// MAIN +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- + +exFAT_BootSector exFAT_VBR @ 0x0; + +// ------------------------------------------------------------------------- +// DERIVED CONSTANTS +// ------------------------------------------------------------------------- + +// ============= SIZES =================================================================== +u32 bytesPerSector = 1 << exFAT_VBR.bytes_per_sector_shift; +u32 bytesPerCluster = bytesPerSector << exFAT_VBR.sectors_per_cluster_shift; + +// ============= OFFSETS ================================================================= +u64 volumeStartSector = exFAT_VBR.partition_offset; +u64 volumeStartOffset = volumeStartSector * bytesPerSector; +u64 volumeSize = exFAT_VBR.volume_length * bytesPerSector; + +u64 FAT1_start_offset = exFAT_VBR.fat_offset * bytesPerSector; + +//For printing absolute offset +u64 RootDir_Offset = (exFAT_VBR.cluster_heap_offset + + ((exFAT_VBR.root_dir_cluster - 2) << exFAT_VBR.sectors_per_cluster_shift)) + * bytesPerSector + volumeStartOffset; + +// ============= CLUSTERS ================================================================ +u32 clusterSize = bytesPerCluster; +u32 clusterCount = exFAT_VBR.cluster_count; +u64 clusterHeapOffset = exFAT_VBR.cluster_heap_offset * bytesPerSector; + +// ------------------------------------------------------------------------- +// SECONDARY +// ------------------------------------------------------------------------- +// V V V V V V V V V +// ------------------------------------------------------------------------- +// ============= USAGE =================================================================== +u8 percentInUse = exFAT_VBR.percent_in_use; + +// ============= EBS ===================================================================== +ExtendedBoot Extended_Boot_Sectors[8] @ $; + +// ============= OEM ===================================================================== +u8 OEM_Parameters[1 * bytesPerSector] @ $; + +// ============= ER ====================================================================== +u8 Extended_Reserved[1 * bytesPerSector] @ $; + +// ============= BCS ===================================================================== +u8 Boot_Checksum[1 * bytesPerSector] @ $; + +// ============= BBS ===================================================================== +exFAT_BootSector Backup_Boot_Sector @ $; + +// ============= BEBS ==================================================================== +ExtendedBoot Backup_Extended_Boot_Sectors[8] @ $; + +// ============= BOEM ==================================================================== +u8 Backup_OEM_Parameters[1 * bytesPerSector] @ $; + +// ============= BER ===================================================================== +u8 Backup_Extended_Reserved[1 * bytesPerSector] @ $; + +// ============= BBCS ==================================================================== +u8 Backup_Boot_Checksum[1 * bytesPerSector] @ $; + +// ============= FAT ===================================================================== +// *** HAS GLOBAL AT TOP *** + +FAT_Header FAT1_HEADER @ FAT1_start_offset; +FAT_Entry FAT1[MAX_FAT_CHUNKS] @ FAT1_start_offset + 8; + +// ============= ROOT ==================================================================== +// ROOT DIRECTORY +// *** HAS GLOBAL AT TOP *** + +// for locating root directory within memory +u64 temp_root_location = (exFAT_VBR.root_dir_cluster - 2) * clusterSize + clusterHeapOffset; +RootDir ROOT_DIRECTORY[MAX_DIR_ENTRIES] @ temp_root_location; + + +// ============= REPORT ================================================================== +// VOLUME REPORT +// *** HAS GLOBAL AT TOP *** + +if (VOLUME_REPORT) { + std::print(" "); + std::print("-----------------------------------------"); + std::print("---------- EXFAT VOLUME_REPORT ----------"); + std::print("-----------------------------------------"); + std::print("FILE_SYSTEM = {}", exFAT_VBR.fs_name); + std::print("SERIAL_NUMBER = 0x{:X}", exFAT_VBR.volume_serial); + std::print("FS_REVISION = {}.{}", (exFAT_VBR.fs_revision >> 8) & 0xFF, exFAT_VBR.fs_revision & 0xFF); + + bool _any = false; + if(exFAT_VBR.volume_flags.Active) { + std::print("FAT_FLAG = 0b{:X}", exFAT_VBR.volume_flags.Active); + _any = true; + } + if(exFAT_VBR.volume_flags.VolumeDirty) { + std::print("DIRTY_FLAG = 0b{:X}", exFAT_VBR.volume_flags.Volume_Dirty); + _any = true; + } + if(exFAT_VBR.volume_flags.Media_Failure) { + std::print("FAILURE_FLAG = 0b{:X}", exFAT_VBR.volume_flags.Media_Failure); + _any = true; + } + if(exFAT_VBR.volume_flags.Clear_to_Zero) { + std::print("CLEAR_TO_ZERO_FLAG = 0b{:X}", exFAT_VBR.volume_flags.Clear_to_Zero); + _any = true; + } + if (!_any){ + std::print("VOL_FLAGS = NONE"); + } + + std::print("-----------------------------------------"); + std::print("BYTES/SECTOR = {}", bytesPerSector); + std::print("SECTORS/CLUSTER = {}", 1 << exFAT_VBR.sectors_per_cluster_shift); + std::print("BYTES/CLUSTER = {}", bytesPerCluster); + std::print("CLUSTER_COUNT = {}", clusterCount); + + std::print("-----------------------------------------"); + std::print("VOLUME_SIZE = {} SECTORS", exFAT_VBR.volume_length); + std::print("VOLUME_SIZE = {:.4f} GB @ 1000", volumeSize / 1000.0 / 1000.0 / 1000.0); + std::print("VOLUME_SIZE = {:.4f} GiB @ 1024", volumeSize / 1024.0 / 1024.0 / 1024.0); + + std::print("-----------------------------------------"); + std::print("VOLUME_START_SEC = {}", volumeStartSector); + std::print("VOLUME_START_OFF = 0x{:X}", volumeStartOffset); + + std::print("FAT1_START_OFF = 0x{:02X}", FAT1_start_offset); + std::print("CLUSTER_HEAP_OFF = 0x{:02X}", clusterHeapOffset); + std::print("ROOT_DIR_CLUSTER = {:02}", exFAT_VBR.root_dir_cluster); + std::print("ROOT_DIR_OFFSET = 0x{:02X}", RootDir_Offset); + + std::print("-----------------------------------------"); + std::print("PERCENT_IN_USE = {:02} %", percentInUse); + std::print("NUMBER_OF_FATS = {:02}", exFAT_VBR.number_of_fats); + std::print("DRIVE_SELECT = 0x{:02X}", exFAT_VBR.drive_select); + + std::print("-----------------------------------------"); + std::print("------------------ END ------------------"); + std::print("-----------------------------------------"); + std::print(" "); +} From 84dff0c8864f480ece8931d1d3bd24ddc56ccea7 Mon Sep 17 00:00:00 2001 From: Fabian Neundorf Date: Fri, 5 Dec 2025 21:19:19 +0100 Subject: [PATCH 91/93] patterns: Add json support for glb files (#412) * patterns: Add json support for glb files This makes it possible to separate display the different buffer views, accessors and images (even visualizing them). Unfortunately the data within the JSON gets sometimes corrupted and this is the reason, why it parses the JSON multiple times at some places. * Use original style and only single json variable * patterns: Reuse json from global variable in gltf * patterns: Check component type in gltf only once * patterns: Fix gltf pattern and add formatting Removes the duplicate definition of `component_type_t` and also removes the need to pass the `component_type` to `stride_type_t`. --- patterns/gltf.hexpat | 178 +++++++++++++++++++++++++++++++++- patterns/png.hexpat | 11 ++- patterns/png2.hexpat | 224 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 405 insertions(+), 8 deletions(-) create mode 100644 patterns/png2.hexpat diff --git a/patterns/gltf.hexpat b/patterns/gltf.hexpat index 85d94b8f..a208b965 100644 --- a/patterns/gltf.hexpat +++ b/patterns/gltf.hexpat @@ -22,13 +22,18 @@ * SOFTWARE. */ -#pragma author H. Utku Maden +#pragma author H. Utku Maden, xZise #pragma description GL Transmission Format binary 3D model (.glb) #pragma MIME model/gltf-binary +#pragma magic [67 6C 54 46] @ 0x00 import std.mem; import std.io; import type.magic; +import std.core; +#ifdef __IMHEX__ +import hex.type.json; +#endif /** * @brief The glTF magic section. @@ -53,7 +58,15 @@ enum gltf_chunk_type_t : u32 { struct gltf_chunk_t { u32 length; /**< Length of this chunk. */ gltf_chunk_type_t type [[format("gltf_format")]]; /**< Type of the chunk. JSON or BIN expected. */ - u8 string[length]; /**< The chunk data. */ +#ifndef __IMHEX__ + u8 data[length]; /**< The chunk data. */ +#endif +#ifdef __IMHEX__ + match (type) { + (gltf_chunk_type_t::JSON): hex::type::Json json; + (gltf_chunk_type_t::BIN): u8 data[length]; + } /**< The chunk data. */ +#endif }; fn gltf_format(gltf_chunk_type_t x) @@ -64,7 +77,162 @@ fn gltf_format(gltf_chunk_type_t x) return ""; }; -gltf_magic_t magic @ 0x00; -gltf_chunk_t chunks[while(!std::mem::eof())] @ $; +struct stride_type_t { + InnerType value [[inline]]; + if (Stride > 0) { + padding[Stride - sizeof(value)]; + } +}; + +enum component_types_t : u64 { + BYTE = 5120, + UNSIGNED_BYTE = 5121, + SHORT = 5122, + UNSIGNED_SHORT = 5123, + UNSIGNED_INT = 5125, + FLOAT = 5126, +}; + +fn component_type_format(component_types_t component_type) +{ + if (component_type == component_types_t::BYTE) return "s8"; +else if (component_type == component_types_t::UNSIGNED_BYTE) return "u8"; +else if (component_type == component_types_t::SHORT) return "s16"; +else if (component_type == component_types_t::UNSIGNED_SHORT) return "u16"; +else if (component_type == component_types_t::UNSIGNED_INT) return "u32"; +else if (component_type == component_types_t::FLOAT) return "float"; + + return std::format("{}", component_type); +}; + +struct component_type_t { + match (component_type) { + (component_types_t::BYTE): s8 value; + (component_types_t::UNSIGNED_BYTE): u8 value; + (component_types_t::SHORT): s16 value; + (component_types_t::UNSIGNED_SHORT): u16 value; + (component_types_t::UNSIGNED_INT): u32 value; + (component_types_t::FLOAT): float value; + } +}; + +struct scalar_t { + component_type_t scalar [[inline]]; +} [[static]]; + +struct vec2_t { + component_type_t x; + component_type_t y; +} [[static]]; + +struct vec3_t { + component_type_t x; + component_type_t y; + component_type_t z; +} [[static]]; + +struct vec4_t { + component_type_t x; + component_type_t y; + component_type_t z; + component_type_t w; +} [[static]]; + +struct mat2_t { + component_type_t a11, a21; + component_type_t a12, a22; +} [[static]]; + +struct mat3_t { + component_type_t a11, a21, a31; + component_type_t a12, a22, a32; + component_type_t a13, a23, a33; +} [[static]]; + +struct mat4_t { + component_type_t a11, a21, a31, a41; + component_type_t a12, a22, a32, a42; + component_type_t a13, a23, a33, a43; + component_type_t a14, a24, a34, a44; +} [[static]]; + +fn mem_cnt(auto value) { + return std::core::member_count(value); +}; + +fn has_mem(auto value, str member) { + return std::core::has_member(value, member); +}; + +struct accessor_t { + u64 accessor_index = std::core::array_index(); + u64 view_index = glb.json_chunk.json.accessors[accessor_index].bufferView [[export]]; + u64 view_offset = glb.json_chunk.json.bufferViews[view_index].byteOffset [[export]]; + if (has_mem(glb.json_chunk.json.bufferViews[view_index], "byteStride")) { + u64 byte_stride = glb.json_chunk.json.bufferViews[view_index].byteStride [[export]]; + } else { + u64 byte_stride = 0 [[export]]; + } + if (has_mem(glb.json_chunk.json.accessors[accessor_index], "byteOffset")) { + u64 accessor_offset = glb.json_chunk.json.accessors[accessor_index].byteOffset [[export]]; + } else { + u64 accessor_offset = 0 [[export]]; + } + view_offset = view_offset + accessor_offset; + u64 count_elements = glb.json_chunk.json.accessors[accessor_index].count; + component_types_t component_type = glb.json_chunk.json.accessors[accessor_index].componentType [[export]]; + str element_type = glb.json_chunk.json.accessors[accessor_index].type [[export]]; + + match (element_type) { + ("SCALAR"): stride_type_t, byte_stride> content[count_elements] @ view_offset + addressof(glb.chunks[0].data); + ("VEC2"): stride_type_t, byte_stride> content[count_elements] @ view_offset + addressof(glb.chunks[0].data); + ("VEC3"): stride_type_t, byte_stride> content[count_elements] @ view_offset + addressof(glb.chunks[0].data); + ("VEC4"): stride_type_t, byte_stride> content[count_elements] @ view_offset + addressof(glb.chunks[0].data); + ("MAT2"): stride_type_t, byte_stride> content[count_elements] @ view_offset + addressof(glb.chunks[0].data); + ("MAT3"): stride_type_t, byte_stride> content[count_elements] @ view_offset + addressof(glb.chunks[0].data); + ("MAT4"): stride_type_t, byte_stride> content[count_elements] @ view_offset + addressof(glb.chunks[0].data); + } +} [[format("accessor_format")]]; + +fn accessor_format(accessor_t accessor) { + return std::format("{}<{}>[{}]", accessor.element_type, accessor.component_type, accessor.count_elements); +}; + +struct image_buffer_t { + u64 image_index = std::core::array_index(); + u64 buffer_view_index = glb.json_chunk.json.images[image_index].bufferView; + u64 byte_offset = glb.json_chunk.json.bufferViews[buffer_view_index].byteOffset; + u64 byte_length = glb.json_chunk.json.bufferViews[buffer_view_index].byteLength; + u8 image[byte_length] @ addressof(glb.chunks[0].data) + byte_offset; +} [[hex::visualize("image", image)]]; + +struct buffer_view_t { + u64 buffer_view_index = std::core::array_index(); + u64 byte_offset = glb.json_chunk.json.bufferViews[buffer_view_index].byteOffset; + u64 byte_length = glb.json_chunk.json.bufferViews[buffer_view_index].byteLength; + u8 data[byte_length] @ addressof(glb.chunks[0].data) + byte_offset; +}; + +struct glb_file_t { + gltf_magic_t magic; + gltf_chunk_t json_chunk; + gltf_chunk_t chunks[while(!std::mem::eof())]; + + std::assert_warn(std::mem::size() == magic.length, "file size mismatch"); +}; + +glb_file_t glb @ 0x00; + +#ifdef __IMHEX__ +struct glb_objects_t { + if (std::core::member_count(glb.chunks) == 1) { + if (has_mem(glb.json_chunk.json, "images")) { + image_buffer_t images[mem_cnt(glb.json_chunk.json.images)]; + } + buffer_view_t buffer_views[mem_cnt(glb.json_chunk.json.bufferViews)]; + accessor_t accessors[mem_cnt(glb.json_chunk.json.accessors)]; + } +}; -std::assert_warn(std::mem::size() == magic.length, "file size mismatch"); +glb_objects_t objects @ 0x00; +#endif \ No newline at end of file diff --git a/patterns/png.hexpat b/patterns/png.hexpat index ebd7122e..787d058f 100644 --- a/patterns/png.hexpat +++ b/patterns/png.hexpat @@ -214,6 +214,11 @@ struct Chunks { chunk_t iend_chunk [[comment("Image End Chunk")]]; }; -u8 visualizer[std::mem::size()] @ 0x00 [[sealed, hex::visualize("image", this)]]; -header_t header @ 0x00 [[comment("PNG file signature"), name("Signature")]]; -Chunks chunks @ 0x08 [[name("Chunks")]]; \ No newline at end of file +struct Png { + header_t header [[comment("PNG file signature"), name("Signature")]]; + Chunks chunks [[name("Chunks")]]; + u128 length = $ - addressof(this); + u8 visualizer[length] @ addressof(this) [[sealed, hex::visualize("image", this), no_unique_address]]; +}; + +Png png @ 0x00; \ No newline at end of file diff --git a/patterns/png2.hexpat b/patterns/png2.hexpat new file mode 100644 index 00000000..787d058f --- /dev/null +++ b/patterns/png2.hexpat @@ -0,0 +1,224 @@ +#pragma description PNG image + +#pragma MIME image/png +#pragma endian big + +import std.mem; + +struct header_t { + u8 highBitByte; + char signature[3]; + char dosLineEnding[2]; + char dosEOF; + char unixLineEnding; +}; + +struct actl_t { + u32 frames [[comment("Total № of frames in animation")]]; + u32 plays [[comment("№ of times animation will loop")]]; +} [[comment("Animation control chunk"), name("acTL")]]; + +enum ColorType: u8 { + Grayscale = 0x0, + RGBTriple = 0x2, + Palette, + GrayscaleAlpha, + RGBA = 0x6 +}; + +enum Interlacing: u8 { + None, + Adam7 +}; + +struct ihdr_t { + u32 width [[comment("Image width")]]; + u32 height [[comment("Image height")]]; + u8 bit_depth; + ColorType color_type [[comment("PNG Image Type")]]; + u8 compression_method [[comment("Only 0x0 = zlib supported by most")]]; + u8 filter_method [[comment("Only 0x0 = adaptive supported by most")]]; + Interlacing interlacing; +}; + +enum sRGB: u8 { + Perceptual = 0x0, + RelativeColorimetric, + Saturation, + AbsoluteColorimetric +}; + +enum Unit: u8 { + Unknown, + Meter +}; + +struct phys_t { + u32 ppu_x [[comment("Pixels per unit, X axis")]]; + u32 ppu_y [[comment("Pixels per unit, Y axis")]]; + Unit unit; +}; + +enum BlendOp: u8 { + Source = 0x0, + Over +}; + +enum DisposeOp: u8 { + None = 0x0, + Background, + Previous +}; + +struct fctl_t { + u32 sequence_no [[comment("Sequence №")]]; + u32 width [[comment("Frame width")]]; + u32 height; + u32 xoff; + u32 yoff; + u16 delay_num; + u16 delay_den; + DisposeOp dispose_op; + BlendOp blend_op; +}; + +struct fdat_t { + u32 sequence_no; +}; + +fn text_len() { + u64 len = parent.parent.length - ($ - addressof(parent.keyword)); + return len; +}; + +struct itxt_t { + char keyword[]; + u8 compression_flag; + u8 compression_method; + char language_tag[]; + char translated_keyword[]; + char text[text_len()]; +}; + +struct ztxt_t { + char keyword[]; + u8 compression_method; + char text[text_len()]; +}; + +struct text_t { + char keyword[]; + char text[text_len()]; +}; + +struct iccp_t { + char keyword[]; + u8 compression_method; + u8 compressed_profile[text_len()]; +}; + +struct palette_entry_t { + u24 color; +} [[inline]]; + +struct chrm_t { + u32 white_point_x; + u32 white_point_y; + u32 red_x; + u32 red_y; + u32 green_x; + u32 green_y; + u32 blue_x; + u32 blue_y; +}; + +struct time_t { + u16 year; + u8 month; + u8 day; + u8 hour; + u8 minute; + u8 second; +}; + +struct chunk_t { + u32 length [[color("17BECF")]]; + char name[4]; + + #define IHDR_k "IHDR" + #define PLTE_k "PLTE" + #define sRGB_k "sRGB" + #define pHYs_k "pHYs" + #define iTXt_k "iTXt" + #define tEXt_k "tEXt" + #define zTXt_k "zTXt" + #define IDAT_k "IDAT" + #define IEND_k "IEND" + #define gAMA_k "gAMA" + #define iCCP_k "iCCP" + #define acTL_k "acTL" + #define fdAT_k "fdAT" + #define fcTL_k "fcTL" + #define cHRM_k "cHRM" + #define tIME_k "tIME" + + if (name == IHDR_k) { + ihdr_t ihdr [[comment("Image Header chunk"), name("IHDR")]]; + } else if (name == PLTE_k) { + palette_entry_t entries[length / 3]; + } else if (name == sRGB_k) { + sRGB srgb; + } else if (name == pHYs_k) { + phys_t phys; + } else if (name == acTL_k) { + actl_t actl [[comment("Animation control chunk")]]; + } else if (name == fcTL_k) { + fctl_t fctl [[comment("Frame control chunk")]]; + } else if (name == iTXt_k) { + itxt_t text; + } else if (name == gAMA_k) { + u32 gamma [[name("image gamma"), comment("4 byte unsigned integer representing gamma times 100000")]]; + } else if (name == iCCP_k) { + iccp_t iccp; + } else if (name == tEXt_k) { + text_t text; + } else if (name == zTXt_k) { + ztxt_t text; + } else if (name == iCCP_k) { + iccp_t iccp; + } else if (name == fdAT_k) { + fdat_t fdat [[comment("Frame data chunk")]]; + u8 data[length-sizeof(u32)]; + } else if (name == cHRM_k) { + chrm_t chrm; + } else if (name == tIME_k) { + time_t time; + } else { + u8 data[length]; + } + + u32 crc; +} [[name(chunkValueName(this))]]; + +fn chunkValueName(ref chunk_t chunk) { + return chunk.name; +}; + +struct chunk_set { + chunk_t chunks[while(builtin::std::mem::read_string($ + 4, 4) != "IEND")] [[inline]]; +} [[inline]]; + +struct Chunks { + chunk_t ihdr_chunk [[comment("PNG Header chunk")]]; + chunk_set set [[comment("PNG Chunks"), name("Chunks"), inline]]; + chunk_t iend_chunk [[comment("Image End Chunk")]]; +}; + +struct Png { + header_t header [[comment("PNG file signature"), name("Signature")]]; + Chunks chunks [[name("Chunks")]]; + u128 length = $ - addressof(this); + u8 visualizer[length] @ addressof(this) [[sealed, hex::visualize("image", this), no_unique_address]]; +}; + +Png png @ 0x00; \ No newline at end of file From ee340409dbd85c24f5a902d3eb8c66bf34a6e01c Mon Sep 17 00:00:00 2001 From: Hrant <149574279+HrantTadevosyan@users.noreply.github.com> Date: Sat, 6 Dec 2025 00:19:37 +0400 Subject: [PATCH 92/93] patterns: Added APFS pattern (#400) * updated APFS hexpat * fix null feature --------- Co-authored-by: Hrant Tadevosyan --- README.md | 1 + patterns/fs/apfs.hexpat | 1479 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 1480 insertions(+) create mode 100644 patterns/fs/apfs.hexpat diff --git a/README.md b/README.md index ff51aa7a..6687304f 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | ADTS | `audio/x-hx-aac-adts` | [`patterns/adts.hexpat`](patterns/adts.hexpat) | ADTS/AAC audio files | | AFE2 | | [`patterns/afe2.hexpat`](patterns/afe2.hexpat) | Nintendo Switch Atmosphère CFW Fatal Error log | | ANI | `application/x-navi-animation` | [`patterns/ani.hexpat`](patterns/ani.hexpat) | Windows Animated Cursor file | +| APFS | | [`patterns/fs/apfs.hexpat`](patterns/fs/apfs.hexpat) | Apple File Ssytem (APFS) | | AppleSingle | `application/applefile` | [`patterns/apple_single_double.hexpat`](patterns/apple_single_double.hexpat) | AppleSingle Dual Fork file | | AppleDouble | `multipart/appledouble` | [`patterns/apple_single_double.hexpat`](patterns/apple_single_double.hexpat) | AppleDouble Resource Fork/Finder Metadata file | | AR | `application/x-archive` | [`patterns/ar.hexpat`](patterns/ar.hexpat) | Static library archive files | diff --git a/patterns/fs/apfs.hexpat b/patterns/fs/apfs.hexpat new file mode 100644 index 00000000..a5db527a --- /dev/null +++ b/patterns/fs/apfs.hexpat @@ -0,0 +1,1479 @@ +#pragma author Hrant Tadevosyan (Axcient, now ConnectWise) +#pragma description Apple File System (APFS) +#pragma endian little + +// refs: +// - https://developer.apple.com/support/downloads/Apple-File-System-Reference.pdf +// - https://github.com/sgan81/apfs-fuse +// - https://github.com/libyal/libfsapfs + +import std.core; +import std.hash; +import std.mem; +import type.guid; +import type.magic; +import type.time; + +using paddr_t = s64; + +struct prange { + paddr_t pr_start_paddr; + u64 pr_block_count; +}; + +using uuid_t = type::GUID; + +#define MAX_CKSUM_SIZE 8 + +using oid_t = u64; +using xid_t = u64; + +#define OID_NX_SUPERBLOCK 1 +#define OID_INVALID 0ULL +#define OID_RESERVED_COUNT 1024 + +#define OBJECT_TYPE_MASK 0x0000ffff +#define OBJECT_TYPE_FLAGS_MASK 0xffff0000 + +#define OBJ_STORAGETYPE_MASK 0xc0000000 +#define OBJECT_TYPE_FLAGS_DEFINED_MASK 0xf8000000 + +enum o_type_id_t : u16 { + OBJECT_TYPE_NX_SUPERBLOCK = 0x0001, + OBJECT_TYPE_BTREE = 0x0002, + OBJECT_TYPE_BTREE_NODE = 0x0003, + OBJECT_TYPE_SPACEMAN = 0x0005, + OBJECT_TYPE_SPACEMAN_CAB = 0x0006, + OBJECT_TYPE_SPACEMAN_CIB = 0x0007, + OBJECT_TYPE_SPACEMAN_BITMAP = 0x0008, + OBJECT_TYPE_SPACEMAN_FREE_QUEUE = 0x0009, + OBJECT_TYPE_EXTENT_LIST_TREE = 0x000a, + OBJECT_TYPE_OMAP = 0x000b, + OBJECT_TYPE_CHECKPOINT_MAP = 0x000c, + OBJECT_TYPE_FS = 0x000d, + OBJECT_TYPE_FSTREE = 0x000e, + OBJECT_TYPE_BLOCKREFTREE = 0x000f, + OBJECT_TYPE_SNAPMETATREE = 0x0010, + OBJECT_TYPE_NX_REAPER = 0x0011, + OBJECT_TYPE_NX_REAP_LIST = 0x0012, + OBJECT_TYPE_OMAP_SNAPSHOT = 0x0013, + OBJECT_TYPE_EFI_JUMPSTART = 0x0014, + OBJECT_TYPE_FUSION_MIDDLE_TREE = 0x0015, + OBJECT_TYPE_NX_FUSION_WBC = 0x0016, + OBJECT_TYPE_NX_FUSION_WBC_LIST = 0x0017, + OBJECT_TYPE_ER_STATE = 0x0018, + OBJECT_TYPE_GBITMAP = 0x0019, + OBJECT_TYPE_GBITMAP_TREE = 0x001a, + OBJECT_TYPE_GBITMAP_BLOCK = 0x001b, + OBJECT_TYPE_ER_RECOVERY_BLOCK = 0x001c, + OBJECT_TYPE_SNAP_META_EXT = 0x001d, + OBJECT_TYPE_INTEGRITY_META = 0x001e, + OBJECT_TYPE_FEXT_TREE = 0x001f, + OBJECT_TYPE_RESERVED_20 = 0x0020, + OBJECT_TYPE_INVALID = 0x0000, + OBJECT_TYPE_TEST = 0x00ff, + OBJECT_TYPE_CONTAINER_KEYBAG = 0x7973, // ys -> keys + OBJECT_TYPE_CONTAINER_KEYBAG_2 = 0x6B65, // ke -> + //OBJECT_TYPE_VOLUME_KEYBAG = "recs", + //OBJECT_TYPE_MEDIA_KEYBAG = "mkey", +}; + +enum o_flag_id_t : u16 { + OBJ_VIRTUAL = 0x0000, + OBJ_EPHEMERAL = 0x8000, + OBJ_PHYSICAL = 0x4000, + OBJ_NOHEADER = 0x2000, + OBJ_ENCRYPTED = 0x1000, + OBJ_NONPERSISTENT = 0x0800, +}; + +struct o_type_t { + o_type_id_t t_type; + o_flag_id_t t_flag; +}; + +struct obj_phys_t { + u8 o_cksum[MAX_CKSUM_SIZE]; + oid_t o_oid; + xid_t o_xid; + o_type_t o_type; + o_type_t o_subtype; +}; + +#define NX_MAGIC_RE "BSXN" +#define NX_MAGIC "NXSB" +#define NX_MAX_FILE_SYSTEMS 100 +#define NX_EPH_INFO_COUNT 4 +#define NX_EPH_MIN_BLOCK_COUNT 8 +#define NX_MAX_FILE_SYSTEM_EPH_STRUCTS 4 +#define NX_TX_MIN_CHECKPOINT_COUNT 4 +#define NX_EPH_INFO_VERSION_1 1 + +#define NX_RESERVED_1 0x00000001LL +#define NX_RESERVED_2 0x00000002LL +#define NX_CRYPTO_SW 0x00000004LL + +#define NX_FEATURE_DEFRAG 0x0000000000000001ULL +#define NX_FEATURE_LCFD 0x0000000000000002ULL +#define NX_SUPPORTED_FEATURES_MASK (NX_FEATURE_DEFRAG | NX_FEATURE_LCFD) + +bitfield nx_features_t { + unsigned defrag : 1; + unsigned lcfd : 1; + padding : 62; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 64)]]; + +#define NX_SUPPORTED_ROCOMPAT_MASK (0x0ULL) + +bitfield nx_rocompat_features_t { + padding : 64; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 64)]]; + +#define NX_INCOMPAT_VERSION1 0x0000000000000001ULL +#define NX_INCOMPAT_VERSION2 0x0000000000000002ULL +#define NX_INCOMPAT_FUSION 0x0000000000000100ULL +#define NX_SUPPORTED_INCOMPAT_MASK (NX_INCOMPAT_VERSION2 | NX_INCOMPAT_FUSION) + +bitfield nx_incompat_features_t { + version1 : 1; + version2 : 1; + padding : 6; + fusion : 1; + padding : 55; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 64)]]; + +#define NX_MINIMUM_BLOCK_SIZE 4096 +#define NX_DEFAULT_BLOCK_SIZE 4096 +#define NX_MAXIMUM_BLOCK_SIZE 65536 + +#define NX_MINIMUM_CONTAINER_SIZE 1048576 + +enum nx_counter_id_t : u64 { + NX_CNTR_OBJ_CKSUM_SET = 0, + NX_CNTR_OBJ_CKSUM_FAIL = 1, + NX_NUM_COUNTERS = 32 +}; + +struct nx_superblock_t { + obj_phys_t nx_o; + type::Magic nx_magic; + u32 nx_block_size; + u64 nx_block_count; + nx_features_t nx_features; + nx_rocompat_features_t nx_readonly_compatible_features; + nx_incompat_features_t nx_incompatible_features; + uuid_t nx_uuid; + oid_t nx_next_oid; + xid_t nx_next_xid; + u32 nx_xp_desc_blocks; + u32 nx_xp_data_blocks; + paddr_t nx_xp_desc_base; + paddr_t nx_xp_data_base; + u32 nx_xp_desc_next; + u32 nx_xp_data_next; + u32 nx_xp_desc_index; + u32 nx_xp_desc_len; + u32 nx_xp_data_index; + u32 nx_xp_data_len; + oid_t nx_spaceman_oid; + oid_t nx_omap_oid; + oid_t nx_reaper_oid; + u32 nx_test_type; + u32 nx_max_file_systems; + oid_t nx_fs_oid[NX_MAX_FILE_SYSTEMS]; + nx_counter_id_t nx_counters[nx_counter_id_t::NX_NUM_COUNTERS]; + prange nx_blocked_out_prange; + oid_t nx_evict_mapping_tree_oid; + u64 nx_flags; + paddr_t nx_efi_jumpstart; + uuid_t nx_fusion_uuid; + prange nx_keylocker; + u64 nx_ephemeral_info[NX_EPH_INFO_COUNT]; + oid_t nx_test_oid; + oid_t nx_fusion_mt_oid; + oid_t nx_fusion_wbc_oid; + prange nx_fusion_wbc; + u64 nx_newest_mounted_version; + prange nx_mkb_locker; +}; + +#define CHECKPOINT_MAP_LAST 0x00000001 + +struct checkpoint_mapping_t { + o_type_t cpm_type; + o_type_t cpm_subtype; + u32 cpm_size; + u32 cpm_pad; + oid_t cpm_fs_oid; + oid_t cpm_oid; + oid_t cpm_paddr; +}; + +struct checkpoint_map_phys_t { + obj_phys_t cpm_o; + u32 cpm_flags; + u32 cpm_count; + + checkpoint_mapping_t cpm_map[cpm_count]; +}; + +struct evict_mapping_val_t { + paddr_t dst_paddr; + u64 len; +}; + +struct omap_phys_t { + obj_phys_t om_o; + u32 om_flags; + u32 om_snap_count; + u32 om_tree_type; + u32 om_snapshot_tree_type; + oid_t om_tree_oid; + oid_t om_snapshot_tree_oid; + xid_t om_most_recent_snap; + xid_t om_pending_revert_min; + xid_t om_pending_revert_max; +}; + +struct omap_key_t { + oid_t ok_oid; + oid_t ok_xid; +}; + +struct omap_val_t { + u32 ov_flags; + u32 ov_size; + paddr_t ov_paddr; +}; + +struct omap_snapshot_t { + u32 oms_flags; + u32 oms_pad; + oid_t oms_oid; +}; + +struct chunk_info_t { + u64 ci_xid; + u64 ci_addr; + u32 ci_block_count; + u32 ci_free_count; + paddr_t ci_bitmap_addr; +}; + +struct chunk_info_block_t { + obj_phys_t cib_o; + u32 cib_index; + u32 cib_chunk_info_count; + chunk_info_t cib_chunk_info[cib_chunk_info_count]; +}; + +struct cib_addr_block_t { + obj_phys_t cab_o; + u32 cab_index; + u32 cab_cib_count; + paddr_t cab_cib_addr[cab_cib_count]; +}; + +struct spaceman_free_queue_key_t { + xid_t sfqk_xid; + paddr_t sfqk_paddr; +}; + +using spaceman_free_queue_val_t = u64; +struct spaceman_free_queue_entry_t { + spaceman_free_queue_key_t sfqe_key; + spaceman_free_queue_val_t sfqe_count; +}; + +struct spaceman_free_queue_t { + u64 sfq_count; + oid_t sfq_tree_oid; + xid_t sfq_oldest_xid; + u16 sfq_tree_node_limit; + u16 sfq_pad16; + u32 sfq_pad32; + u64 sfq_reserved; +}; + +struct spaceman_device_t { + u64 sm_block_count; + u64 sm_chunk_count; + u32 sm_cib_count; + u32 sm_cab_count; + u64 sm_free_count; + u32 sm_addr_offset; + u32 sm_reserved; + u64 sm_reserved2; +}; + +enum smdev_t : u32 { + SD_MAIN = 0, + SD_TIER2 = 1, + SD_COUNT = 2 +}; + +enum sfq_t : u32 { + SFQ_IP = 0, + SFQ_MAIN = 1, + SFQ_TIER2 = 2, + SFQ_COUNT = 3 +}; + +struct spaceman_allocation_zone_boundaries_t { + u64 saz_zone_start; + u64 saz_zone_end; +}; + +#define SM_ALLOCZONE_INVALID_END_BOUNDARY 0 +#define SM_ALLOCZONE_NUM_PREVIOUS_BOUNDARIES 7 +struct spaceman_allocation_zone_info_phys_t { + spaceman_allocation_zone_boundaries_t saz_current_boundaries; + spaceman_allocation_zone_boundaries_t saz_previous_boundaries[SM_ALLOCZONE_NUM_PREVIOUS_BOUNDARIES]; + u16 saz_zone_id; + u16 saz_previous_boundary_index; + u32 saz_reserved; +}; + +struct spaceman_allocation_zones_t { + spaceman_allocation_zone_info_phys_t sdz_allocation_zone_infos[smdev_t::SD_COUNT]; +}; + +#define SM_DATAZONE_ALLOCZONE_COUNT 8 +struct spaceman_datazone_info_phys_t { + spaceman_allocation_zones_t sdz_allocation_zones[SM_DATAZONE_ALLOCZONE_COUNT]; +}; + +struct spaceman_phys_t { + obj_phys_t sm_o; + u32 sm_block_size; + u32 sm_blocks_per_chunk; + u32 sm_chunks_per_cib; + u32 sm_cibs_per_cab; + spaceman_device_t sm_dev[smdev_t::SD_COUNT]; + u32 sm_flags; + u32 sm_ip_bm_tx_multiplier; + u64 sm_ip_block_count; + u32 sm_ip_bm_size_in_blocks; + u32 sm_ip_bm_block_count; + paddr_t sm_ip_bm_base; + paddr_t sm_ip_base; + u64 sm_fs_reserve_block_count; + u64 sm_fs_reserve_alloc_count; + spaceman_free_queue_t sm_fq[sfq_t::SFQ_COUNT]; + u16 sm_ip_bm_free_head; + u16 sm_ip_bm_free_tail; + u32 sm_ip_bm_xid_offset; + u32 sm_ip_bitmap_offset; + u32 sm_ip_bm_free_next_offset; + u32 sm_version; + u32 sm_struct_size; + spaceman_datazone_info_phys_t sm_datazone; +}; + +struct nx_reaper_phys_t { + obj_phys_t nr_o; + u64 nr_next_reap_id; + u64 nr_completed_id; + oid_t nr_head; + oid_t nr_tail; + u32 nr_flags; + u32 nr_rlcount; + u32 nr_type; + u32 nr_size; + oid_t nr_fs_oid; + oid_t nr_oid; + xid_t nr_xid; + u32 nr_nrle_flags; + u32 nr_state_buffer_size; + u8 nr_state_buffer[nr_state_buffer_size]; +}; + +struct nx_reap_list_entry_t { + u32 nrle_next; + u32 nrle_flags; + u32 nrle_type; + u32 nrle_size; + oid_t nrle_fs_oid; + oid_t nrle_oid; + xid_t nrle_xid; +}; + +struct nx_reap_list_phys_t { + obj_phys_t nrl_o; + oid_t nrl_next; + u32 nrl_flags; + u32 nrl_max; + u32 nrl_count; + u32 nrl_first; + u32 nrl_last; + u32 nrl_free; + nx_reap_list_entry_t nrl_entries[nrl_count]; +}; + +enum nx_reap_phase_t : u32 { + APFS_REAP_PHASE_START = 0, + APFS_REAP_PHASE_SNAPSHOTS = 1, + APFS_REAP_PHASE_ACTIVE_FS = 2, + APFS_REAP_PHASE_DESTROY_OMAP = 3, + APFS_REAP_PHASE_DONE = 4, +}; + +struct keybag_entry_t { + uuid_t ke_uuid; + u16 ke_tag; + u16 ke_keylen; + padding[4]; + u8 ke_keydata[]; +}; + +struct kb_locker_t { + u16 kl_version; + u16 kl_nkeys; + u32 kl_nbytes; + padding[8]; + u8 kl_entries[]; +}; + +struct mk_obj_t { + u8 o_cksum[MAX_CKSUM_SIZE]; + oid_t o_oid; + xid_t o_xid; + o_type_id_t o_type; + o_type_id_t o_subtype; +}; + +struct media_keybag_t { + mk_obj_t mk_obj; + kb_locker_t mk_locker; +}; + +using crypto_flags_t = u32; +using cp_key_class_t = u32; +using cp_key_os_version_t = u32; +using cp_key_revision_t = u16; + +struct wrapped_crypto_state_t { + u16 major_version; + u16 minor_version; + crypto_flags_t cpflags; + cp_key_class_t persistent_class; + cp_key_os_version_t key_os_version; + cp_key_revision_t key_revision; + u16 key_len; + u8 persistent_key[0]; +}; + +struct wrapped_meta_crypto_state_t { + u16 major_version; + u16 minor_version; + crypto_flags_t cpflags; + cp_key_class_t persistent_class; + cp_key_os_version_t key_os_version; + cp_key_revision_t key_revision; + u16 unused; +}; + +struct j_crypto_key_t { + // j_key_t hdr; +}; + +struct j_crypto_val_t { + u32 refcnt; + wrapped_crypto_state_t state; +}; + +#define APFS_MODIFIED_NAMELEN 32 +struct apfs_modified_by_t { + u8 id[APFS_MODIFIED_NAMELEN]; + u64 timestamp; + xid_t last_xid; +}; + +bitfield apfs_fs_flags_t { + APFS_FS_UNENCRYPTED : 1; + APFS_FS_RESERVED_2 : 1; + APFS_FS_RESERVED_4 : 1; + APFS_FS_ONEKEY : 1; + APFS_FS_SPILLEDOVER : 1; + APFS_FS_RUN_SPILLOVER_CLEANER : 1; + APFS_FS_ALWAYS_CHECK_EXTENTREF : 1; + APFS_FS_RESERVED_80 : 1; + APFS_FS_RESERVED_100 : 1; + padding : 55; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 64)]]; + +bitfield apfs_incompatible_features_t { + APFS_INCOMPAT_CASE_INSENSITIVE : 1; + APFS_INCOMPAT_DATALESS_SNAPS : 1; + APFS_INCOMPAT_ENC_ROLLED : 1; + APFS_INCOMPAT_NORMALIZATION_INSENSITIVE : 1; + APFS_INCOMPAT_INCOMPLETE_RESTORE : 1; + APFS_INCOMPAT_SEALED_VOLUME : 1; + APFS_INCOMPAT_RESERVED_40 : 1; + padding : 57; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 64)]]; + +apfs_incompatible_features_t apfs_incompatible_features_null; + +#define APFS_MAGIC "BSPA" +#define APFS_MAGIC_LE "APSB" +#define APFS_MAX_HIST 8 +#define APFS_VOLNAME_LEN 256 +struct apfs_superblock_t { + obj_phys_t apfs_o; + type::Magic apfs_magic; + u32 apfs_fs_index; + u64 apfs_features; + u64 apfs_readonly_compatible_features; + apfs_incompatible_features_t apfs_incompatible_features; + u64 apfs_unmount_time; + u64 apfs_fs_reserve_block_count; + u64 apfs_fs_quota_block_count; + u64 apfs_fs_alloc_count; + wrapped_meta_crypto_state_t apfs_meta_crypto; + o_type_t apfs_root_tree_type; + o_type_t apfs_extentref_tree_type; + o_type_t apfs_snap_meta_tree_type; + oid_t apfs_omap_oid; + oid_t apfs_root_tree_oid; + oid_t apfs_extentref_tree_oid; + oid_t apfs_snap_meta_tree_oid; + xid_t apfs_revert_to_xid; + oid_t apfs_revert_to_sblock_oid; + u64 apfs_next_obj_id; + u64 apfs_num_files; + u64 apfs_num_directories; + u64 apfs_num_symlinks; + u64 apfs_num_other_fsobjects; + u64 apfs_num_snapshots; + u64 apfs_total_blocks_alloced; + u64 apfs_total_blocks_freed; + uuid_t apfs_vol_uuid; + u64 apfs_last_mod_time; + apfs_fs_flags_t apfs_fs_flags; + apfs_modified_by_t apfs_formatted_by; + apfs_modified_by_t apfs_modified_by[APFS_MAX_HIST]; + char apfs_volname[APFS_VOLNAME_LEN]; + u32 apfs_next_doc_id; + u16 apfs_role; + u16 reserved; + xid_t apfs_root_to_xid; + oid_t apfs_er_state_oid; + u64 apfs_cloneinfo_id_epoch; + u64 apfs_cloneinfo_xid; + oid_t apfs_snap_meta_ext_oid; + uuid_t apfs_volume_group_id; + oid_t apfs_integrity_meta_oid; + oid_t apfs_fext_tree_oid; + o_type_t apfs_fext_tree_type; + u32 reserved_type; + oid_t reserved_oid; +}; + +#define OBJ_ID_MASK 0x0FFFFFFFFFFFFFFF +#define OBJ_TYPE_MASK 0xF000000000000000 +#define OBJ_TYPE_SHIFT 60 + +struct j_inode_key_t { + // j_key_t hdr; +}; + +bitfield j_inode_flags_t { + INODE_IS_APFS_PRIVATE : 1; + INODE_MAINTAIN_DIR_STATS : 1; + INODE_DIR_STATS_ORIGIN : 1; + INODE_PROT_CLASS_EXPLICIT : 1; + INODE_WAS_CLONED : 1; + INODE_FLAGS_UNUSED : 1; + INODE_HAS_SECURITY_EA : 1; + INODE_BEING_TRUNCATED : 1; + INODE_HAS_FINDER_INFO : 1; + INODE_IS_SPARSE : 1; + INODE_WAS_EVER_CLONED : 1; + INODE_ACTIVE_FILE_TRIMMED : 1; + INODE_PINNED_TO_MAIN : 1; + INODE_PINNED_TO_TIER2 : 1; + INODE_HAS_RSRC_FORK : 1; + INODE_NO_RSRC_FORK : 1; + INODE_ALLOCATION_SPILLEDOVER : 1; + INODE_FAST_PROMOTE : 1; + INODE_HAS_UNCOMPRESSED_SIZE : 1; + INODE_IS_PURGEABLE : 1; + INODE_WANTS_TO_BE_PURGEABLE : 1; + INODE_IS_SYNC_ROOT : 1; + INODE_SNAPSHOT_COW_EXEMPTION : 1; + padding : 41; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 64)]]; + +#define INODE_INHERITED_INTERNAL_FLAGS (INODE_MAINTAIN_DIR_STATS | INODE_SNAPSHOT_COW_EXEMPTION) +#define INODE_CLONED_INTERNAL_FLAGS (INODE_HAS_RSRC_FORK | INODE_NO_RSRC_FORK | INODE_HAS_FINDER_INFO | INODE_SNAPSHOT_COW_EXEMPTION) + +#define OWNING_OBJ_ID_INVALID ~0ULL +#define OWNING_OBJ_ID_UNKNOWN ~1ULL + +#define JOBJ_MAX_KEY_SIZE 832 +#define JOBJ_MAX_VALUE_SIZE 3808 + +#define MIN_DOC_ID = 3; + +#define FEXT_CRYPTO_ID_IS_TWEAK 0x01 + +enum mode_t : u16 { + MODE_S_IFMT = 0o170000, + MODE_S_IFIFO = 0o010000, + MODE_S_IFCHR = 0o020000, + MODE_S_IFDIR = 0o040000, + MODE_S_IFBLK = 0o060000, + MODE_S_IFREG = 0o100000, + MODE_S_IFLNK = 0o120000, + MODE_S_IFSOCK = 0o140000, + MODE_S_IFWHT = 0o160000, +}; + +struct j_inode_val_t { + u64 parent_id; + u64 private_id; + type::time64_t create_time; + type::time64_t mod_time; + type::time64_t change_time; + type::time64_t access_time; + j_inode_flags_t internal_flags; + + u32 nchildren; // or links + + cp_key_class_t default_protection_class; + + u32 write_generation_counter; + u32 bsd_flags; + u32 owner; + u32 group; + mode_t mode; + u16 pad1; + u64 uncompressed_size; + u8 xfields[1]; +}; + +#define J_DREC_LEN_MASK 0x000003FF +#define J_DREC_HASH_MASK 0xFFFFFC00 +#define J_DREC_HASH_SHIFT 10 + +struct j_drec_key_t { + // j_key_t hdr; + u16 name_len; + char name[name_len]; +}; + +bitfield j_drec_hashed_len_t { + length : 10; + hash : 22; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 32)]]; + +struct j_drec_hashed_key_t { + // j_key_t hdr; + j_drec_hashed_len_t name_len_and_hash; + char name[name_len_and_hash.length]; +}; + +enum dir_rec_flags_type_t : u8 { + DT_UNKNOWN = 0, + DT_FIFO = 1, + DT_CHR = 2, + DT_DIR = 4, + DT_BLK = 6, + DT_REG = 8, + DT_LNK = 10, + DT_SOCK = 12, + DT_WHT = 14, +}; + +bitfield dir_rec_flags_t { + DREC_TYPE_MASK : 8; + RESERVED_10 : 1; + padding : 55; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 64)]]; + +struct j_drec_val_t { + u64 file_id; + u64 date_added; + dir_rec_flags_t flags; + u8 xfields[]; +}; + +struct j_dir_stats_key_t { + // j_key_t hdr; +}; + +struct j_dir_stats_val_t { + u64 num_children; + u64 total_size; + u64 chained_key; + u64 gen_count; +}; + +struct j_xattr_key_t { + // j_key_t hdr; + u16 name_len; + char name[name_len]; +}; + +bitfield j_xattr_flags_t { + XATTR_DATA_STREAM : 1; + XATTR_DATA_EMBEDDED : 1; + XATTR_FILE_SYSTEM_OWNED : 1; + XATTR_RESERVED_8 : 1; + padding : 12; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 16)]]; + +#define XATTR_MAX_EMBEDDED_SIZE 3804 +#define SYMLINK_EA_NAME "com.apple.fs.symlink" +#define FIRMLINK_EA_NAME "com.apple.fs.firmlink" +#define APFS_COW_EXEMPT_COUNT_NAME "com.apple.fs.cow-exempt-file-count" + +struct j_xattr_val_t { + j_xattr_flags_t flags; + u16 xdata_len; + u8 xdata[xdata_len]; +}; + +enum j_obj_kinds_t : u8 { + APFS_KIND_ANY = 0, + APFS_KIND_NEW = 1, + APFS_KIND_UPDATE = 2, + APFS_KIND_DEAD = 3, + APFS_KIND_UPDATE_REFCNT = 4, + + APFS_KIND_INVALID = 255 +}; + +bitfield j_inode_bsd_flags { + APFS_UF_NODUMP : 1; + APFS_UF_IMMUTABLE : 1; + APFS_UF_APPEND : 1; + APFS_UF_OPAQUE : 1; + APFS_UF_NOUNLINK : 1; + APFS_UF_COMPRESSED : 1; + APFS_UF_TRACKED : 1; + APFS_UF_DATAVAULT : 1; + reserved : 7; + APFS_UF_HIDDEN : 1; + APFS_SF_ARCHIVED : 1; + APFS_SF_IMMUTABLE : 1; + APFS_SF_APPEND : 1; + APFS_SF_RESTRICTED : 1; + APFS_SF_NOUNLINK : 1; + APFS_SF_SNAPSHOT : 1; + APFS_SF_FIRMLINK : 1; + padding : 6; + APFS_SF_DATALESS : 1; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 64)]]; + +#define INVALID_INO_NUM 0 +#define ROOT_DIR_PARENT 1 +#define ROOT_DIR_INO_NUM 2 +#define PRIV_DIR_INO_NUM 3 +#define SNAP_DIR_INO_NUM 6 +#define PURGEABLE_DIR_INO_NUM 7 +#define MIN_USER_INO_NUM 16 + +#define UNIFIED_ID_SPACE_MARK 0x0800000000000000 + +struct j_phys_ext_key_t { + // j_key_t hdr; +}; + +struct j_phys_ext_val_t { + u64 len_and_kind; + u64 owning_obj_id; + u32 refcnt; +}; + +#define PEXT_LEN_MASK 0x0fffffffffffffffULL +#define PEXT_KIND_MASK 0xf000000000000000ULL +#define PEXT_KIND_SHIFT 60 + +struct j_file_extent_key_t { + // j_key_t hdr; + u64 logical_addr; +}; + +struct j_file_extent_val_t { + u64 len_and_flags; + u64 phys_block_num; + u64 crypto_id; +}; + +#define J_FILE_EXTENT_LEN_MASK 0x00ffffffffffffffULL +#define J_FILE_EXTENT_FLAG_MASK 0xff00000000000000ULL +#define J_FILE_EXTENT_FLAG_SHIFT 56 + +struct j_dstream_id_key_t { + // j_key_t hdr; +}; + +struct j_dstream_id_val_t { + u32 refcnt; +}; + +struct j_dstream_t { + u64 size; + u64 alloced_size; + u64 default_crypto_id; + u64 total_bytes_written; + u64 total_bytes_read; +}; + +struct j_xattr_dstream_t { + u64 xattr_obj_id; + j_dstream_t dstream; +}; + +struct xf_blob_t { + u16 xf_num_exts; + u16 xf_used_data; + u8 xf_data[]; +}; + +enum x_type_t : u8 { + DREC_EXT_TYPE_SIBLING_ID = 1, + + INO_EXT_TYPE_SNAP_XID = 1, + INO_EXT_TYPE_DELTRA_TREE_OID = 2, + INO_EXT_TYPE_DOCUMENT_ID = 3, + INO_EXT_TYPE_NAME = 4, + INO_EXT_TYPE_PREV_FSIZE = 5, + INO_EXT_TYPE_RESERVED_6 = 6, + INO_EXT_TYPE_FINDER_INFO = 7, + INO_EXT_TYPE_DSTREAM = 8, + INO_EXT_TYPE_RESERVED_9 = 9, + INO_EXT_TYPE_DIR_STATS_KEY = 10, + INO_EXT_TYPE_FS_UUID = 11, + INO_EXT_TYPE_RESERVED_12 = 12, + INO_EXT_TYPE_SPARSE_BYTES = 13, + INO_EXT_TYPE_RDEV = 14, + INO_EXT_TYPE_PURGEABLE_FLAGS = 15, + INO_EXT_TYPE_ORIG_SYNC_ROOT_ID = 16 +}; + +bitfield x_flags_t { + XF_DATA_DEPENDENT : 1; + XF_DO_NOT_COPY : 1; + XF_RESERVED_4 : 1; + XF_CHILDREN_INHERIT : 1; + XF_USER_FIELD : 1; + XF_SYSTEM_FIELD : 1; + XF_RESERVED_40 : 1; + XF_RESERVED_80 : 1; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 8)]]; + +struct x_field_t { + x_type_t x_type; + x_flags_t x_flags; + u16 x_size; +}; + +struct j_sibling_key_t { + // j_key_t hdr; + u64 sibling_id; +}; + +struct j_sibling_val_t { + u64 parent_id; + u16 name_len; + char name[name_len]; +}; + +struct j_sibling_map_key_t { + // j_key_t hdr; +}; + +struct j_sibling_map_val_t { + u64 file_id; +}; + +struct j_snap_metadata_key_t { + // j_key_t hdr; +}; + +bitfield snap_meta_flags_t { + SNAP_META_PENDING_DATALESS : 1; + SNAP_META_MERGE_IN_PROGRESS : 1; + padding : 30; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 32)]]; + +struct j_snap_metadata_val_t { + oid_t extentref_tree_oid; + oid_t sblock_oid; + type::time64_t create_time; + type::time64_t change_time; + u64 inum; + o_type_t extentref_tree_type; + snap_meta_flags_t flags; + u16 name_len; + char name[name_len]; +}; + +struct j_snap_name_key_t { + // j_key_t hdr; + u16 name_len; + char name[name_len]; +}; + +struct j_snap_name_val_t { + xid_t snap_xid; +}; + +bitfield j_file_info_lba_t { + lba : 56; + type : 8; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 64)]]; + +#define J_FILE_INFO_LBA_MASK 0x00FFFFFFFFFFFFFF +#define J_FILE_INFO_TYPE_MASK 0xFF00000000000000 +#define J_FILE_INFO_TYPE_SHIFT 56 +struct j_file_info_key_t { + // j_key_t hdr; + j_file_info_lba_t info_and_lba; +}; + +struct j_file_data_hash_val_t { + u16 hashed_len; + u8 hash_size; + u8 hash[hash_size]; +}; + +struct j_file_info_val_t { + j_file_data_hash_val_t dhash; +}; + +enum j_obj_types_t : u8 { + APFS_TYPE_ANY = 0, + APFS_TYPE_SNAP_METADATA = 1, + APFS_TYPE_EXTENT = 2, + APFS_TYPE_INODE = 3, + APFS_TYPE_XATTR = 4, + APFS_TYPE_SIBLING_LINK = 5, + APFS_TYPE_DSTREAM_ID = 6, + APFS_TYPE_CRYPTO_STATE = 7, + APFS_TYPE_FILE_EXTENT = 8, + APFS_TYPE_DIR_REC = 9, + APFS_TYPE_DIR_STATS = 10, + APFS_TYPE_SNAP_NAME = 11, + APFS_TYPE_SIBLING_MAP = 12, + APFS_TYPE_FILE_INFO = 13, + APFS_TYPE_INVALID = 15, +}; + +bitfield j_key_t { + unsigned obj_id : 60; + j_obj_types_t obj_type : 4; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 64)]]; + +struct nloc_t { + u16 off; + u16 len; +}; + +struct kvloc_t { + nloc_t key_loc; + nloc_t val_loc; +}; + +struct kvoff_t { + u16 key_off; + u16 val_off; +}; + +struct kvgen_t { + u16 key_off = 0; + u16 val_off = 0; + if (btn_flags.BTNODE_FIXED_KV_SIZE) { + kvoff_t range; + + key_off = range.key_off; + val_off = range.val_off; + } else { + kvloc_t range; + + key_off = range.key_loc.off; + val_off = range.val_loc.off; + } + + match (subtype) { + (o_type_id_t::OBJECT_TYPE_OMAP): { + omap_key_t key @ key_area + key_off; + + if (btn_level > 0) { + oid_t node_oid @ val_area - val_off; + } else { + omap_val_t val @ val_area - val_off; + } + } + (o_type_id_t::OBJECT_TYPE_FSTREE | o_type_id_t::OBJECT_TYPE_BLOCKREFTREE | o_type_id_t::OBJECT_TYPE_SNAPMETATREE): { + j_key_t key @ key_area + key_off; + match (key.obj_type) { + (j_obj_types_t::APFS_TYPE_SNAP_METADATA): { + j_snap_metadata_key_t snap_meta_key @ key_area + key_off + sizeof (key); + + if (btn_level > 0) { + oid_t node_oid @ val_area - val_off; + } else { + j_snap_metadata_val_t snap_meta_val @ val_area - val_off; + } + } + (j_obj_types_t::APFS_TYPE_EXTENT): { + j_phys_ext_key_t phys_ext_key @ key_area + key_off + sizeof (key); + + if (btn_level > 0) { + oid_t node_oid @ val_area - val_off; + } else { + j_phys_ext_val_t phys_ext_val @ val_area - val_off; + } + } + (j_obj_types_t::APFS_TYPE_INODE): { + j_inode_key_t inode_key @ key_area + key_off + sizeof (key); + + if (btn_level > 0) { + oid_t node_oid @ val_area - val_off; + } else { + j_inode_val_t inode_val @ val_area - val_off; + } + } + (j_obj_types_t::APFS_TYPE_XATTR): { + j_xattr_key_t xattr_key @ key_area + key_off + sizeof (key); + + if (btn_level > 0) { + oid_t node_oid @ val_area - val_off; + } else { + j_xattr_val_t xattr_val @ val_area - val_off; + } + } + (j_obj_types_t::APFS_TYPE_SIBLING_LINK): { + j_sibling_key_t sibling_link_key @ key_area + key_off + sizeof (key); + + if (btn_level > 0) { + oid_t node_oid @ val_area - val_off; + } else { + j_sibling_val_t sibling_link_val @ val_area - val_off; + } + } + (j_obj_types_t::APFS_TYPE_DSTREAM_ID): { + j_dstream_id_key_t dstream_id_key @ key_area + key_off + sizeof (key); + + if (btn_level > 0) { + oid_t node_oid @ val_area - val_off; + } else { + j_dstream_id_val_t dstream_id_val @ val_area - val_off; + } + } + (j_obj_types_t::APFS_TYPE_CRYPTO_STATE): { + j_crypto_key_t crypto_key @ key_area + key_off + sizeof (key); + + if (btn_level > 0) { + oid_t node_oid @ val_area - val_off; + } else { + j_crypto_val_t crypto_val @ val_area - val_off; + } + } + (j_obj_types_t::APFS_TYPE_FILE_EXTENT): { + j_file_extent_key_t file_ext_key @ key_area + key_off + sizeof (key); + + if (btn_level > 0) { + oid_t node_oid @ val_area - val_off; + } else { + j_file_extent_val_t file_ext_val @ val_area - val_off; + } + } + (j_obj_types_t::APFS_TYPE_DIR_REC): { + if (vol_incomp.APFS_INCOMPAT_CASE_INSENSITIVE || + vol_incomp.APFS_INCOMPAT_NORMALIZATION_INSENSITIVE) { + j_drec_hashed_key_t drec_key @ key_area + key_off + sizeof (key); + } else { + j_drec_key_t drec_key @ key_area + key_off + sizeof (key); + } + + if (btn_level > 0) { + oid_t node_oid @ val_area - val_off; + } else { + j_drec_val_t drec_val @ val_area - val_off; + } + } + (j_obj_types_t::APFS_TYPE_DIR_STATS): { + j_dir_stats_key_t dir_stats_key @ key_area + key_off + sizeof (key); + + if (btn_level > 0) { + oid_t node_oid @ val_area - val_off; + } else { + j_dir_stats_val_t dir_stats_val @ val_area - val_off; + } + } + (j_obj_types_t::APFS_TYPE_SNAP_NAME): { + j_snap_name_key_t snap_name_key @ key_area + key_off + sizeof (key); + + if (btn_level > 0) { + oid_t node_oid @ val_area - val_off; + } else { + j_snap_name_val_t snap_name_val @ val_area - val_off; + } + } + (j_obj_types_t::APFS_TYPE_SIBLING_MAP): { + j_sibling_key_t sibling_key @ key_area + key_off + sizeof (key); + + if (btn_level > 0) { + oid_t node_oid @ val_area - val_off; + } else { + j_sibling_val_t sibling_val @ val_area - val_off; + } + } + (j_obj_types_t::APFS_TYPE_FILE_INFO): { + j_file_info_key_t file_info_key @ key_area + key_off + sizeof (key); + + if (btn_level > 0) { + oid_t node_oid @ val_area - val_off; + } else { + j_file_info_val_t file_info_val @ val_area - val_off; + } + } + } + } + } +}; + +bitfield btn_flags_t { + BTNODE_ROOT : 1; + BTNODE_LEAF : 1; + BTNODE_FIXED_KV_SIZE : 1; + BTNODE_HASHED : 1; + BTNODE_NOHEADER : 1; + padding : 10; + BTNODE_CHECK_KOFF_INVAL : 1; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 16)]]; + +bitfield bt_flags_t { + BTREE_UINT64_KEYS : 1; + BTREE_SEQUENTIAL_INSERT : 1; + BTREE_ALLOW_GHOSTS : 1; + BTREE_EPHEMERAL : 1; + BTREE_PHYSICAL : 1; + BTREE_NONPERSISTENT : 1; + BTREE_KV_NONALIGNED : 1; + BTREE_HASHED : 1; + BTREE_NOHEADER : 1; + padding : 23; +} [[bitfield_order(std::core::BitfieldOrder::LeastToMostSignificant, 32)]]; + +struct btree_info_fixed_t { + bt_flags_t bt_flags; + u32 bt_node_size; + u32 bt_key_size; + u32 bt_val_size; +}; + +struct btree_info_t { + btree_info_fixed_t bt_fixed; + u32 bt_longest_key; + u32 bt_longest_val; + u64 bt_key_count; + u64 bt_node_count; +}; + +struct btree_node_phys_t { + u32 node_start = $; + + obj_phys_t btn_o; + btn_flags_t btn_flags; + u16 btn_level; + u32 btn_nkeys; + nloc_t btn_table_space; + nloc_t btn_free_space; + nloc_t btn_key_free_list; + nloc_t btn_val_free_list; + + if (btn_table_space.off > $) { + padding[btn_table_space.off - $]; + } + u32 btn_toc_start = $; + + padding[btn_table_space.len]; + u32 key_area = $; + u32 val_area = node_start + block_size; + if (btn_flags.BTNODE_ROOT) { + val_area -= sizeof (btree_info_t); + } + + u8 btn_key_area; + kvgen_t btn_toc[btn_nkeys] @ btn_toc_start; + + padding[btn_free_space.off]; + u8 btn_free_area_start; + + std::mem::AlignTo; + + if (btn_flags.BTNODE_ROOT) { + btree_info_t info @ node_start + block_size - sizeof (btree_info_t); + } +}; + +#define BTREE_NODE_HASH_SIZE_MAX 64 +struct btn_index_node_val_t { + oid_t binv_child_oid; + u8 binv_child_hash[BTREE_NODE_HASH_SIZE_MAX]; +}; + +// ================== HELPERS ================== +fn fletcher64(u64 offset, u64 count, u64 init) { + u64 sum1 = init & 0xFFFFFFFF; + u64 sum2 = init >> 32; + + for (u64 key = 0, key < count, key += 1) { + u32 data @ offset + key * sizeof (u32); + sum1 += data; + sum2 += sum1; + } + + sum1 %= 0xFFFFFFFF; + sum2 %= 0xFFFFFFFF; + + return (sum2 << 32) | sum1; +}; + +fn block_verify(u64 offset, u64 size) { + u64 chk @ offset; + if (chk == 0) + return false; + if (chk == 0xFFFFFFFFFFFFFFFF) + return false; + + u64 cks = fletcher64(offset + MAX_CKSUM_SIZE, size / sizeof (u32) - 2, 0); + cks = fletcher64(offset, MAX_CKSUM_SIZE / sizeof (u32), cks); + + std::assert(cks == 0, std::format("block verification failed, offset: 0x{:X}, size: 0x{:X}", offset, size)); +}; + +fn object_get_latest(u64 offset, u64 count, u64 size, o_type_id_t type, u64 max = 0) { + if (max == 0) { + max = count; + } + + u64 max_xid = 0; + u64 result = 0; + for (u64 address = offset, address <= (offset + count), address += 1) { + if ((address - offset) >= max) { + return result; + } + + obj_phys_t object @ address * size; + + if (object.o_type.t_type != type) { + continue; + } + + if (max_xid < object.o_xid) { + max_xid = object.o_xid; + result = address * size; + } + } + + return result; +}; + +fn checkpoint_map_lookup(checkpoint_map_phys_t checkpoint_map, oid_t oid, o_type_id_t type) { + for (u64 iter = 0, iter < checkpoint_map.cpm_count, iter += 1) { + if (checkpoint_map.cpm_map[iter].cpm_oid == oid && + checkpoint_map.cpm_map[iter].cpm_type.t_type == type) { + return checkpoint_map.cpm_map[iter]; + } + } + + checkpoint_mapping_t empty; + return empty; +}; + +fn keybag_is_encrypted(media_keybag_t keybag) { + bool is_decrypted = + keybag.mk_obj.o_type == o_type_id_t::OBJECT_TYPE_CONTAINER_KEYBAG && + keybag.mk_obj.o_subtype == o_type_id_t::OBJECT_TYPE_CONTAINER_KEYBAG_2; + + std::assert(is_decrypted, "encrypted keybags are not supported"); +}; + +fn omap_node_lookup(paddr_t off, u64 block_size, oid_t oid, xid_t xid) { + while (true) { + btree_node_phys_t node @ off; + if (node.btn_nkeys <= 0) { + return 0; + } + + s64 beg = 0; + s64 end = node.btn_nkeys - 1; + s64 mid = 0; + s64 idx = 0; + while (beg <= end) { + mid = beg + (end - beg) / 2; + + omap_key_t key = node.btn_toc[mid].key; + if (oid > key.ok_oid) { + beg += 1; + } else if (oid < key.ok_oid) { + end -= 1; + } else if (xid > key.ok_xid) { + beg += 1; + idx = mid; + } else if (xid < key.ok_xid) { + end -= 1; + idx = mid; + } else { + idx = mid; + break; + } + } + + if (node.btn_level > 0) { + off = node.btn_toc[idx].node_oid * block_size; + continue; + } + + omap_key_t key = node.btn_toc[idx].key; + if (key.ok_oid == oid) { + return node.btn_toc[idx].val.ov_paddr; + } else { + return 0; + } + } +}; + +fn fstree_inode_lookup( + paddr_t root_off, + paddr_t omap_off, + u64 block_size, + u64 ino, + apfs_incompatible_features_t incomp, + xid_t xid +) { + u64 inode_key = (j_obj_types_t::APFS_TYPE_INODE << OBJ_TYPE_SHIFT) | (ino & OBJ_ID_MASK); + u64 node_off = root_off; + while (true) { + btree_node_phys_t node @ node_off; + + s64 beg = 0; + s64 end = node.btn_nkeys - 1; + s64 mid = 0; + s64 current = 0; + while (beg <= end) { + mid = beg + (end - beg) / 2; + u64 entry_key @ addressof (node.btn_toc[mid].key); + if ((entry_key & OBJ_ID_MASK) > (inode_key & OBJ_ID_MASK)) { + end -= 1; + } else if ((entry_key & OBJ_ID_MASK) < (inode_key & OBJ_ID_MASK)) { + beg += 1; + } else if (((entry_key & OBJ_TYPE_MASK) >> OBJ_TYPE_SHIFT) > ((inode_key & OBJ_TYPE_MASK) >> OBJ_TYPE_SHIFT)) { + end -= 1; + current = mid; + } else if (((entry_key & OBJ_TYPE_MASK) >> OBJ_TYPE_SHIFT) < ((inode_key & OBJ_TYPE_MASK) >> OBJ_TYPE_SHIFT)) { + beg += 1; + current = mid; + } else { + current = mid; + break; + } + } + + if (node.btn_level > 0) { + node_off = omap_node_lookup( + omap_off, + block_size, + node.btn_toc[current].node_oid, + xid) * superblock.nx_block_size; + continue; + } + + u64 entry_key @ addressof (node.btn_toc[current].key); + if (entry_key != inode_key) { + return 0; + } + + return addressof (node.btn_toc[current].inode_val); + } +}; + +// ================== PARSE ================== +nx_superblock_t initial_superblock @ 0x00; +nx_superblock_t superblock @ object_get_latest( + initial_superblock.nx_xp_desc_base, + initial_superblock.nx_xp_desc_blocks, + initial_superblock.nx_block_size, + o_type_id_t::OBJECT_TYPE_NX_SUPERBLOCK); + +checkpoint_map_phys_t checkpoint_map @ (superblock.nx_xp_desc_base + superblock.nx_xp_desc_index) * superblock.nx_block_size; + +omap_phys_t object_map @ superblock.nx_omap_oid * superblock.nx_block_size; +btree_node_phys_t object_map_tree @ object_map.om_tree_oid * superblock.nx_block_size; + +apfs_superblock_t volume @ omap_node_lookup( + addressof (object_map_tree), + superblock.nx_block_size, + superblock.nx_fs_oid[0], + superblock.nx_o.o_xid) * superblock.nx_block_size; +omap_phys_t volume_object_map @ volume.apfs_omap_oid * superblock.nx_block_size; +btree_node_phys_t volume_object_map_tree @ volume_object_map.om_tree_oid * superblock.nx_block_size; + +btree_node_phys_t volume_root_tree @ omap_node_lookup( + addressof (volume_object_map_tree), + superblock.nx_block_size, + volume.apfs_root_tree_oid, + volume.apfs_o.o_xid) * superblock.nx_block_size; +j_inode_val_t volume_root_folder @ fstree_inode_lookup( + addressof (volume_root_tree), + addressof (volume_object_map_tree), + superblock.nx_block_size, + ROOT_DIR_INO_NUM, + volume.apfs_incompatible_features, + volume.apfs_o.o_xid +); + +btree_node_phys_t volume_extentref_tree @ volume.apfs_extentref_tree_oid * superblock.nx_block_size; +btree_node_phys_t volume_snapshot_tree @ volume.apfs_snap_meta_tree_oid * superblock.nx_block_size; + +apfs_superblock_t snapshot_volume @ volume_snapshot_tree.btn_toc[0].snap_meta_val.sblock_oid * superblock.nx_block_size; +btree_node_phys_t snapshot_extentref_tree @ volume_snapshot_tree.btn_toc[0].snap_meta_val.extentref_tree_oid * superblock.nx_block_size; + +checkpoint_mapping_t spaceman_cp = checkpoint_map_lookup(checkpoint_map, superblock.nx_spaceman_oid, o_type_id_t::OBJECT_TYPE_SPACEMAN); +spaceman_phys_t spaceman @ spaceman_cp.cpm_paddr * superblock.nx_block_size; + +checkpoint_mapping_t spaceman_freequeue_manager_cp = checkpoint_map_lookup( + checkpoint_map, + spaceman.sm_fq[sfq_t::SFQ_IP].sfq_tree_oid, + o_type_id_t::OBJECT_TYPE_BTREE); +btree_node_phys_t spaceman_freequeue_manager @ spaceman_freequeue_manager_cp.cpm_paddr * superblock.nx_block_size; + +checkpoint_mapping_t spaceman_freequeue_volume_cp = checkpoint_map_lookup( + checkpoint_map, + spaceman.sm_fq[sfq_t::SFQ_MAIN].sfq_tree_oid, + o_type_id_t::OBJECT_TYPE_BTREE); + +btree_node_phys_t spaceman_freequeue_volume @ spaceman_freequeue_volume_cp.cpm_paddr * superblock.nx_block_size; + +paddr_t spaceman_main_cibs_addr @ addressof (spaceman) + spaceman.sm_dev[smdev_t::SD_MAIN].sm_addr_offset; +chunk_info_block_t spaceman_main_cibs[spaceman.sm_dev[smdev_t::SD_MAIN].sm_cib_count] @ spaceman_main_cibs_addr * superblock.nx_block_size; + +paddr_t spaceman_tier2_cibs_addr @ addressof (spaceman) + spaceman.sm_dev[smdev_t::SD_TIER2].sm_addr_offset; +chunk_info_block_t spaceman_tier2_cibs[spaceman.sm_dev[smdev_t::SD_TIER2].sm_cib_count] @ spaceman_tier2_cibs_addr * superblock.nx_block_size; + +checkpoint_mapping_t reaper_cp = checkpoint_map_lookup(checkpoint_map, superblock.nx_reaper_oid, o_type_id_t::OBJECT_TYPE_NX_REAPER); +nx_reaper_phys_t reaper @ reaper_cp.cpm_paddr * superblock.nx_block_size; + +media_keybag_t container_keybag @ superblock.nx_keylocker.pr_start_paddr * superblock.nx_block_size; + +// ================== VERIFY ================== +block_verify(addressof (initial_superblock), initial_superblock.nx_block_size); +block_verify(addressof (superblock), superblock.nx_block_size); +block_verify(addressof (object_map), superblock.nx_block_size); +block_verify(addressof (object_map_tree), superblock.nx_block_size); +block_verify(addressof (volume), superblock.nx_block_size); +block_verify(addressof (volume_object_map), superblock.nx_block_size); +block_verify(addressof (volume_root_tree), superblock.nx_block_size); +block_verify(addressof (spaceman), superblock.nx_block_size); +block_verify(addressof (reaper), superblock.nx_block_size); +block_verify(addressof (spaceman_freequeue_manager), superblock.nx_block_size); +block_verify(addressof (spaceman_freequeue_volume), superblock.nx_block_size); +block_verify(addressof (container_keybag), superblock.nx_block_size); +keybag_is_encrypted(container_keybag); \ No newline at end of file From 2a9676238f97747319d5786fc4d09b22db6bdbae Mon Sep 17 00:00:00 2001 From: tympanicblock61 <99092380+tympanicblock61@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:54:19 -0500 Subject: [PATCH 93/93] pattern: Add lua 4.0 pattern (#465) * add lua 4.0 pattern * fix offset the top level function is at * Update README.md --------- Co-authored-by: Nik --- README.md | 1 + patterns/lua40.hexpat | 133 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 patterns/lua40.hexpat diff --git a/README.md b/README.md index 6687304f..aadeb3db 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | JPEG | `image/jpeg` | [`patterns/jpeg.hexpat`](patterns/jpeg.hexpat) | JPEG Image Format | | KTX | `image/ktx` | [`patterns/ktx.hexpat`](patterns/ktx.hexpat) | Khronos TeXture 1.0 | | LOC | | [`patterns/loc.hexpat`](patterns/loc.hexpat) | Minecraft Legacy Console Edition Language file | +| Lua 4.0 | | [`patterns/lua40.hexpat`](patterns/lua40.hexpat) | Lua 4.0 bytecode | | LUC | | [`patterns/popcap_luc.hexpat`](patterns/popcap_luc.hexpat) | PopCap's proprietary Lua bytecode | | Lua 5.1 | | [`patterns/lua51.hexpat`](patterns/lua51.hexpat) | Lua 5.1 bytecode | | Lua 5.2 | | [`patterns/lua52.hexpat`](patterns/lua52.hexpat) | Lua 5.2 bytecode | diff --git a/patterns/lua40.hexpat b/patterns/lua40.hexpat new file mode 100644 index 00000000..6e30e047 --- /dev/null +++ b/patterns/lua40.hexpat @@ -0,0 +1,133 @@ +#pragma description Lua 4.0 bytecode +#pragma magic [ 1B 4C 75 61 40 ] @ 0x00 +// based off of https://www.lua.org/source/4.0/dump.c.html + +import std.io; + +namespace impl { + fn format_LuaString(auto string) { + if (string.size == 0) { + return "None"; + } + return std::format("\"{}\"", string.data); + }; + + fn format_Version(auto ver) { + return std::format("Ver. {}.{}", ver.major, ver.minor); + }; +} +using LuaFunction; + +bitfield Version { + minor : 4; + major : 4; +} [[format("impl::format_Version")]]; + +struct LuaBinaryHeader { + u8 id_chunk; + char magic[3]; + Version version; + u8 endianness; + u8 size_of_int; + u8 size_of_size_t; + u8 size_of_instruction; // ??? + u8 size_INSTRUCTION; // SIZE_INSTRUCTION in Lua 4 source + u8 size_OP; // SIZE_OP + u8 size_B; // SIZE_B + u8 size_number; // sizeof(Number) + + if (size_number == 4) { + u32 TEST_NUMBER; + } else { + u64 TEST_NUMBER; + } +}; + +LuaBinaryHeader header @ 0; + +struct LuaString { + if (header.size_of_size_t == 4) { + u32 size; + } else { + u64 size; + } + + if (size > 0) { + char data[size]; + } +}[[format("impl::format_LuaString")]]; + +struct Vector { + if (header.size_of_int == 4) { + u32 size; + } else { + u64 size; + } + if (size > 0) { + T values[size]; + } +}; + +struct LocalVar { + LuaString varname; + if (header.size_of_int == 4) { + u32 startpc; + u32 endpc; + } else { + u64 startpc; + u64 endpc; + + } +}; + +struct LuaDebugInfo { + Vector localVar; + if (header.size_of_int == 4) { + Vector lineInfo; // i think this is correct + } else { + Vector lineInfo; + } + +}; + +bitfield LuaNumber { + raw : header.size_number; +}; + +struct LuaConstants{ + Vector stringConstants; + if (header.size_of_int == 4) { + Vector intConstants; + } else { + Vector intConstants; + } + Vector protos; +}; + +struct LuaFunction { + LuaString source; + if (header.size_of_int == 4) { + u32 linedefined; + u32 numparams; + } else { + u64 linedefined; + u64 numparams; + } + u8 is_vararg; + + if (header.size_of_int == 4) { + u32 maxstacksize; + } else { + u64 maxstacksize; + } + + LuaDebugInfo debugInfo; + LuaConstants luaConstants; + if (header.size_of_int == 4) { + Vector code; + } else { + Vector code; + } +}; + +LuaFunction toplevelFunction @ sizeof(header);; // Lua header size is not always the same