diff --git a/README.md b/README.md index a198900..a43ec41 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi |------|------|------|-------------| | 3DS | | [`patterns/3ds.hexpat`](patterns/3ds.hexpat) | Autodesk 3DS Max Model file | | 7Z | | [`patterns/7z.hexpat`](patterns/7z.hexpat) | 7z File Format | +| ADTFDAT | | [`patterns/adtfdat.hexpat`](patterns/adtfdat.hexpat) | [ADTFDAT files](https://digitalwerk.gitlab.io/solutions/adtf_content/adtf_base/adtf_file_library) | | 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 | diff --git a/patterns/adtfdat.hexpat b/patterns/adtfdat.hexpat new file mode 100644 index 0000000..e83c4cd --- /dev/null +++ b/patterns/adtfdat.hexpat @@ -0,0 +1,176 @@ +#pragma description ADTFDAT File + +#pragma magic [ 49 46 48 44 ] @ 0x00 +#pragma endian little + +import std.io; +import std.mem; +import type.time; + +enum FileVersion : u32 { + with_history_end_offset = 0x00000301, + v500 = 0x00000500 +}; + +bitfield ChunkFlags { + key : 1; + info : 1; + marker : 1; + type : 1; + trigger : 1; + reserved: 11; +}; + +struct Extension +{ + char identifier[384]; + u16 stream_id; + u8 reserved1[2]; + u32 user_id; + u32 type_id; + u32 version_id; + u64 data_pos; + u64 data_size; + u8 reserved[96]; + u8 payload[data_size] @ data_pos; +}; + +struct ChunkHeader { + u64 time_stamp [[format("format_timestamp_with_offset")]];; + u32 ref_master_table_index; + u32 offset_to_last; + u32 size; + u16 stream_id; + ChunkFlags flags; + u64 stream_index; +}; + +struct SampleInfo { + u8 memory_layout_version; + u32 size; + u8 payload[size]; +}; + +struct Sample { + s64 timestamp [[format("format_timestamp")]];; + s32 flags; + u64 buffer_size; + u8 payload[buffer_size]; + if (flags & 0x100) { + SampleInfo sample_info; + } + if (flags & 0x200) { + u32 substream_id; + } else { + u32 substream_id = 0 [[export]]; + } +}; + +struct Chunk { + auto start_address = $; + + ChunkHeader header; + + if (header.size < 32 || start_address + header.size > std::mem::size()) { + std::warning(std::format("Invalid header size {} in chunk {} at offset 0x{:X}, last valid chunk at 0x{:X}.", header.size, chunk_index, start_address, last_valid_chunk_offset)); + break; + } + + last_valid_chunk_offset = start_address; + + u8 payload[header.size - 32]; + + if (!skip_valid_chunks) { + if (!header.flags.info && !header.flags.marker && !header.flags.type && !header.flags.trigger) { + try { + Sample sample @ addressof(payload); + } catch { + std::warning(std::format("Invalid sample payload at chunk {} at 0x{:X}", chunk_index, start_address)); + } + } + } + + chunk_index += 1; + + // Padding with 0xEE to the next chunk header + std::mem::AlignTo<16>; + + if (skip_valid_chunks) { + continue; + } +}; + +struct Header { + u32 file_id; + u32 version_id; + u32 flags; + u32 extension_count; + u64 extension_offset; + u64 data_offset; + u64 data_size; + u64 chunk_count; + u64 max_chunk_size; + u64 duration; + type::time64_t filetime; + u8 header_byte_order; + u64 time_offset [[format("format_timestamp")]]; + u8 patch_number; + u64 first_chunk_offset; + u64 continuous_offset; + u64 ring_buffer_end_offset; + u8 reserved[30]; + char description[1912]; +}; + +struct IndexedFile { + Header header; + Extension extensions[header.extension_count] @ header.extension_offset; + + if (header.version_id >= FileVersion::with_history_end_offset && + header.continuous_offset != header.data_offset) { + try { + Chunk ring_front[while($ < header.continuous_offset)] @ header.first_chunk_offset; + Chunk ring_back[while($ < header.ring_buffer_end_offset)] @ header.data_offset; + Chunk continueous[header.chunk_count - chunk_index] @ header.continuous_offset; + } catch { + std::warning("Too many chunks. Performing check only."); + skip_valid_chunks = true; + chunk_index = 0; + Chunk ring_front[while($ < header.continuous_offset)] @ header.first_chunk_offset; + Chunk ring_back[while($ < header.ring_buffer_end_offset)] @ header.data_offset; + Chunk continueous[while(chunk_index < header.chunk_count)] @ header.continuous_offset; + } + } else { + try { + Chunk chunks[header.chunk_count] @ header.data_offset; + } catch { + std::warning("Too many chunks. Performing check only."); + skip_valid_chunks = true; + chunk_index = 0; + Chunk chunks[while(chunk_index < header.chunk_count)] @ header.data_offset; + } + } +}; + +fn format_timestamp(u64 data) { + double seconds = 0; + if (file.header.version_id < FileVersion::v500) { + // microseconds + seconds = data / double(1000000); + } else { + // nanoseconds + seconds = data / double(1000000000); + } + + std::time::Time time64 = std::time::to_utc(seconds); + return std::time::format(time64); +}; + +fn format_timestamp_with_offset(u64 data) { + return format_timestamp(data + file.header.time_offset); +}; + +bool skip_valid_chunks = false; +u64 last_valid_chunk_offset = 0; +u64 chunk_index = 0; +IndexedFile file @ 0x00; diff --git a/tests/patterns/test_data/adtfdat.hexpat.adtfdat b/tests/patterns/test_data/adtfdat.hexpat.adtfdat new file mode 100644 index 0000000..4e45fcd Binary files /dev/null and b/tests/patterns/test_data/adtfdat.hexpat.adtfdat differ