From 2cc88687274f1a364e9fcb0a0f27d69bb0aad0fc Mon Sep 17 00:00:00 2001 From: Luca Corbatto Date: Sat, 14 Dec 2024 13:48:06 +0100 Subject: [PATCH] patterns: Added rar file pattern as requested in #258 (#324) * Add rar file pattern as requested in #258 * Fix rar pattern Removed some experimental (and apperantly broken) code. * Break on EndOfArchive header instead of EOF --------- Co-authored-by: Nik --- README.md | 1 + patterns/rar.hexpat | 361 ++++++++++++++++++++++++ tests/patterns/test_data/rar.hexpat.rar | Bin 0 -> 452 bytes 3 files changed, 362 insertions(+) create mode 100644 patterns/rar.hexpat create mode 100644 tests/patterns/test_data/rar.hexpat.rar diff --git a/README.md b/README.md index 909522f..e1e4daa 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | QBCL | | [`patterns/qbcl.hexpat`](patterns/qbcl.hexpat) | Qubicle voxel scene project file | | QOI | `image/qoi` | [`patterns/qoi.hexpat`](patterns/qoi.hexpat) | QOI image files | | quantized-mesh | | [`patterns/quantized-mesh.hexpat`](patterns/quantized-mesh.hexpat) | Cesium quantized-mesh terrain | +| RAR | `application/x-rar` | [`patterns/rar.hexpat`](patterns/rar.hexpat) | RAR archive file format | | RAS | `image/x-sun-raster` | [`patterns/ras.hexpat`](patterns/ras.hexpat) | RAS image files | | ReFS | | [`patterns/refs.hexpat`](patterns/refs.hexpat) | Microsoft Resilient File System | | RGBDS | | [`patterns/rgbds.hexpat`](patterns/rgbds.hexpat) | [RGBDS](https://rgbds.gbdev.io) object file format | diff --git a/patterns/rar.hexpat b/patterns/rar.hexpat new file mode 100644 index 0000000..34a20ef --- /dev/null +++ b/patterns/rar.hexpat @@ -0,0 +1,361 @@ +#pragma author targodan +#pragma description RAR archive file format +#pragma MIME application/x-rar + +import type.magic; +import type.byte; +import std.mem; +import std.math; +import std.string; +import std.time; + +// Source: https://www.rarlab.com/technote.htm + +#define RARv5_MAGIC "Rar!\x1a\x07\x01\x00" + +using FILETIME = u64 [[format("format_windows_time")]]; +using time_t = std::time::EpochTime [[format("format_unix_time")]]; +using nanotime_t = u64 [[format("format_unix_time")]]; + +fn format_unix_time(time_t t) { + return std::time::format(std::time::to_local(t), "%c"); +}; + +fn format_windows_time(FILETIME t) { + return format_unix_time(std::time::filetime_to_unix(t)); +}; + +struct vint { + u8 data[while(std::mem::read_unsigned($, 1) & 0x80 != 0)]; + u8 last; +} [[sealed, transform("vint_value"), format("vint_value")]]; + +fn vint_value(vint vi) { + u64 value = 0; + u8 i = 0; + for(i = 0, i < sizeof(vi.data), i = i + 1) { + value |= (vi.data[i] & 0x7F) << (i * 7); + } + value |= vi.last << (i * 7); + return value; +}; + +bitfield LocatorRecordFlags { + bool quickOpenRecordOffsetIsPresent : 1; + bool recoveryRecordOffsetIsPresent : 1; +}; + +struct LocatorRecord { + LocatorRecordFlags flags; // should be vint but not compatible with bitfield + if (flags.quickOpenRecordOffsetIsPresent) { + vint quickOpenRecordOffset; + } + if (flags.recoveryRecordOffsetIsPresent) { + vint recoveryRecordOffset; + } +}; + +bitfield MetadataRecordFlags { + bool archiveNameIsPresent : 1; + bool archiveOriginalCreationTimeIsPresent : 1; + bool timeIsUnixTime : 1; + bool unixTimeIsNanosecondPrecision : 1; +}; + +struct MetadataRecord { + MetadataRecordFlags flags; // should be vint but not compatible with bitfield + if (flags.archiveNameIsPresent) { + vint nameLength; + char name[nameLength]; + } + if (flags.archiveOriginalCreationTimeIsPresent) { + match (flags.timeIsUnixTime, flags.unixTimeIsNanosecondPrecision) { + (true, true): nanotime_t originalCreationTime; + (true, false): time_t originalCreationTime; + (false, _): FILETIME originalCreationTime; + } + } +}; + +enum MainHeaderExtraRecordType : vint { + Locator = 1, + Metadata = 2 +}; + +struct MainHeaderExtraRecord { + vint size; + MainHeaderExtraRecordType type; + match(type) { + (MainHeaderExtraRecordType::Locator): LocatorRecord record [[inline]]; + (MainHeaderExtraRecordType::Metadata): MetadataRecord record [[inline]]; + } +}; + +bitfield HeaderFlags { + bool extraAreaIsPresent : 1; + bool dataAreaIsPresent : 1; +}; + +bitfield ArchiveFlags { + bool archiveIsPartOfMultiVolumeSet : 1; + bool volumeNumberFieldIsPresent : 1; + bool isSolidArchive : 1; + bool recoveryRecordIsPresent : 1; + bool isLockedArchive : 1; +}; + +enum HeaderType : vint { + MainHeader = 1, + FileHeader, + ServiceHeader, + EncryptionHeader, + EndOfArchive +}; + +struct MainArchiveHeader { + HeaderFlags flags; // should be vint but not compatible with bitfield + if (flags.extraAreaIsPresent) { + vint extraAreaSize; + } + ArchiveFlags archiveFlags; // should be vint but not compatible with bitfield + if (archiveFlags.volumeNumberFieldIsPresent) { + vint volumeNumber; + } + if (flags.extraAreaIsPresent) { + u64 extraEnd = $ + extraAreaSize; + MainHeaderExtraRecord extraArea[while($ < extraEnd)]; + } +}; + +bitfield EncryptionHeaderFlags { + bool passwordCheckDataIsPresent : 1; +}; + +struct EncryptionHeader { + HeaderFlags flags; // should be vint but not compatible with bitfield + vint encryptionVersion; // only 0 (AES-256) is supported + EncryptionHeaderFlags encryptionFlags; + u8 kdfCount; + u8 salt[16]; + if (encryptionFlags.passwordCheckDataIsPresent) { + u8 checkValue[12]; + } + + u8 iv[16]; + // if this header is present, all following headers are encrypted => gobble up all data + u8 encryptedHeaders[while(!std::mem::eof())]; + // With a bit of guess work, I figured out the exact crypto: + // key = PBKDF2(password, salt, keysize=256, iterations=2^kdfCount, hash=sha256) + // plainHeaders = AES256Decrypt(key, iv, mode=CBC) +}; + +bitfield FileAndServiceHeaderFlags { + bool isDirectory : 1; + bool timeFieldIsPresent : 1; + bool crc32FieldIsPresent : 1; + bool unpackedSizeIsUnknown : 1; +}; + +enum HostOS : vint { + Windows = 0, + Unix = 1 +}; + +bitfield FileEncryptionFlags { + bool passwordCheckDataIsPresent : 1; + bool useTweakedChecksums : 1; +}; + +struct FileEncryptionRecord { + vint encryptionVersion; // only 0 (AES-256) is supported + FileEncryptionFlags flags; // should be vint but not compatible with bitfield + u8 kdfCount; + u8 salt[16]; + u8 iv[16]; + u8 checkValue[12]; +}; + +struct FileHashRecord { + vint hashType; // only 0 (BLAKE2sp) hash is documented + u8 hashData[32]; // depends on hashType, but only one type is supported +}; + +bitfield FileTimeFlags { + bool timeIsStoredInUnixTime : 1; + bool mtimeIsPresent : 1; + bool ctimeIsPresent : 1; + bool atimeIsPresent : 1; + bool unixTimeIsNanosecondPrecision : 1; +}; + +struct FileTimeRecord { + FileTimeFlags flags; // should be vint but not compatible with bitfield + if (flags.mtimeIsPresent) { + match (flags.timeIsStoredInUnixTime, flags.unixTimeIsNanosecondPrecision) { + (true, true): nanotime_t mtime; + (true, false): time_t mtime; + (false, _): FILETIME mtime; + } + } + if (flags.ctimeIsPresent) { + match (flags.timeIsStoredInUnixTime, flags.unixTimeIsNanosecondPrecision) { + (true, true): nanotime_t ctime; + (true, false): time_t ctime; + (false, _): FILETIME ctime; + } + } + if (flags.atimeIsPresent) { + match (flags.timeIsStoredInUnixTime, flags.unixTimeIsNanosecondPrecision) { + (true, true): nanotime_t atime; + (true, false): time_t atime; + (false, _): FILETIME atime; + } + } +}; + +struct FileVersionRecord { + vint flags; + vint versionNumber; +}; + +enum RedirectionType : vint { + unixSymlink = 1, + windowsSymlink, + windowsJunktion, + hardLink, + fileCopy +}; + +struct RedirectionRecord { + RedirectionType type; + vint flags; + vint targetLength; + char target[targetLength]; +}; + +bitfield UnixOwnerFlags { + bool userNameStringIsPresent : 1; + bool groupNameStringIsPresent : 1; + bool numericUserIDIsPresent : 1; + bool numericGroupIDIsPresent : 1; +}; + +struct UnixOwnerRecord { + UnixOwnerFlags flags; // should be vint but not compatible with bitfield + if (flags.userNameStringIsPresent) { + vint userNameLength; + char userName[usernameLength]; + } + if (flags.groupNameStringIsPresent) { + vint groupNameLength; + char groupName[groupNameLength]; + } + if (flags.numericUserIDIsPresent) { + vint uid; + } + if (flags.numericGroupIDIsPresent) { + vint gid; + } +}; + +enum FSHeaderExtraRecordType : vint { + FileEncryption = 1, + FileHash, + FileTime, + FileVersion, + Redirection, + UnixOwner, + ServiceData +}; + +struct FSHeaderExtraRecord { + vint size; + FSHeaderExtraRecordType type; + match(type) { + (FSHeaderExtraRecordType::FileEncryption): FileEncryptionRecord record [[inline]]; + (FSHeaderExtraRecordType::FileHash): FileHashRecord record [[inline]]; + (FSHeaderExtraRecordType::FileTime): FileTimeRecord record [[inline]]; + (FSHeaderExtraRecordType::FileVersion): FileVersionRecord record [[inline]]; + (FSHeaderExtraRecordType::Redirection): RedirectionRecord record [[inline]]; + (FSHeaderExtraRecordType::UnixOwner): UnixOwnerRecord record [[inline]]; + (FSHeaderExtraRecordType::ServiceData): u8 data[size - sizeof(type)]; // couldn't find any info on these records + } +}; + +bitfield _CompressionInformation { + version : 6; + bool isSolid : 1; + method : 3; + minDictionarySizeExponent : 5; // min dict size is `128 KB * 2^minDictionarySizeExponent` + additionalDictionarySize : 5; + bool compressionAlgorithmIsZeroButSizeIsV1 : 1; +}; + +struct CompressionInformation { + vint value; // TODO: I'd like to somehow cast the resulting value to _CompressionInformation +}; + +struct FileOrServiceHeader { + HeaderFlags flags; // should be vint but not compatible with bitfield + if (flags.extraAreaIsPresent) { + vint extraAreaSize; + } + if (flags.dataAreaIsPresent) { + vint dataAreaSize; + } + FileAndServiceHeaderFlags fileOrServiceFlags; // should be vint but not compatible with bitfield + vint unpackedSize; + vint fileAttributes; + if (fileOrServiceFlags.timeFieldIsPresent) { + u32 fileModificationUnixTime; + } + if (fileOrServiceFlags.crc32FieldIsPresent) { + u32 unpackedCrc32; + } + CompressionInformation compressionInformation; + try { + HostOS hostOS; + vint nameLength; + char name[nameLength]; + if (flags.extraAreaIsPresent) { + u64 extraEnd = $ + extraAreaSize; + FSHeaderExtraRecord extraArea[while($ < extraEnd)]; + } + if (flags.dataAreaIsPresent) { + u8 data[dataAreaSize] [[sealed]]; + } + } catch {} +}; + +bitfield EOAHeaderFlags { + bool archiveIsVolumeAndNotLastInVolumeSet : 1; +}; + +struct EndOfArchiveHeader { + HeaderFlags flags; + EOAHeaderFlags eoaFlags; +}; + +struct Header { + u32 crc32; + vint headerSize; + HeaderType type; + match (type) { + (HeaderType::EncryptionHeader): EncryptionHeader [[inline]]; + (HeaderType::MainHeader): MainArchiveHeader [[inline]]; + (HeaderType::FileHeader): FileOrServiceHeader [[inline]]; + (HeaderType::ServiceHeader): FileOrServiceHeader [[inline]]; + (HeaderType::EndOfArchive): EndOfArchiveHeader [[inline]]; + } + if (type == HeaderType::EndOfArchive) { + break; + } +}; + +struct RARv5 { + type::Magic magic; + Header headers[while(!std::mem::eof())]; // TODO: would be better to read until EOAHeader +}; + +RARv5 archive @ std::mem::find_string(0, RARv5_MAGIC); + diff --git a/tests/patterns/test_data/rar.hexpat.rar b/tests/patterns/test_data/rar.hexpat.rar new file mode 100644 index 0000000000000000000000000000000000000000..c419cf02294a4e2bbe0df98e0e59e8b5d67352d4 GIT binary patch literal 452 zcmWGaEK-zWXJjy*wDl<$BP$yNDYpR^thtGJ^ZJ_#LUgg z!n&lHX;Q*d1)wP$AdPw@6(wBE!e4WB(jUFpB$$z!lh5_oadw|J6Ept=29`z+pn9DM zv4#d_Moy4=BdCUL8+FpxL@=>1voJ6on9j?je@T1K_Wzr-_lvGRpxYN36YbabTFY$3+w7I5M_-lfP;kjR=B_$lKufnC7%Uf6 z+|!jFn;ENJW?X*l)8cOlFZmkxL<<}`uxo~%W%a3xvd)X92!6i#OSU{LkeQW*0RTy> BqI&=U literal 0 HcmV?d00001