{
    "archive_path": "archive/1788386747.431867",
    "base_url": "werwolv.net/posts/file_format_reverse_engineering",
    "basename": "",
    "bookmarked_date": "2026-09-02 22:05",
    "canonical": {
        "archive_org_path": "https://web.archive.org/web/werwolv.net/posts/file_format_reverse_engineering",
        "dom_path": "output.html",
        "favicon_path": "favicon.ico",
        "git_path": "git/",
        "google_favicon_path": "https://www.google.com/s2/favicons?domain=werwolv.net",
        "headers_path": "headers.json",
        "htmltotext_path": "htmltotext.txt",
        "index_path": "index.html",
        "media_path": "media/",
        "mercury_path": "mercury/content.html",
        "pdf_path": "output.pdf",
        "readability_path": "readability/content.html",
        "screenshot_path": "screenshot.png",
        "singlefile_path": "singlefile.html",
        "warc_path": "warc/",
        "wget_path": "werwolv.net/posts/file_format_reverse_engineering/index.html"
    },
    "domain": "werwolv.net",
    "extension": "",
    "hash": "1ZCMVPEVJ7AFMB74KH7T",
    "history": {
        "archive_org": [],
        "dom": [],
        "favicon": [
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--max-time",
                    "120",
                    "--output",
                    "favicon.ico",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://www.google.com/s2/favicons?domain=werwolv.net"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-09-02T22:06:01.859924+00:00",
                "index_texts": null,
                "output": "favicon.ico",
                "pwd": "/data/archive/1788386747.431867",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-02T22:06:01.548431+00:00",
                "status": "succeeded"
            }
        ],
        "git": [],
        "headers": [
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--head",
                    "--max-time",
                    "120",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://werwolv.net/posts/file_format_reverse_engineering/"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-09-02T22:06:02.093685+00:00",
                "index_texts": null,
                "output": "headers.json",
                "pwd": "/data/archive/1788386747.431867",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-02T22:06:01.867319+00:00",
                "status": "succeeded"
            }
        ],
        "htmltotext": [],
        "media": [],
        "mercury": [
            {
                "cmd": [
                    "/app/node_modules/@postlight/parser/cli.js",
                    "https://werwolv.net/posts/file_format_reverse_engineering/"
                ],
                "cmd_version": "1.0.0",
                "end_ts": "2026-09-02T22:06:17.470177+00:00",
                "index_texts": null,
                "output": "mercury",
                "pwd": "/data/archive/1788386747.431867",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-02T22:06:11.640034+00:00",
                "status": "succeeded"
            }
        ],
        "pdf": [],
        "readability": [
            {
                "cmd": [
                    "/app/node_modules/readability-extractor/readability-extractor",
                    "/tmp/tmp1usv7cws",
                    "https://werwolv.net/posts/file_format_reverse_engineering/"
                ],
                "cmd_version": "0.0.11",
                "end_ts": "2026-09-02T22:06:11.630255+00:00",
                "index_texts": [
                    "Introduction\nOver the years I\u2019ve been asked the same question countless times:\nPerson on Discord asking for help reverse engineering a file format\nI usually couldn\u2019t really give them a good answer except, \u201cLook at the decompiled code of whatever program reads/writes these files and work backwards from there.\u201d This post is meant to change that. We\u2019ll go from a completely custom binary save file for the game FEZ to a full definition written in the Pattern Language, which is part of ImHex, the hex editor I\u2019ve been developing for the past few years. It is free, open source and available on any operating system (or even through the browser if you prefer that: ImHex Web).\nImHex VersionAt the time of writing, some features used here are not in a release yet but only available in the Nightly build (that can also be downloaded above from the same link).\nIf you\u2019re on ImHex v1.38.1 or below and experiencing issues, consider upgrading to the Nightly build\nGetting Started\nSpoiler WarningFEZ was released all the way back in 2012. Still, if you haven\u2019t played it yet and want to get the full experience, I highly recommend playing it before you continue reading. Some of the code shown here will contain heavy spoilers for secrets and endgame content that might ruin your experience. You have been warned.\nThe first thing we need is the save file. I downloaded the game from Steam (the latest full release currently available, released 2. December 2016), started it and played for a little bit until it saved. Then I went looking through my filesystem and found the save file under /home/werwolv/.local/share/FEZ/SaveSlot2. On Windows, it will be elsewhere.\nOpening the file in ImHex shows this:\nHex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F00000000  3E 74 E1 41 6B BA DA 01  06 00 00 00 00 00 00 00  >t.Ak...........00000010  3A AC 78 49 C9 B4 CF 01  01 00 01 00 00 01 12 00  :.xI............00000020  00 00 01 11 44 4F 54 5F  4C 4F 43 4B 45 44 5F 44  ....DOT_LOCKED_D00000030  4F 4F 52 5F 41 00 01 10  44 4F 54 5F 4E 55 54 5F  OOR_A...DOT_NUT_00000040  4E 5F 42 4F 4C 54 5F 41  00 01 0B 44 4F 54 5F 50  N_BOLT_A...DOT_P00000050  49 56 4F 54 5F 41 01 01  11 44 4F 54 5F 54 49 4D  IVOT_A...DOT_TIM00000060  45 5F 53 57 49 54 43 48  5F 41 00 01 0F 44 4F 54  E_SWITCH_A...DOT00000070  5F 54 4F 4D 42 53 54 4F  4E 45 5F 41 01 01 0C 44  _TOMBSTONE_A...D00000080  4F 54 5F 54 52 45 41 53  55 52 45 00 01 0B 44 4F  OT_TREASURE...DO00000090  54 5F 56 41 4C 56 45 5F  41 01 01 13 44 4F 54 5F  T_VALVE_A...DOT_\nThis already reveals a few things. The file seems to be uncompressed and unencrypted, as seen by the plain-text strings and other patterns in the file that can be easily spotted by just looking at the bytes and characters. The data also doesn\u2019t have a file magic (some readable text at the start of the file to make it more easily identifiable), and it doesn\u2019t look like anything standard, as ImHex can\u2019t identify its type directly either.\nMagic file information from ImHex\nWithout any more information, we\u2019re basically stuck here. The data can mean anything, and only the program generating and parsing it can make sense of it.\nDecompiling the Game\nFinding the right files\nClicking on the gear icon on the Steam page and selecting Manage -> Browse local files brings us to the game\u2019s binary location. What immediately sticks out are files like System.Core.dll or mscorlib.dll. The game is written in the C# programming language, which is generally really easy to reverse engineer. Tools like JetBrains Rider can decompile the binaries back to what looks like the original source code.\nFor that, we can open the game\u2019s folder as a project and then simply Right Click -> View in Assembly Explorer for all the .dll files that look interesting. To me, particularly interesting were FEZ.exe, FezEngine.dll, Common.dll, ContentSerialization.dll and EasyStorage.dll. The rest are system libraries or external dependencies that look unrelated to what we\u2019re trying to do here.\nFinding the right functions\nJust clicking through the namespaces quickly reveals an interesting-looking file: EasyStorage -> PCSaveDevice.\nIn the constructor of that class, we can also immediately see string str = \"SaveSlot\" + (object) index;, which looks like it\u2019s building the name of our file, SaveSlot2, so we found the right place for sure.\nScrolling down a bit, we can find a function called Save that creates a byte buffer and starts filling it in using a BinaryWriter stream before saving it to our save file location. Bingo!\npublic virtual bool Save(string fileName, SaveAction saveAction){  // ...  byte[] buffer = new byte[40960 /*0xA000*/];  using (MemoryStream output = new MemoryStream(buffer))  {    using (BinaryWriter writer = new BinaryWriter((Stream) output))    {      writer.Write(DateTime.Now.ToFileTime());      saveAction(writer);      if (output.Length < 40960L /*0xA000*/)      {        long length = 40960L /*0xA000*/ - output.Length;        writer.Write(new byte[length]);      }      else if (output.Length > 40960L /*0xA000*/)        throw new InvalidOperationException(          \"Save file greater than the imposed limit!\"        );    }  }  // ...}\nWriting the ImHex Pattern\nHumble Beginnings\nNow that we\u2019ve found where the save file is being generated, we can start writing a Pattern file in ImHex to decode the data. Open the Pattern Editor tab to reveal a text editor where we can write our source code.\nWe can start simply by creating a struct FezSaveFile and placing it at the start of the file using the @ placement operator.\nstruct FezSaveFile {  // Struct Definition};FezSaveFile saveFile @ 0x00;\nThis instantiates the FezSaveFile pattern object at address 0x00 of our file.\nNext, in the save file generation code, we see writer.Write(DateTime.Now.ToFileTime());, which writes the current timestamp as a Windows File Time to the output. As seen in the Remarks section of the docs, this is simply a little endian, 64-bit value (a long in C#) that represents the number of 100 ns intervals that have passed since the year of our lord 1601 A.D. We could, of course, properly decode this value and everything but to get started we can simply place a s64 in its place in the Pattern to read it.\nAlternatively, we can also write type aliases with the using keyword to make the code in our pattern resemble the types used in the real code even more closely. These simply define a new type that has the exact properties of the type on the right hand side but with a potentially more descriptive name.\nusing int  = s32;using long = s64;struct FezSaveFile {  long fileTime;};FezSaveFile saveFile @ 0x00;\nAfter clicking the  button at the bottom of the Pattern Editor (or pressing the F5 key), the region of that value is now highlighted in the Hex Editor View, and it also appears in the pattern tree in the Pattern Data View.\nHighlighted Bytes in the Hex Editor and decoded value in the Pattern Data View\nFor this particular case though, we\u2019re in luck and the standard library already implements a type for decoding a Windows FILETIME value. To get access to it, we can import the type.time library which defines that type and then use it like any other type in our code:\nimport type.time;struct FezSaveFile {  type::FILETIME fileTime;};FezSaveFile saveFile @ 0x00;\nThis simple change now turns that unreadable number from before into a nice, human readable representation of the actual time value:\nDecoding the FILETIME value using the `type::FILETIME` type from the standard library\nAnd that\u2019s it for the start, congrats! You wrote your first pattern!\n[[fixed_size]] attributeOne thing we can also see in the code is that the Save() function ensures that the save file is always 0xA000 bytes long. If it\u2019s shorter, it will pad it out with zeros, and if it is longer, an InvalidOperationException will be thrown.This maps incredibly well to the [[fixed_size(0xA000)]] attribute that can be attached to FezSaveFile to ensure that. This is entirely optional but helps document the official behavior.\nThe Actual Save Data\nBack to the C# code, the next thing that\u2019s done is to call out to the saveAction callback, which is implemented elsewhere. Thankfully, Rider helps here, as you can just Ctrl-click on the name of the Save function to find definitions. There we see a few places it\u2019s called from, but the interesting one is in GameStateManager.cs SaveInternal().\nFind usages in JetBrains Rider\nprivate void SaveInternal(bool ngpBackup){  // ...  this.ActiveSaveDevice.Save(    \"SaveSlot\" + (object) this.SaveSlot,    new SaveAction(this.DoSave)  );  // ...}\nThere we can see that the actual dumping of the save data is delegated to the DoSave() function, which calls SaveFileOperations.Write(). This is the juicy stuff now. Here we can see aaaaaaalll the different fields that are being written out to the binary.\npublic static void Write(CrcWriter w, SaveData sd){  w.Write(6L);  w.Write(sd.CreationTime);  w.Write(sd.Finished32);  w.Write(sd.Finished64);  w.Write(sd.HasFPView);  w.Write(sd.HasStereo3D);  w.Write(sd.CanNewGamePlus);  w.Write(sd.IsNewGamePlus);  // ...}\nLooking at the types of those values allows them to be easily converted to the ImHex Pattern:\nstruct FezSaveFile {  // From PCSaveDevice.cs  type::FILETIME fileTime;  // From SaveFileOperations.cs  long version; // Checked in the `Read()` function to be 6  long creationTime;  bool finished32;  bool finished64;  bool hasFpView;  bool hasStereo3d;  bool canNewGamePlus;  bool isNewGamePlus;};\nThe first field seems to be a save file version as can be seen in the Read() function below which reads that field, makes sure it is also 6 and throws an exception if it\u2019s not.\nWe can simply parse that field but if we want to be extra fancy and make sure that we only load files that are actually compatible with our pattern, we can easily assert on this field. In the Pattern Language, we can have conditions and function calls intertwined with our type definitions which makes things like this possible:\nimport std.sys;struct FezSaveFile {  // From PCSaveDevice.cs  type::FILETIME fileTime;  // From SaveFileOperations.cs  long version; // Checked in the `Read()` function to be 6  std::assert(version == 6, \"Unsupported Save File Version. Only Version 6 is supported\");\nObjects and Strings\nThe next part is interesting. Here, a list of String -> Bool key-value pairs is being serialized. First, the number of pairs is stored, followed by that number of serialized pairs.\nw.Write(sd.OneTimeTutorials.Count);foreach (KeyValuePair<string, bool> oneTimeTutorial in sd.OneTimeTutorials){  w.WriteObject(oneTimeTutorial.Key);  w.Write(oneTimeTutorial.Value);}\nw.WriteObject is a bit more involved and deserves a closer look.\npublic static void WriteObject(this CrcWriter writer, string s){  writer.Write(s != null);  if (s == null)    return;  writer.Write(s);}\nObjects seem to be defined as something that may or may not exist. First, a bool is written to the file that represents whether or not the object is null. If it is null, that\u2019s the end of it, and we don\u2019t write down anything more. If it\u2019s not null, though, we serialize the value. In the Pattern Language, this looks like this:\nstruct Object<T> {  bool isValid;  if (isValid)    T value;};\nThis code defines a new template struct called Object. It places a bool in the output file, then checks if that bool is true. Only if it is does it place a value of the template parameter\u2019s type.\nNext we need to see how string types are serialized. There\u2019s another function of the BinaryWriter that does this:\npublic virtual void Write(string value){  if (this.disposed)    throw new ObjectDisposedException(nameof (BinaryWriter), \"Cannot write to a closed BinaryWriter\");  this.Write7BitEncodedInt(this.m_encoding.GetByteCount(value));  if (this.stringBuffer == null)  {    this.stringBuffer = new byte[512 /*0x0200*/];    this.maxCharsPerRound = 512 /*0x0200*/ / this.m_encoding.GetMaxByteCount(1);  }  int charIndex = 0;  int charCount;  for (int length = value.Length; length > 0; length -= charCount)  {    charCount = length <= this.maxCharsPerRound ? length : this.maxCharsPerRound;    this.OutStream.Write(this.stringBuffer, 0, this.m_encoding.GetBytes(value, charIndex, charCount, this.stringBuffer, 0));    charIndex += charCount;  }}\nThis seems to first be writing out the length of the string in bytes in some 7BitEncodedInt format followed by the actual string data.\nprotected void Write7BitEncodedInt(int value){  do  {    int num1 = value >> 7 & 33554431 /*0x01FFFFFF*/;    byte num2 = (byte) (value & (int) sbyte.MaxValue);    if (num1 != 0)      num2 |= (byte) 128 /*0x80*/;    this.Write(num2);    value = num1;  }  while (value != 0);}\nWrite7BitEncodedInt looks a bit daunting, but after playing through it with some values, all this does is use the MSB of each byte as a flag to tell the parser if there\u2019s another byte still coming. The other 7 bits are the actual encoded value.\nIn the Pattern Language this can be implemented like this:\nstruct SevenBitEncodedIntByte {  // Read a byte  u8 byte;  // If that byte doesn't have bit 7 set,  // this is the last one and we can stop here.  if ((byte & 0x80) == 0x00)    break;};struct SevenBitEncodedInt {  // Keep decoding bytes until the `break` above is run  SevenBitEncodedIntByte bytes[while(true)];};\nAdditionally, to make this type a bit easier to work with, we can use the [[format]] attribute to display the decoded integer value in the Pattern Data View and the [[transform]] attribute so the rest of our code can simply read from a variable of this type and get back the decoded integer value instead:\nstruct SevenBitEncodedInt {    SevenBitEncodedIntByte bytes[while(true)];} [[format(\"transformSevenBitEncodedInt\"), transform(\"transformSevenBitEncodedInt\")]];fn transformSevenBitEncodedInt(ref auto encodedInt) {  u64 result = 0;  // Loop over all the bytes we placed before  for (u32 i = 0, i < std::core::member_count(encodedInt.bytes), i += 1) {    // Each byte contains the next more-significant group of 7 bits    result |= (encodedInt.bytes[i].byte & 0x7F) << (i * 7);  }  return result;};\nNow that all of this is done, we can finally define our String type. Again with a nice [[format]] function so we can see the string directly in the UI.\nstruct String {  // Read the string's size  SevenBitEncodedInt size;  char string[size];} [[format(\"formatString\")]];fn formatString(ref auto string) {  return string.string;};\nThe final Object<String> type being parsed by ImHex\nLists\nw.Write(sd.OneTimeTutorials.Count);foreach (KeyValuePair<string, bool> oneTimeTutorial in sd.OneTimeTutorials){  w.WriteObject(oneTimeTutorial.Key);  w.Write(oneTimeTutorial.Value);}\nBack to the code from before, we can define a few more types now to finally parse this construct. This time using a s32 (or our int type alias) because Count is a int in C#.\nstruct List<T> {  // Read the number of items  int count;  // Place an array of `count` items down  T items[count];};struct KeyValuePair<Key, Value> {  Key key;  Value value;};\nAll of this together now lets us finally decode the list. Take a step back for a bit and see how all of this came together and how it maps to the C# code.\nstruct FezSaveFile {  // ...  List<               // A size-prefixed list containing...    KeyValuePair<     //   pairs of...      Object<String>, //     an optional, length prefixed string...      bool            //     and a bool    >  > oneTimeTutorials;};\nThe final List< type being parsed by ImHex\nEnums\nFollowing the same pattern, we can keep going down the Write function and decoding the data in ImHex.\nThe next interesting bit is this code:\nforeach (ActorType artifact in sd.Artifacts)  w.Write((int) artifact);\nThis writes down not integers directly, but an enumeration instead. We could just treat it as an int like the serializer code does, but it would be nicer to keep the names available in ImHex as well.\nThe definition of the enum in C# can be copy-pasted over almost 1:1:\npublic enum ActorType{  None,  Ladder,  Bouncer,  Sign,  GoldenCube,  // ...}\nenum ActorType : int{  None,  Ladder,  Bouncer,  Sign,  GoldenCube,  // ...};\nThis type can then simply be used in place of a int and gives us a nicer display:\nActorType enum as seen in ImHex\nMore Subtypes\nThe same pattern as above keeps on going for a while longer until we reach the end where we have some nested serialization:\nforeach (KeyValuePair<string, LevelSaveData> keyValuePair in sd.World){  w.WriteObject(keyValuePair.Key);  SaveFileOperations.Write(w, keyValuePair.Value);}\nThis maps really nicely to a new struct that we can call LevelSaveData and just keep going in there as before:\nstruct LevelSaveData {  // ...};struct FezSaveFile {  // ...  List<KeyValuePair<Object<String>, LevelSaveData>> world;};\nYou should now have everything that\u2019s needed to decode the rest of the format yourself. Give it a try!\nThe Fruits of our Labour\nAt this point, you should be able to look at the Hex Editor View and see every single byte (except the large padding at the end) highlighted with some color. You can now browse through the Pattern Data View and inspect what all these different values mean and even modify them by double-clicking the value!\nFully decoded Save File in ImHex\nIf you\u2019re interested in how I did it, you can take a look at My Pattern.\nWrapping things up\nAfter reading this post you should have a basic understanding of how to reverse engineer a binary file format. Of course, not all programs will be as easy to decompile and analyze as this one but the general workflow remains the same:\nCheck if the file is in a known formatThis can be done in various ways, ImHex magic detection and tools like binwalk can help a lot. If it is an existing format, there might be tools available already to parse that format. Otherwise you may be able to look at the specificationFind the piece of code that parses or generates that fileThis usually means looking at the program\u2019s Source Code if available or to decompile the program first using a tool that fits the language used. Rider works great for .NET, Ghidra, IDA or Binary Ninja for native-compiled programs, Recaf for JVM languages. What helps me is to look for library calls for doing File I/O or for strings mentioning (parts of) the generated or loaded file name.Analyze the code and find the building blocks used in the fileMost file formats want the same thing: To store integers, booleans, strings and other data structures. Identifying them is the first step to understanding the file step by stepWrite a Pattern File to document your findings and verify their validityPatterns are great not only for decoding the file once you know how it works but also for documenting and verifying your findings along the way.\nIf you have any questions about the process, ImHex or the Pattern Language itself, feel free to reach out on the ImHex Discord Server, via Discord DMs @werwolv or by email at [email\u00a0protected]."
                ],
                "output": "readability",
                "pwd": "/data/archive/1788386747.431867",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-02T22:06:07.987298+00:00",
                "status": "succeeded"
            }
        ],
        "screenshot": [],
        "singlefile": [],
        "title": [
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--max-time",
                    "120",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://werwolv.net/posts/file_format_reverse_engineering/"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-09-02T22:06:07.979885+00:00",
                "index_texts": null,
                "output": "Reverse Engineering Unknown File Formats with ImHex | WerWolv",
                "pwd": "/data/archive/1788386747.431867",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-02T22:06:07.899158+00:00",
                "status": "succeeded"
            }
        ],
        "wget": [
            {
                "cmd": [
                    "wget",
                    "--no-verbose",
                    "--adjust-extension",
                    "--convert-links",
                    "--force-directories",
                    "--backup-converted",
                    "--span-hosts",
                    "--no-parent",
                    "-e",
                    "robots=off",
                    "--timeout=120",
                    "--restrict-file-names=windows",
                    "--warc-file=/data/archive/1788386747.431867/warc/1788386762",
                    "--page-requisites",
                    "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) wget/GNU Wget 1.21.3",
                    "--compression=auto",
                    "https://werwolv.net/posts/file_format_reverse_engineering/"
                ],
                "cmd_version": "GNU Wget 1.21.3",
                "end_ts": "2026-09-02T22:06:07.890159+00:00",
                "index_texts": null,
                "output": "werwolv.net/posts/file_format_reverse_engineering/index.html",
                "pwd": "/data/archive/1788386747.431867",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-02T22:06:02.102901+00:00",
                "status": "succeeded"
            }
        ]
    },
    "icons": null,
    "is_archived": true,
    "is_static": false,
    "latest": {
        "archive_org": null,
        "dom": null,
        "favicon": "favicon.ico",
        "git": null,
        "media": null,
        "pdf": null,
        "screenshot": null,
        "singlefile": null,
        "title": "Reverse Engineering Unknown File Formats with ImHex | WerWolv",
        "warc": null,
        "wget": "werwolv.net/posts/file_format_reverse_engineering/index.html"
    },
    "link_dir": "/data/archive/1788386747.431867",
    "newest_archive_date": "2026-09-02T22:06:11.640034+00:00",
    "num_failures": 0,
    "num_outputs": 6,
    "oldest_archive_date": "2026-09-02T22:06:01.548431+00:00",
    "path": "/posts/file_format_reverse_engineering/",
    "schema": "Link",
    "scheme": "https",
    "snapshot_id": "d1cdd103-7cbc-4a00-9a82-227ad6625cb3",
    "sources": [
        "/data/sources/1788386747-import.txt"
    ],
    "tags": null,
    "tags_str": "",
    "timestamp": "1788386747.431867",
    "title": "Reverse Engineering Unknown File Formats with ImHex | WerWolv",
    "updated": "2026-09-02T22:06:01.547914+00:00",
    "updated_date": "2026-09-02 22:06",
    "url": "https://werwolv.net/posts/file_format_reverse_engineering/"
}