From a41de2864620d86669b1d1fa5800d776e7680471 Mon Sep 17 00:00:00 2001 From: Hikodroid <172511799+Hikodroid@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:58:48 +0200 Subject: [PATCH] patterns: Added pattern for LZNT1 compressed data --- README.md | 1 + patterns/lznt1.hexpat | 206 ++++++++++++++++++++ tests/patterns/test_data/lznt1.hexpat.lznt1 | Bin 0 -> 10940 bytes 3 files changed, 207 insertions(+) create mode 100644 patterns/lznt1.hexpat create mode 100644 tests/patterns/test_data/lznt1.hexpat.lznt1 diff --git a/README.md b/README.md index 2acf9c32..2770681f 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | Lua 5.3 | | [`patterns/lua53.hexpat`](patterns/lua53.hexpat) | Lua 5.3 bytecode | | Lua 5.4 | | [`patterns/lua54.hexpat`](patterns/lua54.hexpat) | Lua 5.4 bytecode | | LCE Savefile | | [`patterns/lcesave.hexpat`](patterns/lcesave.hexpat) | Minecraft Legacy Console Edition save file | +| LZNT1 | | [`patterns/lznt1.hexpat`](patterns/lznt1.hexpat) | LZNT1 compressed data format | | Mach-O | `application/x-mach-binary` | [`patterns/macho.hexpat`](patterns/macho.hexpat) | Mach-O executable | | MIDI | `audio/midi` | [`patterns/midi.hexpat`](patterns/midi.hexpat) | MIDI header, event fields provided | | MiniDump | `application/x-dmp` | [`patterns/minidump.hexpat`](patterns/minidump.hexpat) | Windows MiniDump files | diff --git a/patterns/lznt1.hexpat b/patterns/lznt1.hexpat new file mode 100644 index 00000000..dfbfe441 --- /dev/null +++ b/patterns/lznt1.hexpat @@ -0,0 +1,206 @@ +#pragma description LZNT1 +#pragma endian little +#pragma pattern_limit 1000000 + +/* + * References: + * https://github.com/libyal/libfwnt/blob/main/documentation/Compression%20methods.asciidoc + * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-xca/5655f4a3-6ba4-489b-959f-e1f407c52f15 + * https://github.com/tuxera/ntfs-3g/blob/edge/libntfs-3g/compress.c + */ + +import std.core; +import std.io; +import std.mem; +import std.sys; + +using BitfieldOrder = std::core::BitfieldOrder; +bool createDecompressedSection in; +std::mem::Section decompressedSection = 0; +u128 uncompressedDataSize = 0; +u128 maxUnitSize = 4096; + +fn appendData(ref auto data) { + if (createDecompressedSection) { + std::mem::copy_value_to_section(data, + decompressedSection, + std::mem::get_section_size(decompressedSection)); + } + + uncompressedDataSize += sizeof(data); +}; + +struct Value { + u8 value; +}; + +fn appendU8(u8 data) { + Value value; + value.value = data; + appendData(value); +}; + +bitfield Flag { + bool A : 1; + bool B : 1; + bool C : 1; + bool D : 1; + bool E : 1; + bool F : 1; + bool G : 1; + bool H : 1; +} [[bitfield_order(BitfieldOrder::LeastToMostSignificant, 8)]]; + +bitfield CompressedTuple { + std::assert(lengthSize >= 4 && lengthSize <= 12, + std::format("lengthSize has an invalid value {}. Must be between 4 and 12.", lengthSize)); + std::assert(displacementSize >= 4 && displacementSize <= 12, + std::format("displacementSizehas an invalid value {}. Must be between 4 and 12.", displacementSize)); + std::assert((lengthSize + displacementSize) == 16, + std::format("lengthSize {} and displacementSize {} must add up to 16.", lengthSize, displacementSize)); + unsigned length : lengthSize; + unsigned displacement : displacementSize; + u16 actualLength = u16(length) + 3 [[export]]; + s16 actualDisplacement = -1 * (s16(displacement) + 1) [[export]]; +} [[bitfield_order(BitfieldOrder::LeastToMostSignificant, 16)]]; + +fn calculateLengthSize() { + s8 lengthSize = 12; + + for (u128 i = uncompressedDataSize - 1, i >= 0x10, i = i >> 1) { + lengthSize = lengthSize - 1; + } + + if (lengthSize < 4) { + lengthSize = 4; + } + + return u8(lengthSize); +}; + +fn copySequentially(std::mem::Section section, u128 sourcePos, u128 destinationPos, u128 length) { + for (u128 i = 0, i < length, i = i + 1) { + std::mem::copy_section_to_section(section, + sourcePos + i, + section, + destinationPos + i, + 1); + } +}; + +struct Tuple { + std::assert(uncompressedDataSize > 0, + "uncompressedDataSize must be greater than zero" + + " because otherwise there would be no data to backrefrence!"); + u8 ls = calculateLengthSize(); + CompressedTuple ct[[inline]]; + std::assert((-1 * ct.actualDisplacement) <= uncompressedDataSize, + std::format("The actualDisplacement {} is referencing data before the beginning" + + " of the current decompressed chunk! Current decompressed size is {}.", + ct.actualDisplacement, + uncompressedDataSize)); + + if (createDecompressedSection) { + u128 destinationPos = std::mem::get_section_size(decompressedSection); + u128 destinationBackrefPos = destinationPos + ct.actualDisplacement; + u128 maxNonOverlap = destinationPos - destinationBackrefPos; + + if (ct.actualLength <= maxNonOverlap) { // Not overlapping + std::mem::copy_section_to_section(decompressedSection, + destinationBackrefPos, + decompressedSection, + destinationPos, + ct.actualLength); + } else { // Overlapping + // Copy non-overlapping part + std::mem::copy_section_to_section(decompressedSection, + destinationBackrefPos, + decompressedSection, + destinationPos, + maxNonOverlap); + // Copy overlapping part + destinationPos += maxNonOverlap; + destinationBackrefPos += maxNonOverlap; + copySequentially(decompressedSection, destinationBackrefPos, + destinationPos, ct.actualLength - maxNonOverlap); + } + } + + uncompressedDataSize += ct.actualLength; + std::assert(uncompressedDataSize <= maxUnitSize, + std::format("uncompressedDataSize {} is larger than the maximum allowed size of {}.", + uncompressedDataSize, + maxUnitSize)); +}; + +struct FlagGroup { + Flag flag [[comment("Each bit represents whether a data element in a group of 8 is compressed " + + "with the first value being the least significant bit.")]]; + // (up to) 8 data elements + if ($ >= endOffset) break; + if (flag.A) { Tuple A; } else { u8 A; appendU8(A); } + if ($ >= endOffset) break; + if (flag.B) { Tuple B; } else { u8 B; appendU8(B); } + if ($ >= endOffset) break; + if (flag.C) { Tuple C; } else { u8 C; appendU8(C); } + if ($ >= endOffset) break; + if (flag.D) { Tuple D; } else { u8 D; appendU8(D); } + if ($ >= endOffset) break; + if (flag.E) { Tuple E; } else { u8 E; appendU8(E); } + if ($ >= endOffset) break; + if (flag.F) { Tuple F; } else { u8 F; appendU8(F); } + if ($ >= endOffset) break; + if (flag.G) { Tuple G; } else { u8 G; appendU8(G); } + if ($ >= endOffset) break; + if (flag.H) { Tuple H; } else { u8 H; appendU8(H); } +}; + +bitfield ChunkHeader { + unsigned chunkDataSize : 12 [[comment("The actual value stored is the chunk data size - 1.")]]; + unsigned signatureValue : 3 [[comment("The value is always 3 except in an all-zero chunk header.")]]; + bool isCompressed : 1; +} [[bitfield_order(BitfieldOrder::LeastToMostSignificant, 16)]]; + +struct Chunk { + auto headerPos = $; + ChunkHeader header; + + /* An all-zero chunk header indicates the end of an LZNT1 compression stream. + Might not be present if there is not enough space to store it. */ + if (header.chunkDataSize == 0 + && header.signatureValue == 0 + && !header.isCompressed) { + break; + } + + std::assert_warn(header.signatureValue == 3, + std::format("ChunkHeader @ {:#x} has a signatureValue other than 3: {}", + headerPos, header.signatureValue)); + + u128 actualChunkDataSize = u128(header.chunkDataSize) + 1 [[export]]; + + if (header.isCompressed) { + auto currentEndOffset = $ + actualChunkDataSize; + uncompressedDataSize = 0; + FlagGroup data[while($ < currentEndOffset)]; + std::assert($ == currentEndOffset, + std::format("Invalid size of Chunk @ {:#x}.", headerPos)); + } else { + std::assert_warn(actualChunkDataSize == maxUnitSize, + std::format("actualChunkDataSize {} must be equal to maxUnitSize {}.", + actualChunkDataSize, + maxUnitSize)); + u8 data[actualChunkDataSize]; + appendData(data); + } +}; + +struct LZNT1 { + if (createDecompressedSection) { + decompressedSection = std::mem::create_section(std::format("decompressed @ {:#x}", $)); + } + + Chunk chunks[while($ < sizeof($))]; +}; + +LZNT1 lznt1 @ 0x00; \ No newline at end of file diff --git a/tests/patterns/test_data/lznt1.hexpat.lznt1 b/tests/patterns/test_data/lznt1.hexpat.lznt1 new file mode 100644 index 0000000000000000000000000000000000000000..07d4fa2cd761517b5918a3cf07c7927d553758b1 GIT binary patch literal 10940 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 literal 0 HcmV?d00001