Skip to content

Commit

Permalink
Add rar file pattern as requested in WerWolv#258
Browse files Browse the repository at this point in the history
  • Loading branch information
targodan committed Nov 27, 2024
1 parent b9f5f16 commit d7208c5
Show file tree
Hide file tree
Showing 3 changed files with 365 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,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 |
Expand Down
364 changes: 364 additions & 0 deletions patterns/rar.hexpat
Original file line number Diff line number Diff line change
@@ -0,0 +1,364 @@
#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
};

fn to_CompressionInformation(vint v) {
std::mem::Reinterpreter<u64, _CompressionInformation> conv;
conv.from = v;
return conv.to;
};

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]];
}
};

struct RARv5 {
type::Magic<RARv5_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);

Binary file added tests/patterns/test_data/rar.hexpat.rar
Binary file not shown.

0 comments on commit d7208c5

Please sign in to comment.