{"id":24616317,"url":"https://github.com/wunkolo/libsai","last_synced_at":"2025-04-06T05:16:14.493Z","repository":{"id":48185684,"uuid":"64907615","full_name":"Wunkolo/libsai","owner":"Wunkolo","description":"Library for interfacing with SYSTEMAX's Easy Paint Tool Sai.","archived":false,"fork":false,"pushed_at":"2025-03-22T22:02:01.000Z","size":362,"stargazers_count":111,"open_issues_count":4,"forks_count":15,"subscribers_count":16,"default_branch":"main","last_synced_at":"2025-03-30T03:11:04.462Z","etag":null,"topics":["painttool-sai"],"latest_commit_sha":null,"homepage":null,"language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Wunkolo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-08-04T06:13:58.000Z","updated_at":"2025-03-04T11:39:40.000Z","dependencies_parsed_at":"2022-08-30T08:41:55.620Z","dependency_job_id":"6c20d9c4-f42b-473c-ae96-a05f15a715c3","html_url":"https://github.com/Wunkolo/libsai","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wunkolo%2Flibsai","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wunkolo%2Flibsai/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wunkolo%2Flibsai/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wunkolo%2Flibsai/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Wunkolo","download_url":"https://codeload.github.com/Wunkolo/libsai/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247436286,"owners_count":20938533,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["painttool-sai"],"created_at":"2025-01-24T22:16:49.162Z","updated_at":"2025-04-06T05:16:14.235Z","avatar_url":"https://github.com/Wunkolo.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003c!-- TOC --\u003e\r\n\r\n- [Cracking PaintTool Sai documents](#cracking-painttool-sai-documents)\r\n- [Decryption](#decryption)\r\n\t- [Caching](#caching)\r\n- [File System](#file-system)\r\n- [Folder structure](#folder-structure)\r\n\t- [Serialization Streams](#serialization-streams)\r\n- [Files](#files)\r\n\t- [\".XXXXXXXXXXXXXXXX\"](#xxxxxxxxxxxxxxxx)\r\n\t- [\"canvas\"](#canvas)\r\n\t- [\"laytbl\" \"subtbl\"](#laytbl-subtbl)\r\n\t- [\"/layers\" \"/sublayers\"](#layers-sublayers)\r\n\t- [Raster Layers](#raster-layers)\r\n\t- [Linework Layers](#linework-layers)\r\n- [Decryption Keys](#decryption-keys)\r\n\t- [UserKey](#userkey)\r\n\t- [NotRemoveMe](#notremoveme)\r\n\t- [LocalState](#localstate)\r\n\t- [sai.ssd](#saissd)\r\n\r\n\u003c!-- /TOC --\u003e\r\n\r\n# Cracking PaintTool Sai documents\r\n\r\nThis document represents about a year and a half of off-and-on hobby-research on reverse engineering the digitizing raster/vector art program PaintTool Sai. This write-up in particular is focused on the technical specifications of the user-created `.sai` file format used to archive a user's artwork and the layers of abstraction implemented by SYSTEMAX for extracting this data outside of the context of the original software. This document is more directed at anyone that wants to implement their own library to read or interface with `.sai` files or just to get a comprehensive understanding of the decisions that SYSTEMAX has chosen to make for their file format. If you find anything in this document to be misleading, incomplete, or flat-out incorrect feel free to shoot me an email at `Wunkolo (at) gmail.com`. Previous work includes my now-abandoned run-time exploitation framework [SaiPal](https://github.com/Wunkolo/SaiPal/releases) and the more recent Windows explorer thumbnail extension [SaiThumbs](https://github.com/Wunkolo/SaiThumbs). This document assumes you have some knowledge of the C and C++ syntax as the data structures and algorithms here will be presented in the form of C and C++ structures and subroutines.\r\n\r\n\u003e PaintTool SAI Ver.1\r\n\u003e \r\n\u003e ![](https://www.systemax.jp/image/sai_logo.jpg)\r\n\u003e \r\n\u003e `PaintTool SAI is high quality and lightweight painting software, fully digitizer support, amazing anti-aliased paintings, provide easy and stable operation, this software make digital art more enjoyable and comfortable.`\r\n\u003e \r\n\u003e SYSTEMAX Software Development\r\n\u003e\r\n\u003e Details:\r\n\u003e - Fully digitizer support with pressure.\r\n\u003e - Amazing anti-aliased drawings.\r\n\u003e - Highly accurate composition with 16bit ARGB channels.\r\n\u003e - Simple but powerful user interface, easy to learn.\r\n\u003e - Fully support Intel MMX Technology.\r\n\u003e - Data protection function to avoid abnormal termination such as bugs.\r\n\u003e \r\n\u003e Copyright 1996-2016 SYSTEMAX Software Development\r\n\r\n# Decryption\r\n\r\nSai uses the file type `.sai` as its document format for storing both raster and vector layers as well as other canvas related meta-data. The `.sai` file among with other files such as thumbnails, the `sai.ssd` file and others is but an archive containing a _file-system-like_ structure once decrypted. Each layer, mask, and related meta data is stored in an individual pseudo-file which also has a layer of block-level encryption. The file itself is encrypted in [ECB](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_Codebook_.28ECB.29) blocks in which any randomly accessed block can be decrypted by also decrypting the appropriate `Table-Block` and accessing its 32-bit key found within. It's been found that some preliminary files such as thumbnails and the archive responsible for swatches/palettes use a different decryption key, block size, and `Table-Block` location. This document will mostly cover the method used for sai's user created `.sai` documents and very partially show related information for the other files.\r\n\r\nAn individual block in a `.sai` file is `4096` bytes of data. Every block index that is a multiple of `512`(`0, 512, 1024, etc`) is a `Table-Block` containing meta-data about the block itself and the `511` blocks after it. Every other block that is not a `Table-Block` is a `Data-Block`:\r\n\r\n```cpp\r\n// Gets the Table-Block index appropriate for the current block index\r\nstd::size_t NearestTable(std::size_t BlockIndex)\r\n{\r\n\treturn BlockIndex \u0026 ~(0x1FF);\r\n}\r\n// Demonstrating how to quickly determine if a block Index is a data-block or a table-block\r\nbool IsTableBlock(std::size_t BlockIndex)\r\n{\r\n\treturn (BlockIndex \u0026 0x1FF) ? false:true;\r\n}\r\nbool IsDataBlock(std::size_t BlockIndex)\r\n{\r\n\treturn (BlockIndex \u0026 0x1FF) ? true:false;\r\n}\r\n```\r\n\r\nAll blocks are encrypted and decrypted symmetrically using a simple exclusive-or-based encryption which refers to a static atlas of 256 32-bit integers which can be found at the end of this text. Different files related to Sai use different static keys. The keyvault used for the `.sai` file will be referred to as the `UserKey` since this is the only symmetrical key used to decrypt and encrypt files generated by the end-ser. `Table-Blocks` and `Data-Blocks` are encrypted differently using the same `UserKey`.\r\n\r\n`Table-Blocks` can be decrypted by random access using only their multiple-of-512 block index and the the `UserKey`. The first block of a `.sai` file (block index 0) will be a `Table-Block` storing related data for the `511` blocks after it. When decrypting a `Table-Block`, four of the 256 keys within `UserKey` are indexed by the four bytes of the 32-bit block-index and then summed together. This sum is exclusive-ored with the current 4-byte cipher-word and the block-index followed by a 16-bit left rotation of the result. When decrypting a `Data-Block`, an initial decryption vector is given which selects the appropriate integers from `UserKey` using the individual bytes of the 32-bit vector integer and xors with the vector integer itself, and subtracts this value from the cipher to get the plaintext before passing on the vector to the next round using the cipher integer. The input `Vector` is the checksum integer found in the `Table-Block`.\r\n\r\n```cpp\r\n// Ensure BlockIndex is a valid Table-Block index\r\nvoid DecryptTable(std::uint32_t BlockIndex, std::uint32_t* Data)\r\n{\r\n\t// see \"IsTableBlock\" above on making sure BlockIndex\r\n\t// is a table or use:\r\n\t// BlockNumber \u0026= (~0x1FF);\r\n\tfor( std::size_t i = 0; i \u003c 1024; i++ )\r\n\t{\r\n\t\tstd::uint32_t CurCipher = Data[i];\r\n\t\tstd::uint32_t X = BlockIndex ^ CurCipher ^ (\r\n\t\t\tUserKey[(BlockIndex \u003e\u003e 24) \u0026 0xFF]\r\n\t\t\t+ UserKey[(BlockIndex \u003e\u003e 16) \u0026 0xFF]\r\n\t\t\t+ UserKey[(BlockIndex \u003e\u003e 8) \u0026 0xFF]\r\n\t\t\t+ UserKey[BlockIndex \u0026 0xFF]);\r\n\r\n\t\tData[i] = static_cast\u003cstd::uint32_t\u003e((X \u003c\u003c 16) | (X \u003e\u003e 16));\r\n\r\n\t\tBlockIndex = CurCipher;\r\n\t};\r\n}\r\n\r\nvoid DecryptData(std::uint32_t Vector, std::uint32_t* Data)\r\n{\r\n\tfor( std::size_t i = 0; i \u003c 1024; i++ )\r\n\t{\r\n\t\tstd::uint32_t CurCipher = Data[i];\r\n\t\tData[i] =\r\n\t\t\tCurCipher\r\n\t\t\t- (Vector ^ (\r\n\t\t\t\tUserKey[Vector \u0026 0xFF]\r\n\t\t\t\t+ UserKey[(Vector \u003e\u003e 8) \u0026 0xFF]\r\n\t\t\t\t+ UserKey[(Vector \u003e\u003e 16) \u0026 0xFF]\r\n\t\t\t\t+ UserKey[(Vector \u003e\u003e 24) \u0026 0xFF]));\r\n\t\tVector = CurCipher;\r\n\t}\r\n}\r\n```\r\n\r\n`Table-Blocks` contain 512 8-byte structures containing a a 32-bit checksum and a 32-bit integer used to store an index to the next block(similar to a singly linked list). Each index of table-entries corresponds to the appropriate block index after the table index. The first checksum entry found within the `Table-Block` is a checksum of the table itself, excluding the first 32-bit integer. Setting the first checksum to 0 and calculating the checksum of the entire table produces the same results as if the first entry was skipped. A table entry with a checksum of `0` is considered to be an unallocated/unused block.\r\n\r\n```cpp\r\nstruct TableEntry\r\n{\r\n\tstd::uint32_t Checksum;\r\n\tstd::uint32_t NextBlock;\r\n} TableEntries[512];\r\n```\r\n\r\n```\r\n                                                   ~         ~\r\n                             Table-Block           |         |\r\n                       +----------+----------+\u003c----+---------+\r\n                     0 |0xChecksum|0xPrelimin|     |XXXX|XXXX| Block 512\r\nChecksum used to+--\u003e 1 |0xChecksum|0xPrelimin|     |XXXX|XXXX| 0x200200\r\ndecrypt block 513    2 |0xChecksum|0xPrelimin|     |XXXX|XXXX|\r\n                     3 |0xChecksum|0xPrelimin|     +---------+\r\n                     4 |0xChecksum|0xPrelimin|    /|         | Block 513\r\n        512 entries  5 |0xChecksum|0xPrelimin|   / |         | 0x200400\r\n                     6 |0xChecksum|0xPrelimi.|  /  |         |\r\n                     7 |0xChecksum|0xPrelim..| /   +---------+\r\n                     8 |0xChecksum|0xPreli...|\u003c    |         | Block 514\r\n                     9 |0xChecksum|0xPrel....|     |         | 0x200600\r\n                    10 |0xChecksu.|          |     |         |\r\n                       ~          ~          ~     +---------+\r\n                                                   |         |\r\n                                                   ~         ~\r\n\r\n```\r\n\r\nThe checksum for `Data-Blocks` and `Table-Blocks` is a simple exclusive-or and bit-rotate which interprets all 4096 bytes of the block as 1024 32-bit integers, with the exception that the checksum for `Table-Blocks` does not include the first four bytes(the checksum integer of the block itself). All 1024 integers are exclusive-ored with an initial checksum of zero, which is rotated left 1 bit before the exclusive-or operation. Finally the lowest bit is set, making all checksums an odd number.\r\n\r\nThe `NextBlock` integer is a block index used to point to the next block that should be read if one is trying to read a serial stream of data. Ex: A large file that spans multiple blocks will be broken up into multiple blocks, and the table-block will use the \"NextBlock\" flag to point to the next block that should be read, with \"0\" being the last block.\r\n\r\n```cpp\r\n// If your block number is a multiple of 512, set `Table` to true.\r\nstd::uint32_t Checksum(bool Table, std::uint32_t* Data)\r\n{\r\n\tstd::uint32_t Sum = 0;\r\n\tfor( std::size_t i = (Table ? 1 : 0); i \u003c 1024; i++ )\r\n\t{\r\n\t\tSum = ( ( Sum \u003c\u003c 1 ) | (Sum \u003e\u003e 31)) ^ Data[i];\r\n\t}\r\n\treturn Sum | 1; \r\n}\r\n\r\n// Generic version for both Table-Blocks and Data-Blocks\r\n// Works on tables if you set the first 32-bit integer to 0 before running.\r\nstd::uint32_t Checksum(std::uint32_t* Data)\r\n{\r\n\tstd::uint32_t Sum = 0;\r\n\tfor( std::size_t i = 0; i \u003c 1024; i++ )\r\n\t{\r\n\t\tSum = ( ( Sum \u003c\u003c 1 ) | (Sum \u003e\u003e 31)) ^ Data[i];\r\n\t}\r\n\treturn Sum | 1;\r\n}\r\n```\r\n\r\nA block-level corruption can be detected by a checksum mismatch. If the `Data-Block`'s generated checksum does not match the checksum found at the appropriate table entry within the `Table-Block` then the `Data-Block` is considered corrupted.\r\n\r\n## Caching\r\n\r\nSai internally uses a Direct Mapped cache table to speed up the random access and decryption of a file by caching both `Table-Blocks` and `Data-Blocks`. An arbitrary block number will have its appropriate cache entry looked up by first shifting the `BlockNumber` integer right by 14 bits and comparing both the upper 18 bits of the block ID to the lower 31 bits of the cache entry found within the internally mounted file object. Should these two numbers match then a cache-hit has occurred. Otherwise the block is to fully loaded and decrypted into the cache. The the mounted file context object(I've called it `VFSObject` in IDA Pro, has exactly 32 cache lines for `Table-Blocks`. The highest bit of the cache table line is the `dirty` bit which notes if the block is due for a write-back before a new block is to overwrite the entry. Cache size seems to generally be the block-size divided by 8 and will be a different size depending on the file being handled. This cache mechanism is Sai's mechanism to minimize the need for constant file IO stalls at run-time and for efficient file-writing and flushing. Changes are fully \"flushed\" simply by writing any remaining  cache lines to the file with the upper `dirty` bit set(and adjusting appropriate checksums within appropriate `Table-Blocks` if needed). If you plan to implement a library that reads from `.sai` files, you should probably follow the same cache routine to speed up your file access as Sai. `Table-Blocks` should at the very least be cached as almost every random access of a `.sai` file will require you to read the appropriate `Table-Block` before being able to decrypt the `Data-Block`.\r\n\r\n# File System\r\n\r\nNow that the cipher can be fully randomly accessed and decrypted, the virtual file system actually implemented can be deciphered. The file system found after decrypting will be described as a `Virtual File system` or `VFS`(Internally sai refers to them as a `VFS` along with terminology such as \"mounting\" within its error messages). Individual files are described by a `File Allocation Table` that describe the name, timestamp, starting block index, and the size(in bytes) of the data. A `Data-Block` can contain a max of `64` `FATEntries`. Folders are described by having their `Type` variable set to `Folder` and the starting `Block` variable instead points to another `Data-Block` of 64 `FATEntries` depicting the contents of the folder.\r\n\r\n```cpp\r\nenum class EntryType : std::uint8_t\r\n{\r\n\tFolder = 0x10,\r\n\tFile = 0x80\r\n};\r\n\r\nstruct FATEntry\r\n{\r\n\tstd::uint32_t Flags;\r\n\tchar Name[32];\r\n\tstd::uint8_t Pad1;\r\n\tstd::uint8_t Pad2;\r\n\tstd::uint8_t Type; // EntryType enum\r\n\tstd::uint8_t Pad4;\r\n\tstd::uint32_t Block;\r\n\tstd::uint32_t Size;\r\n\t// Windows FILETIME structure\r\n\t// https://msdn.microsoft.com/en-us/library/windows/desktop/ms724284(v=vs.85).aspx\r\n\tstd::uint64_t TimeStamp;\r\n\tstd::uint64_t UnknownB;\r\n};\r\n\r\nstruct FATBlock\r\n{\r\n\tFATEntry Entries[64];\r\n}\r\n```\r\n\u003e**Note:** When reading file-data of an FATEntry, files are **not** stored continuously.\r\n\u003e\r\n\u003e`TableBlocks` may intercept the file stream and must be skipped. So when reading filedata you must abstract away table blocks.\r\n\u003eThis means when reading a file, you must skip all table blocks as if they did not exist and skip over them to simulate continuous files\r\n\u003e\r\n\u003eSo offsets such as:\r\n\u003e\r\n\u003e[0,4096],[2097152,2101248],[4194304,4198400],...,**[TableIndex * 4096,TableIndex * 4096 + 4096]**\r\n\u003e\r\n\u003emust be skipped over\r\n\r\nSome info on `TimeStamp`: To convert this 64 bit integer to the more standardized `time_t` variable simply divide the 64-bit integer by `10000000UL` and subtract by `11644473600ULL`. `FILETIME` is the number of 100-nanosecond intervals since January 1, 1601 while `time_t` is the number of 1-second intervals since January 1, 1970. If you're writing a multi-platform library it's best to use the more standardized `time_t` format when available as most functions converting timestamps into strings use the `time_t` format.\r\n\r\n```cpp\r\ntime_t filetime_to_time_t(std::uint64_t Time)\r\n{\r\n\treturn Time / 10000000ULL - 11644473600ULL;\r\n}\r\n```\r\n\r\nThe `root` directory of the `VFS` will always start at block index `2`. This will always be the position of the first `FATBlock` containing 64 `FatEntries` of the `root` folder. If the `Flags` variable of the `FATEntry` structure is `0` the entry is considered to be unused. The full hierarchy of files can be traversed simply by iterating through all 64 entries of the `FatBlock` within block index `2` and stopping at the entry whose `Flags` variable is set to `0`. If any of the 64 `FATEntries` is a folder, then recursively iterate at the 64 `FatEntries` at the `Block` variable. If the entry is a file then simply go to the starting block index and read `Size` amount of bytes continuously, decrypting appropriate `Data-Blocks` along the way should `Size` be larger than 1 block(`0x1000` bytes). Padded bytes within a block will always be `0`. \r\n\r\nFrom this point on it is assumed you are capable of decrypting the file for random access and can interpret the internal file system format. Now we will look at the actual files and the strucutre in which they are placed within this virtual file system.\r\n\r\n# Folder structure\r\n\r\nThe actual file/folder structure found within `.sai` files describes information on the canvas, layers, a thumbnail image, and other meta-data. Here is a sample file structure of a `.sai` file created in October.\r\n\r\n```\r\n/.a1541b366925e034 |     32 bytes | 2016/10/12 03:53:53\r\n/canvas            |     56 bytes | 2016/10/12 03:53:53\r\n/laytbl            |     60 bytes | 2016/10/12 03:53:53\r\n/layers/           |          --- | 2016/10/12 03:53:53\r\n     /0000000a     | 464007 bytes | 2016/10/12 03:53:53\r\n     /00000010     |    452 bytes | 2016/10/12 03:53:53\r\n     /0000000e     |    361 bytes | 2016/10/12 03:53:53\r\n     /00000011     |    373 bytes | 2016/10/12 03:53:53\r\n     /00000012     |    373 bytes | 2016/10/12 03:53:53\r\n     /0000000f     |    538 bytes | 2016/10/12 03:53:53\r\n     /0000000b     |  82454 bytes | 2016/10/12 03:53:53\r\n/subtbl            |     12 bytes | 2016/10/12 03:53:53\r\n/sublayers/        |          --- | 2016/10/12 03:53:53\r\n     /0000000d     |  87213 bytes | 2016/10/12 03:53:53\r\n/thumbnail         |  90012 bytes | 2016/10/12 03:53:53\r\n```\r\n\r\nthe first entry `.a1541b366925e034` will vary in name but will always be the first entry. See [.xxxxxxxxxxxxxxxx](#xxxxxxxxxxxxxxxx) for more info on this file.\r\n\r\n## Serialization Streams\r\n\r\nBefore going into the file formats a specific format of serialization needs to be explained that is found across the internal files.\r\nSai.exe internally uses a specially formatted array of 32 bit integers that describe how serialized data is to be read and written to a file. A size of `0` delimits the end of the table.\r\n\r\nFormat of the Serial-Table found within Sai.exe for the `reso` identifier.\r\n```\r\n  Serialization Table for `reso` identifier \r\n\r\n0-0x00000004 Serial Entry+-----+------------------------+\r\n  0x0000014C \u003c-----------+     |                        |\r\n1-0x00000002 Serial Entry \\    |  Size in Bytes         |\r\n  0x00000150 \u003c-----------+ \\   +------------------------+\r\n2-0x00000002 Serial Entry   \\  |                        |\r\n  0x00000152 \u003c-----------+   \\ |  Runtime Offset        |\r\n  0x00000000 End               +------------------------+\r\n```\r\n`Runtime Offset` is the offset within the runtime object where `Size` amount of data gets written to in memory after reading from the file. In C++ code this would be the `offsetof` and `sizeof` macro of specific fields of an object being stored in an array. One could trace what an unknown serial entry does by finding what runtime object gets written to and finding out when that specific field gets used again.\r\n\r\nSYSTEMAX Source code, probably\r\n```cpp\r\nstruct ResData\r\n{\r\n\t...\r\n\tstd::uint32_t DPI;//0x14C bytes within some class/struct/etc\r\n\tstd::uint16_t Unknown150;\r\n\tstd::uint16_t Unknown152;\r\n\t...\r\n};\r\n\r\nstd::uint32_t ResDataStream[] =\r\n{\r\n\tsizeof(ResData.DPI),\r\n\toffsetof(ResData, DPI),\r\n\tsizeof(ResData.Unknown150),\r\n\toffsetof(ResData, Unknown152)\r\n};\r\n```\r\n\r\nOutput written by the Serial-Table for some arbitrary runtime ResData object\r\n```\r\n6F 73 65 72 08 00 00 00 00 00 48 00 00 00 00 00\r\n^         ^ ^         ^ ^         ^ ^   ^ ^   ^\r\n+---------+ +---------+ +---------+ +---+ +---+\r\n  `oser`       Size       Serial    Ser.  Ser.\r\n                          Data      Data  Data\r\n                            0        1     2\r\n```\r\n\r\n`oser` is the little endian storage of `reso`. In code the identifier `oser` is actually defined as something along the lines of:\r\n```cpp\r\nconst std::uint32_t ResDataMagic = `reso`;\r\n```\r\n`Size` is simply the sum of all `Size` integers for each `Serial Entry`. This integer gets written so that entire streams of unneeded data may be skipped. If two streams `reso` and `lyid` were next to each other, one could skip to the `lyid` stream by reading 32-bit identifier `reso` to see that it does not match up with `lyid` and use the next 32-bit `Size` integer to know the amount of bytes to skip to get to the next stream. A tag identifier of `0` delimits the end of a `Serial Stream`.\r\n\r\nSample code for reading a serial stream.\r\n```cpp\r\nstd::uint32_t CurTag;\r\nstd::uint32_t CurTagSize;\r\nwhile( File.Read\u003cstd::uint32_t\u003e(CurTag) \u0026\u0026 CurTag )\r\n{\r\n\tFile.Read\u003cstd::uint32_t\u003e(CurTagSize);\r\n\tswitch( CurTag )\r\n\t{\r\n\t\tcase 'reso':\r\n\t\t{\r\n\t\t\t//Handle 'reso' data\r\n\t\t\tFile.Read\u003cstd::uint32_t\u003e(...);\r\n\t\t\tFile.Read\u003cstd::uint16_t\u003e(...);\r\n\t\t\tFile.Read\u003cstd::uint16_t\u003e(...);\r\n\t\t\tbreak;\r\n\t\t}\r\n\t\tcase 'lyid':\r\n\t\t{\r\n\t\t\t//...\r\n\t\t\tbreak;\r\n\t\t}\r\n\t\tcase 'layr':\r\n\t\t{\r\n\t\t\t//...\r\n\t\t\tbreak;\r\n\t\t}\r\n\t\tdefault:\r\n\t\t{\r\n\t\t\t// for any streams that we do not handle,\r\n\t\t\t// we just skip forward in the stream\r\n\t\t\tFile.Seek(File.Tell() + CurTagSize);\r\n\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n}\r\n```\r\n\r\nSerial streams from here on out will be depicted as an enumeration of the four-byte identifier and the formatted data that it contains.\r\n\r\n# Files\r\n\r\n## \".XXXXXXXXXXXXXXXX\"\r\n\r\nThis file name is procedurally generated based on the system that wrote the file. It is a 64 bit hash integer generated from a string involving the information of the motherboard formatted into a `%s/%s/%s` string.\r\n\r\nThree strings are queried from Windows Management Instrumentation(WMI) first with the query\r\n\r\n```\r\nSELECT * FROM Win32_BaseBoard\r\n```\r\n\r\nand then taking the `Manufacturer`, `Product`, and `SerialNumber` table entries (making sure to convert the UTF16 into UTF8) and formatting them together into a string identifying the user's chipset(formatted `%s/%s/%s`). An example chipset:\r\n\r\n```\r\nASUSTeK COMPUTER INC./Z87-DELUXE/130410781704124\r\n```\r\n\r\nThe machine-identifying hash is then calculated with this from this string.\r\nWithin the hash function this null-terminated string is repeated continuously until it fits a 256 byte span.\r\n\r\n```\r\nASUSTeK COMPUTER INC./Z87-DELUXE\r\n/130410781704124\\ASUSTeK COMPUTE\r\nR INC./Z87-DELUXE/13041078170412\r\n4\\0ASUSTeK COMPUTER INC./Z87-DELU\r\nXE/130410781704124\\0ASUSTeK COMPU\r\nTER INC./Z87-DELUXE/130410781704\r\n124\\0ASUSTeK COMPUTER INC./Z87-DE\r\nLUXE/130410781704124\\0ASUSTeK COM\r\n```\r\n\r\nThis 256 byte array of characters is then interpreted as 64 32-bit integers for a chained rotate-and-xor hashing function, generating a 64 bit hash.\r\n\r\n```cpp\r\nstd::uint64_t MachineHash(const char* MachineIdentifier)\r\n{\r\n    std::uint32_t StringBlock[64];\r\n    const char* ReadPoint = MachineIdentifier;\r\n    for(std::size_t i = 0; i \u003c 256; i++)\r\n    {\r\n        reinterpret_cast\u003cstd::uint8_t*\u003e(StringBlock)[i] = *ReadPoint;\r\n        ReadPoint = *ReadPoint ? ++ReadPoint : MachineIdentifier;\r\n    }\r\n    std::uint32_t UpperHash = 0;\r\n    std::uint32_t LowerHash = 0;\r\n    std::uint32_t Temp1 = 0;\r\n    for(std::size_t i = 0; i \u003c 64; i++)\r\n    {\r\n        std::uint32_t CurUpper = UpperHash + StringBlock[i % 64];\r\n        std::uint32_t CurLower = LowerHash + StringBlock[(i + 1) % 64];\r\n        for( std::size_t j = 0; j \u003c 4; j++ )\r\n        {\r\n            CurUpper = CurLower + ((CurUpper \u003c\u003c CurLower) | (CurUpper \u003e\u003e (32 - CurLower)));\r\n            CurLower = CurUpper + ((CurLower \u003c\u003c CurUpper) | (CurLower \u003e\u003e (32 - CurUpper)));\r\n        }\r\n        LowerHash = CurLower ^ Temp1;\r\n        UpperHash ^= CurUpper;\r\n        Temp1 ^= CurLower;\r\n    }\r\n    return (static_cast\u003cstd::uint64_t\u003e(UpperHash) \u003c\u003c 32) | LowerHash;\r\n}\r\n```\r\n\r\nThe resulting hash for the above formatted string is `a1541b366925e034` which would make the filename `.a1541b366925e034` using the internal format `/%s.%016I64x`. The first string seems to always be null leaving the hash to simply have a period character prepended to it.\r\n\r\nThe file itself is only 32 bytes long.\r\n\r\n```cpp\r\nstruct AuthorSystemInfo\r\n{\r\n\tstd::uint32_t BitFlag; // always 0x08000000\r\n\tstd::uint32_t Unknown4;\r\n\tstd::uint64_t DateCreated; // Date Created\r\n\tstd::uint64_t DateModified; // Date Modified\r\n\tstd::uint64_t MachineHash; // Calculated using the above routine\r\n}\r\n```\r\n\r\nTimestamps are 64 bit integer counts of seconds since `January 1, 1601`. This value is calculated using [GetSystemTimeAsFileTime](https://msdn.microsoft.com/en-us/library/windows/desktop/ms724397%28v=vs.85%29.aspx?f=255\u0026MSPPError=-2147217396) and then dividing the 64-bit result by `10000000` to convert from 100-nanosecond-intervals into seconds.\r\n\r\n## \"canvas\"\r\n\r\nThis file contains metadata involving the dimensions of the canvas. The first three integers are a static structure:\r\n\r\n```cpp\r\nstruct CanvasInfo\r\n{\r\n\tstd::uint32_t Unknown0; // Always 0x10(16), possibly bpc or alignment\r\n\tstd::uint32_t Width;\r\n\tstd::uint32_t Height\r\n};\r\n```\r\n\r\nAfter this, a [Serial Stream](#serialization-streams):\r\n\r\n- `reso`\r\n```cpp\r\n// 16.16 fixed point integer\r\nstd::uint32_t DotsPerInch;\r\n// 0 = pixels, 1 = inch, 2 = cm, 3 = mm\r\nstd::uint16_t SizeUnits;\r\n// 0 = pixel/inch, 1 = pixel/cm\r\nstd::uint16_t ResolutionUnits;\r\n```\r\n\r\n- `wsrc`\r\nLayer marked as the selection source\r\n```cpp\r\nstd::uint32_t SelectionSourceID;\r\n```\r\n\r\n- `layr`\r\n```cpp\r\nstd::uint32_t SelectedLayerID;\r\n```\r\n\r\n- `lyid`\r\nSeems to be a duplication of `layr`\r\n```cpp\r\nstd::uint32_t SelectedLayerID;\r\n```\r\n\r\n## \"laytbl\" \"subtbl\"\r\n\r\nThese files contains a description of all layers that make up an image stored from \"lowest\" layer to \"highest\". `subtbl` contains preliminary layers such as masks. Both `laytbl` and `subtbl` have the same format and describe the contents within their respective `layers` and `sublayers` folder.\r\n\r\nThe first integer of either file is a is a 32bit integer for the number of layers followed by an equivalent amount of `LayerTableEntries`. Layers are identified by 32 bit integers with their appropriate filename found in the `layers` and `sublayers` folder using an 8 digit lowercase hexidecimal file name. The full path for any given layer or sublayer identifier can be generated given the identifying integer and the [printf](http://en.cppreference.com/w/cpp/io/c/fprintf) format `/layers/%08x` or `/sublayers/%08x`.\r\n\r\n```cpp\r\nenum class LayerType : std::uint16_t\r\n{\r\n\tNull = 0x00,\r\n\tLayer = 0x03, // Regular Layer\r\n\tUnknown4 = 0x4, // Unknown\r\n\tLinework = 0x05, // Vector Linework Layer\r\n\tMask = 0x06, // Masks applied to any layer object\r\n\tUnknown7 = 0x07, //Unknown\r\n\tSet = 0x08//Layer Folder\r\n};\r\n\r\nstruct LayerTableEntry\r\n{\r\n\tstd::uint32_t Identifier;\r\n\tstd::uint16_t Type; // LayerType enum\r\n\tstd::uint16_t Unknown6; // Gets sent as windows message 0x80CA for some reason\r\n};\r\n```\r\n\r\nSample routine:\r\n```cpp\r\n// First integer is number of layer entires\r\nstd::uint32_t LayerCount = File.Read\u003cstd::uint32_t\u003e();\r\nwhile( LayerCount-- ) // Read each layer entry\r\n{\r\n\t// Read current layer entry into above structure\r\n\tLayerTableEntry CurrentLayer = File.Read\u003cLayerTableEntry\u003e();\r\n\t// Do something with this layer\r\n\t//...\r\n}\r\n\r\n```\r\n---\r\n## \"/layers\" \"/sublayers\"\r\n\r\nThe individual layer files within these folders match the numerical hexidecimal identifiers found in `laytbl` or `subtbl`. These files contain the actual raster or vector data(or none) of the specified layer entry. The header of the file is a static struture identifying the layer's opacity, size, blending mode, etc.\r\n\r\n```cpp\r\nenum BlendingModes : std::uint32_t\r\n{\r\n\tPassThrough = 'pass',\r\n\tNormal = 'norm',\r\n\tMultiply = 'mul\\0',\r\n\tScreen = 'scrn',\r\n\tOverlay = 'over',\r\n\tLuminosity = 'add\\0',\r\n\tShade = 'sub\\0',\r\n\tLumiShade = 'adsb',\r\n\tBinary = 'cbin'\r\n};\r\n\r\n// Rectangular bounds\r\n// Can be off-canvas or larger than canvas if the user moves\r\n// The layer outside of the \"canvas window\" without cropping\r\n// similar to photoshop\r\n// 0,0 is top-left corner of image\r\nstruct LayerBounds\r\n{\r\n\t// Can be negative, rounded to nearest multiple of 32\r\n\tstd::int32_t X;\r\n\tstd::int32_t Y;\r\n\tstd::uint32_t Width;\r\n\tstd::uint32_t Height;\r\n};\r\n\r\nstruct LayerHeader\r\n{\r\n\tstd::uint32_t Type; // LayerType enum\r\n\tstd::uint32_t Identifier;\r\n\tLayerBounds Bounds;\r\n\tstd::uint32_t Unknown18;\r\n\tstd::uint8_t Opacity;\r\n\tstd::uint8_t Visible;\r\n\tstd::uint8_t PreserveOpacity;\r\n\tstd::uint8_t Clipping;\r\n\tstd::uint8_t Unknown1C;\r\n\tstd::uint32_t Blending; // BlendingModes enum\r\n};\r\n```\r\n\r\nImmediately after the `LayerHeader` is a [Serial Stream](#serialization-streams).\r\n\r\n\u003e**Note:**\r\n\u003eNot all streams might be present depending on the type of layer the file is referencing.\r\n\u003eStreams such as `texp` and `peff` may not exist if the layer is a lineart layer or folder\r\n\r\n - `lorg`\r\n\r\n```cpp\r\nstd::uint32_t Unknown0;\r\nstd::uint32_t Unknown4;\r\n```\r\n\r\n- `name`\r\n\r\nZero terminated string of the layer's name.\r\n```cpp\r\nstd::uint8_t LayerName[256];\r\n```\r\n- `pfid`\r\n\r\nParent Set ID. If this layer is a child of a folder this will be a layer identifier of the parent container layer.\r\n```cpp\r\nstd::uint32_t ParentSetID;\r\n```\r\n\r\n- `plid`\r\n\r\nParent Layer ID. If this layer is a child of another layer(ex, a mask-layer) this will be a layer identifier of the parent container layer.\r\n```cpp\r\nstd::uint32_t ParentLayerID;\r\n```\r\n\r\n- `lmfl`\r\n\r\nOnly appears in mask layers\r\n```cpp\r\n// 0b01 = Nonzero blending mode?\r\n// 0b10 = Opacity is greater than 0\r\nstd::uint32_t Unknown0; // Bitmask, only the bottom two bits are used\r\n```\r\n\r\n- `fopn`\r\n\r\nPresent only in a layer that is a Set/Folder. \r\nA single `bool` variable for if the folder is expanded within the layers panel or not\r\n```cpp\r\nstd::uint8_t Open;\r\n```\r\n\r\n- `texn`\r\n\r\nName of the overlay-texture assigned to a layer. Ex: `Watercolor A`\r\nOnly appears in layers that have an overlay enabled\r\n```cpp\r\nstd::uint8_t TextureName[64]; // UTF16 string\r\n```\r\n\r\n- `texp`\r\n\r\n\r\nOptions related to the overlay-texture\r\n```cpp\r\nstd::uint16_t TextureScale;\r\nstd::uint8_t TextureOpacity;\r\n```\r\n\r\n- `peff`\r\n\r\nOptions related to the watercolor fringe assigned to a layer\r\n```cpp\r\nstd::uint8_t Enabled; // bool\r\nstd::uint8_t Opacity; // 100\r\nstd::uint8_t Width;   // 1 - 15\r\n```\r\n\r\n- `vmrk`\r\n```cpp\r\nstd::uint8_t Unknown0;\r\n```\r\n\r\nImmediately after the stream may be the contents of the layer. If the layer is a folder or set, there is no additional data. If the layer is a raster layer of pixels then specially formatted `raster` data follows. If the layer is a linework layer, specifically formatted `linework` data follows.\r\n\r\nSample layer file reading procedure\r\n```cpp\r\n// Read header\r\nLayerHeader CurHeader = LayerFile.Read\u003cLayerHeader\u003e(LayerHead);\r\n\r\n// Read Serial Stream\r\nstd::uint32_t CurTag, CurTagSize;\r\nCurTag = CurTagSize = 0;\r\n\r\nchar Name[256];\r\n\r\nwhile( LayerFile.Read\u003cstd::uint32_t\u003e(CurTag) \u0026\u0026 CurTag )\r\n{\r\n\tLayerFile.Read\u003cstd::uint32_t\u003e(CurTagSize);\r\n\r\n\tswitch( CurTag )\r\n\t{\r\n\tcase 'name':\r\n\t{\r\n\t\tLayerFile.Read\u003cchar[256]\u003e(Name);\r\n\t\tbreak;\r\n\t}\r\n\t// any other cases you care for\r\n\tcase 'pfid': // Parent folder ID\r\n\t{\r\n\t\t// ...\r\n\t\tbreak;\r\n\t}\r\n\tdefault:\r\n\t{\r\n\t\tLayerFile.Seek(LayerFile.Tell() + CurTagSize);\r\n\t\tbreak;\r\n\t}\r\n\t}\r\n}\r\n\r\nif( CurHeader.Type == LayerType::Layer )\r\n{\r\n\t// Read Raster data\r\n}\r\nelse if( CurHeader.Type == LayerType::Linework )\r\n{\r\n\t// Read Linework data\r\n}\r\n\r\n```\r\n\r\n\r\n## Raster Layers\r\n\r\nRaster data is stored in a tiled format immediately after the header structure above. There is an array of `(LayerWidth / 32) * (LayerHeight / 32)` 8-bit boolean integer values stored before the compressed channel pixel data. Each boolean value within this `BlockMap` determines if the appropriately positioned `32x32` tile of bitmap data contains pixel data that varies from pure black transparency. If a tile is active(1), its pixel data is stored as four or more streams of Run-Length-Encoding compressed data for each color channel for that `32x32` tile. If a tile is not active(0), the tile is to be filled with a `32x32` fully transparent block of pixels(`0x00000000` for all pixels). If more than four streams exist, the extra streams may be safely ignored and skipped. Note that the RLE routine is the very same algorithm that Photoshop uses when compressing layer data and the same as the [PackBits](https://en.wikipedia.org/wiki/PackBits) algorithm that apple uses.\r\n\r\nRLE streams are prefixed with a 16-bit size integer for the amount of RLE stream bytes that follow. Compressed channel data will be at max `0x800` bytes. Decompressed data will be at most `0x1000` bytes. Use these as your buffer sizes when reading and decompressing in-place. Color data is stored with `premultiplied alpha` and should be converted to `straight` as soon as relavently needed. It is highly recommended to use SIMD intrinsics featured in C headers such as `emmintrin.h` and `tmmintrin.h` to speed up conversions and arithmetic upon pixel data. Internally Sai uses `MMX` for all of its SIMD speedups so many structures already lend themselves to more modern SIMD speedups(SSE,AVX,etc). Pixel data is stored in BGRA order\r\n\r\n 1. First, load in the array of `(LayerWidth / 32) * (LayerHeight / 32)` bytes immediately following the layer's Serial Stream as `BlockMap`\r\n 2. Iterate both Y and X dimensions by `LayerHeight / 32` and `LayerWidth / 32` times respectively\r\n - **Be sure to iterate the Y dimension first, then the X to ensure a row-by-row iteration.**\r\n   - Access the the boolean at index `(LayerWidth/32) * Y + X` from `BlockMap`\r\n   - If the boolean is true(1)\r\n     - Read a 16 bit integer\r\n\t - If nonzero, read this amount of data, decompress it, and put this data into the correct `B`, `G`, `R`, or `A` channel in order for however you're formatting your pixel data. Read another 16-bit integer and test for non-zero again in step one to get the next channel.\r\n\t   - If there are more than 4 streams(channels) you can safely skip the extra RLE streams by this 16 bit integer amount in bytes by iterating again at step 2.\r\n\t   - I have yet to find out what the extra channels are but it is possibly \"mip-map-like\" data for different zoom levels to speed up certain calculations\r\n\t - If zero, no more streams to read. Move on to the next tile by iterating at step 2.\r\n\r\nHere is a sample scratch-implementation I made using SIMD to shuffle channels into `RGBA` format and convert from `premultiplied alpha` to `straight alpha` as well as \r\n\r\nRoutine for decompressing an RLE stream and placing resulting data into the appropriate interleaved 32bpp 8bpc channel index.\r\n```cpp\r\nvoid RLEDecompress32(void* Destination, const std::uint8_t *Source, std::size_t SourceSize, std::size_t IntCount, std::size_t Channel)\r\n{\r\n\tstd::uint8_t *Write = reinterpret_cast\u003cstd::uint8_t*\u003e(Destination) + Channel;\r\n\tstd::size_t WriteCount = 0;\r\n\r\n\twhile( WriteCount \u003c IntCount )\r\n\t{\r\n\t\tstd::uint8_t Length = *Source++;\r\n\t\tif( Length == 128 ) // No-op\r\n\t\t{\r\n\t\t}\r\n\t\telse if( Length \u003c 128 ) // Copy\r\n\t\t{\r\n\t\t\t// Copy the next Length+1 bytes\r\n\t\t\tLength++;\r\n\t\t\tWriteCount += Length;\r\n\t\t\twhile( Length )\r\n\t\t\t{\r\n\t\t\t\t*Write = *Source++;\r\n\t\t\t\tWrite += 4;\r\n\t\t\t\tLength--;\r\n\t\t\t}\r\n\t\t}\r\n\t\telse if( Length \u003e 128 ) // Repeating byte\r\n\t\t{\r\n\t\t\t// Repeat next byte exactly \"-Length + 1\" times\r\n\t\t\tLength ^= 0xFF;\r\n\t\t\tLength += 2;\r\n\t\t\tWriteCount += Length;\r\n\t\t\tstd::uint8_t Value = *Source++;\r\n\t\t\twhile( Length )\r\n\t\t\t{\r\n\t\t\t\t*Write = Value;\r\n\t\t\t\tWrite += 4;\r\n\t\t\t\tLength--;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n```\r\n\r\n```cpp\r\n\r\n// Read BlockMap\r\n// Do not use a vector\u003cbool\u003e as this is commonly implemented as a specialized vector type that does not implement individual bool values as bytes but rather as packed bits within a word\r\nstd::vector\u003cstd::uint8_t\u003e BlockMap;\r\nTileData.resize((LayerHead.Bounds.Width / 32) * (LayerHead.Bounds.Height / 32));\r\n\r\n// Read Block Map\r\nLayerFile.Read(BlockMap.data(), (LayerHead.Bounds.Width / 32) * (LayerHead.Bounds.Height / 32));\r\n\r\n// the resulting raster image data for this layer, RGBA 32bpp interleaved\r\n// Use a vector to ensure that tiles with no data are still initialized\r\n// to #00000000\r\n// Also note that the claim that SystemMax has made involving 16bit color depth\r\n// may actually only be true at run-time. All raster data found in files are stored at\r\n// 8bpc while only some run-time color arithmetic converts to 16-bit\r\nstd::vector\u003cstd::uint8_t\u003e LayerImage;\r\nLayerImage.resize(LayerHead.Bounds.Width * LayerHead.Bounds.Height * 4);\r\n\r\n\r\n// iterate 32x32 tile chunks row by row\r\nfor( std::size_t y = 0; y \u003c (LayerHead.Bounds.Height / 32); y++ )\r\n{\r\n\tfor( std::size_t x = 0; x \u003c (LayerHead.Bounds.Width / 32); x++ )\r\n\t{\r\n\t\tif( BlockMap[(LayerHead.Bounds.Width / 32) * y + x] ) // if tile is active\r\n\t\t{\r\n\t\t\t// Decompress Tile\r\n\t\t\tstd::array\u003cstd::uint8_t, 0x800\u003e CompressedTile;\r\n\r\n\t\t\t// Aligned memory for simd\r\n\t\t\talignas(sizeof(__m128i)) std::array\u003cstd::uint8_t, 0x1000\u003e DecompressedTile;\r\n\r\n\t\t\tstd::uint8_t Channel = 0;\r\n\t\t\tstd::uint16_t Size = 0;\r\n\t\t\twhile( LayerFile.Read\u003cstd::uint16_t\u003e(Size) ) // Get Current RLE stream size\r\n\t\t\t{\r\n\t\t\t\tLayerFile.Read(CompressedTile.data(), Size);\r\n\t\t\t\t// decompress and place into the appropriate interleaved channel\r\n\t\t\t\tRLEDecompress32(\r\n\t\t\t\t\tDecompressedTile.data(),\r\n\t\t\t\t\tCompressedTile.data(),\r\n\t\t\t\t\tSize,\r\n\t\t\t\t\t1024,\r\n\t\t\t\t\tChannel\r\n\t\t\t\t);\r\n\t\t\t\tChannel++; // Move on to next channel\r\n\t\t\t\tif( Channel \u003e= 4 ) // skip all other channels besides the RGBA ones we care about\r\n\t\t\t\t{\r\n\t\t\t\t\tfor( std::size_t i = 0; i \u003c 4; i++ )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tstd::uint16_t Size = LayerFile.Read\u003cstd::uint16_t\u003e();\r\n\t\t\t\t\t\tLayerFile.Seek(LayerFile.Tell() + Size);\r\n\t\t\t\t\t}\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\t// Current 32x32 tile within final image\r\n\t\t\tstd::uint32_t *ImageBlock = reinterpret_cast\u003cstd::uint32_t*\u003e(LayerImage.data()) + (x * 32) + ((y * LayerHead.Bounds.Width) * 32);\r\n\r\n\t\t\tfor( std::size_t i = 0; i \u003c (32 * 32) / 4; i++ ) // Process 4 pixels at a time\r\n\t\t\t{\r\n\t\t\t\t__m128i QuadPixel = _mm_load_si128(\r\n\t\t\t\t\treinterpret_cast\u003c__m128i*\u003e(DecompressedTile.data()) + i\r\n\t\t\t\t);\r\n\r\n\t\t\t\t// ABGR to ARGB, if you want.\r\n\t\t\t\t// Do your swizzling here\r\n\t\t\t\tQuadPixel = _mm_shuffle_epi8(\r\n\t\t\t\t\tQuadPixel,\r\n\t\t\t\t\t_mm_set_epi8(\r\n\t\t\t\t\t\t15, 12, 13, 14,\r\n\t\t\t\t\t\t11, 8, 9, 10,\r\n\t\t\t\t\t\t7, 4, 5, 6,\r\n\t\t\t\t\t\t3, 0, 1, 2)\r\n\t\t\t\t);\r\n\r\n\t\t\t\t/// Alpha is pre-multiplied, convert to straight\r\n\t\t\t\t// Get Alpha into [0.0,1.0] range\r\n\t\t\t\t__m128 Scale = _mm_div_ps(\r\n\t\t\t\t\t_mm_cvtepi32_ps(\r\n\t\t\t\t\t\t_mm_shuffle_epi8(\r\n\t\t\t\t\t\t\tQuadPixel,\r\n\t\t\t\t\t\t\t_mm_set_epi8(\r\n\t\t\t\t\t\t\t\t-1, -1, -1, 15,\r\n\t\t\t\t\t\t\t\t-1, -1, -1, 11,\r\n\t\t\t\t\t\t\t\t-1, -1, -1, 7,\r\n\t\t\t\t\t\t\t\t-1, -1, -1, 3\r\n\t\t\t\t\t\t\t)\r\n\t\t\t\t\t\t)\r\n\t\t\t\t\t), _mm_set1_ps(255.0f));\r\n\r\n\t\t\t\t// Normalize each channel into straight color\r\n\t\t\t\tfor( std::uint8_t i = 0; i \u003c 3; i++ )\r\n\t\t\t\t{\r\n\t\t\t\t\t__m128i CurChannel = _mm_srli_epi32(QuadPixel, i * 8);\r\n\t\t\t\t\tCurChannel = _mm_and_si128(CurChannel, _mm_set1_epi32(0xFF));\r\n\t\t\t\t\t__m128 ChannelFloat = _mm_cvtepi32_ps(CurChannel);\r\n\r\n\t\t\t\t\tChannelFloat = _mm_div_ps(ChannelFloat, _mm_set1_ps(255.0));// [0,255] to [0,1]\r\n\t\t\t\t\tChannelFloat = _mm_div_ps(ChannelFloat, Scale);\r\n\t\t\t\t\tChannelFloat = _mm_mul_ps(ChannelFloat, _mm_set1_ps(255.0));// [0,1] to [0,255]\r\n\r\n\t\t\t\t\tCurChannel = _mm_cvtps_epi32(ChannelFloat);\r\n\t\t\t\t\tCurChannel = _mm_and_si128(CurChannel, _mm_set1_epi32(0xff));\r\n\t\t\t\t\tCurChannel = _mm_slli_epi32(CurChannel, i * 8);\r\n\r\n\t\t\t\t\tQuadPixel = _mm_andnot_si128(_mm_set1_epi32(0xFF \u003c\u003c (i * 8)), QuadPixel);\r\n\t\t\t\t\tQuadPixel = _mm_or_si128(QuadPixel, CurChannel);\r\n\t\t\t\t}\r\n\r\n\t\t\t\t// Write directly to final image\r\n\t\t\t\t_mm_store_si128(\r\n\t\t\t\t\treinterpret_cast\u003c__m128i*\u003e(ImageBlock) + (i % 8) + ((i / 8) * (LayerHead.Bounds.Width / 4)),\r\n\t\t\t\t\tQuadPixel\r\n\t\t\t\t);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n```\r\n\r\n---\r\n\r\n## Mask Layers\r\n\r\nMask layers consist of 16bpc grayscale pixels, stored in big endian. They can be read with the same procedure that `raster` data uses.\r\n\r\nThis is a snippet with the current implementation of `ReadRasterLayer` on `Document.cpp`, but using `int16_t` and a smaller `Compress` and `Decompressed` buffer instead:\r\n\r\n```cpp\r\nstd::unique_ptr\u003cstd::int16_t[]\u003e ReadMaskLayer(\r\n\tconst sai::LayerHeader\u0026 LayerHeader, sai::VirtualFileEntry\u0026 LayerFile\r\n)\r\n{\r\n\tconst std::size_t TileSize    = 32u;\r\n\tconst std::size_t LayerTilesX = LayerHeader.Bounds.Width / TileSize;\r\n\tconst std::size_t LayerTilesY = LayerHeader.Bounds.Height / TileSize;\r\n\tconst auto Index2D = [](std::size_t X, std::size_t Y, std::size_t Stride\r\n\t\t\t\t\t\t ) -\u003e std::size_t { return X + (Y * Stride); };\r\n\t// Do not use a std::vector\u003cbool\u003e as this is implemented as a specialized\r\n\t// type that does not implement individual bool values as bytes, but rather\r\n\t// as packed bits within a word.\r\n\r\n\tstd::unique_ptr\u003cstd::uint8_t[]\u003e TileMap\r\n\t\t= std::make_unique\u003cstd::uint8_t[]\u003e(LayerTilesX * LayerTilesY);\r\n\tLayerFile.Read(TileMap.get(), LayerTilesX * LayerTilesY);\r\n\r\n\tstd::unique_ptr\u003cstd::int16_t[]\u003e LayerImage\r\n\t\t= std::make_unique\u003cstd::int16_t[]\u003e(\r\n\t\t\tLayerHeader.Bounds.Width * LayerHeader.Bounds.Height\r\n\t\t);\r\n\r\n\t// 32 x 32 Tile of G8A8 pixels\r\n\tstd::array\u003cstd::uint8_t, 0x800\u003e CompressedTile   = {};\r\n\tstd::array\u003cstd::uint8_t, 0x800\u003e DecompressedTile = {};\r\n\r\n\t// Iterate 32x32 tile chunks row by row\r\n\tfor( std::size_t y = 0; y \u003c LayerTilesY; ++y )\r\n\t{\r\n\t\tfor( std::size_t x = 0; x \u003c LayerTilesX; ++x )\r\n\t\t{\r\n\t\t\t// Process active Tiles\r\n\t\t\tif( !TileMap[Index2D(x, y, LayerTilesX)] )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tstd::uint8_t  CurChannel = 0;\r\n\t\t\tstd::uint16_t RLESize    = 0;\r\n\t\t\t// Iterate RLE streams for each channel\r\n\t\t\twhile( LayerFile.Read\u003cstd::uint16_t\u003e(RLESize)\r\n\t\t\t\t   == sizeof(std::uint16_t) )\r\n\t\t\t{\r\n\t\t\t\tassert(RLESize \u003c= CompressedTile.size());\r\n\t\t\t\tif( LayerFile.Read(CompressedTile.data(), RLESize) != RLESize )\r\n\t\t\t\t{\r\n\t\t\t\t\t// Error reading RLE stream\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t\t// Decompress and place into the appropriate interleaved channel\r\n\t\t\t\tRLEDecompressStride(\r\n\t\t\t\t\tDecompressedTile.data(), CompressedTile.data(),\r\n\t\t\t\t\tsizeof(std::int16_t), 0x1000 / sizeof(std::uint32_t),\r\n\t\t\t\t\tCurChannel\r\n\t\t\t\t);\r\n\t\t\t\t++CurChannel;\r\n\t\t\t\tif( CurChannel == 2 )\r\n\t\t\t\t{\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\t// Write 32x32 tile into final image\r\n\t\t\tconst std::int16_t* ImageSource\r\n\t\t\t\t= reinterpret_cast\u003cconst std::int16_t*\u003e(DecompressedTile.data()\r\n\t\t\t\t);\r\n\t\t\t// Current 32x32 tile within final image\r\n\t\t\tstd::int16_t* ImageDest\r\n\t\t\t\t= LayerImage.get()\r\n\t\t\t\t+ Index2D(x * TileSize, y * LayerHeader.Bounds.Width, TileSize);\r\n\t\t\tfor( std::size_t i = 0; i \u003c (TileSize * TileSize); i++ )\r\n\t\t\t{\r\n\t\t\t\tstd::int16_t CurPixel = ImageSource[i];\r\n\t\t\t\t///\r\n\t\t\t\t// Do any Per-Pixel processing you need to do here\r\n\t\t\t\t///\r\n\t\t\t\tImageDest[Index2D(\r\n\t\t\t\t\ti % TileSize, i / TileSize, LayerHeader.Bounds.Width\r\n\t\t\t\t)] = CurPixel;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\treturn LayerImage;\r\n}\r\n```\r\n\r\n### LayerType::Unknown4 and LayerType::Unknown7\r\n\r\nBoth of this types use the same reading/writing procedure that mask layers, which means that they are probably related to greyscale/monochrome color formats ( although, is not clear if they are actually used at all ).\r\n\r\n---\r\n\r\n## Linework Layers\r\n\r\nTodo\r\n\r\n---\r\n# Decryption Keys\r\n\r\n## UserKey\r\nThis is the key that we care for. Used to encrypt/decrypt all user-created files.\r\nDecrypts `.sai` files.\r\n\r\n```cpp\r\nconst std::uint32_t UserKey[256] =\r\n{\r\n\t0x9913D29E,0x83F58D3D,0xD0BE1526,0x86442EB7,0x7EC69BFB,0x89D75F64,0xFB51B239,0xFF097C56,\r\n\t0xA206EF1E,0x973D668D,0xC383770D,0x1CB4CCEB,0x36F7108B,0x40336BCD,0x84D123BD,0xAFEF5DF3,\r\n\t0x90326747,0xCBFFA8DD,0x25B94703,0xD7C5A4BA,0xE40A17A0,0xEADAE6F2,0x6B738250,0x76ECF24A,\r\n\t0x6F2746CC,0x9BF95E24,0x1ECA68C5,0xE71C5929,0x7817E56C,0x2F99C471,0x395A32B9,0x61438343,\r\n\t0x5E3E4F88,0x80A9332C,0x1879C69F,0x7A03D354,0x12E89720,0xF980448E,0x03643576,0x963C1D7B,\r\n\t0xBBED01D6,0xC512A6B1,0x51CB492B,0x44BADEC9,0xB2D54BC1,0x4E7C2893,0x1531C9A3,0x43A32CA5,\r\n\t0x55B25A87,0x70D9FA79,0xEF5B4AE3,0x8AE7F495,0x923A8505,0x1D92650C,0xC94A9A5C,0x27D4BB14,\r\n\t0x1372A9F7,0x0C19A7FE,0x64FA1A53,0xF1A2EB6D,0x9FEB910F,0x4CE10C4E,0x20825601,0x7DFC98C4,\r\n\t0xA046C808,0x8E90E7BE,0x601DE357,0xF360F37C,0x00CD6F77,0xCC6AB9D4,0x24CC4E78,0xAB1E0BFC,\r\n\t0x6A8BC585,0xFD70ABF0,0xD4A75261,0x1ABF5834,0x45DCFE17,0x5F67E136,0x948FD915,0x65AD9EF5,\r\n\t0x81AB20E9,0xD36EAF42,0x0F7F45C7,0x1BAE72D9,0xBE116AC6,0xDF58B4D5,0x3F0B960E,0xC2613F98,\r\n\t0xB065F8B0,0x6259F975,0xC49AEE84,0x29718963,0x0B6D991D,0x09CF7A37,0x692A6DF8,0x67B68B02,\r\n\t0x2E10DBC2,0x6C34E93C,0xA84B50A1,0xAC6FC0BB,0x5CA6184C,0x34E46183,0x42B379A9,0x79883AB6,\r\n\t0x08750921,0x35AF2B19,0xF7AA886A,0x49F281D3,0xA1768059,0x14568CFD,0x8B3625F6,0x3E1B2D9D,\r\n\t0xF60E14CE,0x1157270A,0xDB5C7EB3,0x738A0AFA,0x19C248E5,0x590CBD62,0x7B37C312,0xFC00B148,\r\n\t0xD808CF07,0xD6BD1C82,0xBD50F1D8,0x91DEA3B8,0xFA86B340,0xF5DF2A80,0x9A7BEA6E,0x1720B8F1,\r\n\t0xED94A56B,0xBF02BE28,0x0D419FA8,0x073B4DBC,0x829E3144,0x029F43E1,0x71E6D51F,0xA9381F09,\r\n\t0x583075E0,0xE398D789,0xF0E31106,0x75073EB5,0x5704863E,0x6EF1043B,0xBC407F33,0x8DBCFB25,\r\n\t0x886C8F22,0x5AF4DD7A,0x2CEACA35,0x8FC969DC,0x9DB8D6B4,0xC65EDC2F,0xE60F9316,0x0A84519A,\r\n\t0x3A294011,0xDCF3063F,0x41621623,0x228CB75B,0x28E9D166,0xAE631B7F,0x06D8C267,0xDA693C94,\r\n\t0x54A5E860,0x7C2170F4,0xF2E294CB,0x5B77A0F9,0xB91522A6,0xEC549500,0x10DD78A7,0x3823E458,\r\n\t0x77D3635A,0x018E3069,0xE039D055,0xD5C341BF,0x9C2400EA,0x85C0A1D1,0x66059C86,0x0416FF1A,\r\n\t0xE27E05C8,0xB19C4C2D,0xFE4DF58F,0xD2F0CE2A,0x32E013C0,0xEED637D7,0xE9FEC1E8,0xA4890DCA,\r\n\t0xF4180313,0x7291738C,0xE1B053A2,0x9801267E,0x2DA15BDB,0xADC4DA4F,0xCF95D474,0xC0265781,\r\n\t0x1F226CED,0xA7472952,0x3C5F0273,0xC152BA68,0xDD66F09B,0x93C7EDCF,0x4F147404,0x3193425D,\r\n\t0x26B5768A,0x0E683B2E,0x952FDF30,0x2A6BAE46,0xA3559270,0xB781D897,0xEB4ECB51,0xDE49394D,\r\n\t0x483F629C,0x2153845E,0xB40D64E2,0x47DB0ED0,0x302D8E4B,0x4BF8125F,0x2BD2B0AC,0x3DC836EC,\r\n\t0xC7871965,0xB64C5CDE,0x9EA8BC27,0xD1853490,0x3B42EC6F,0x63A4FD91,0xAA289D18,0x4D2B1E49,\r\n\t0xB8A060AD,0xB5F6C799,0x6D1F7D1C,0xBA8DAAE6,0xE51A0FC3,0xD94890E7,0x167DF6D2,0x879BCD41,\r\n\t0x5096AC1B,0x05ACB5DA,0x375D24EE,0x7F2EB6AA,0xA535F738,0xCAD0AD10,0xF8456E3A,0x23FD5492,\r\n\t0xB3745532,0x53C1A272,0x469DFCDF,0xE897BF7D,0xA6BBE2AE,0x68CE38AF,0x5D783D0B,0x524F21E4,\r\n\t0x4A257B31,0xCE7A07B2,0x562CE045,0x33B708A4,0x8CEE8AEF,0xC8FB71FF,0x74E52FAB,0xCDB18796\r\n};\r\n```\r\n\r\n## NotRemoveMe\r\nSeems to only be used for the `Notremoveme.ssd` file located in `\"C:\\ProgramData\\SYSTEMAX Software Development\\SAI\"`\r\n\r\nAppears to contain log data similar to `sai.ssd`\r\n\r\n```cpp\r\nconst std::uint32_t NotRemoveMeKey[256] =\r\n{\r\n\t0xA0C62B54,0x0374CB94,0xB3A53F76,0x5B772C6B,0xF2B92931,0x80F923A9,0x7A22EF7A,0x216C7582,\r\n\t0xEDFF8B71,0x8B0C6642,0xAF81AD2F,0x8E095A62,0x02926C0C,0xDD2F56B9,0xA3614155,0xF9AED6E4,\r\n\t0x079C3E5E,0xE6D9E1FD,0x256F165C,0x77280767,0x5D2037A1,0x3019B3CE,0xFC13CC15,0xF457C85F,\r\n\t0x728DF4E9,0x4405AA18,0x2AE0B950,0xE847316F,0xD69FA172,0x62F658E2,0xB0F21F89,0x8AFB852E,\r\n\t0x1A3E924A,0xDBAD0B48,0x88ECBD5A,0xC53FC908,0x81251757,0x57D53685,0x73F463A3,0x048F4B58,\r\n\t0xC36A46AC,0x9A8B6FBD,0x35DC9DC1,0xF76EABF5,0x9280D935,0xBFCC93FB,0x4B2BCA7D,0x60861DFC,\r\n\t0x7C548877,0x2EA46821,0x7136998F,0x5AD45EDF,0x019BA6EF,0x6FC598C7,0x1DF383EC,0x39BAC06D,\r\n\t0x5C3A5B1F,0x7827FB39,0x27FCA953,0x8601E843,0x6C429623,0xBA5DC127,0xCE659075,0x48291378,\r\n\t0x5EDA6B5B,0xE355AC99,0xCF8C704D,0x965E6A29,0xF5035103,0x20582702,0x1B7909DB,0xCA974452,\r\n\t0x7DB20E30,0x2807326C,0x2DF56D0E,0x084E9C41,0xA42DE39C,0x9170A5C3,0x9DB4F95D,0x53CA2068,\r\n\t0x3488FC6E,0xD1BB7AE8,0xC61F81C5,0x310857E5,0xEF1694EE,0xF63067B1,0x3E621B8B,0x22523BFF,\r\n\t0x0D37A4BA,0xCB83BECA,0x9BE78691,0xB7D84E2C,0x45A676DD,0x1F31F636,0x7FAB97C6,0x3CA15F33,\r\n\t0xFA6DB6FE,0x67DD72DC,0x6B8948FA,0x9849FF4B,0xBE452E79,0x38AF6E7F,0x8FE211A7,0x941728B4,\r\n\t0x63217749,0x70EF1280,0x13A9F201,0xACDB14A2,0x1184E73A,0x337E87B5,0xB6008EB7,0xC868C43C,\r\n\t0x85F7DC83,0xD35AD519,0xF87310ED,0xA7C0D29B,0x361D2DCF,0xC1D27C3F,0x9C78DFE0,0x2C4FD8C4,\r\n\t0x05357D9D,0x2B398964,0x182AC610,0xFD4A3873,0xE71E6416,0x842C4A05,0x5946F70F,0xB95FA366,\r\n\t0x1C0B71CB,0x50CEFA06,0xAB9DC211,0x659ABCAE,0xD2E17FE7,0x581A0365,0xA61BE0B0,0xD460B084,\r\n\t0xE21C5CF9,0x87B1D460,0x4DF8CF04,0x4C1573EA,0xCD967432,0xD58EBA12,0x5F2E9A3B,0x6A9955EB,\r\n\t0x55A391AF,0xEBC1EED5,0xB59E8C7C,0x1E825946,0xAA18A04E,0x6891EDF3,0x663C542D,0xC459D37E,\r\n\t0xC06453BC,0x460D223E,0x1690F8DE,0xC97580F7,0xA1F08D4F,0x56DE4381,0xEE06B5E3,0xC2FA05D1,\r\n\t0x3794B488,0xEACD428E,0x7B2362C2,0xE97FDE9F,0xBB4C60D2,0xE4B3E2AB,0x74C93909,0x76AA2FDA,\r\n\t0x9F049B7B,0x93BCDA8A,0x51BEC790,0x0FD6E4CC,0x8972E6AD,0xBCA70F40,0x405C2469,0x10673486,\r\n\t0xBD104C97,0x49381E0D,0x063B456A,0x23D02634,0x43ACEC9E,0xE50E49F8,0x197DBF1B,0x8DF1BB9A,\r\n\t0xB46B1CA6,0xD7E895A5,0xCC51A217,0xE1C2F196,0xDEB533C9,0x24FDC58D,0x32850822,0x12DF4DA8,\r\n\t0x90BD3500,0x97C7F320,0xDA3450F4,0x2F534059,0xDC7B3D63,0x95B6CD98,0x09BF19D6,0xA5D15DBF,\r\n\t0x42E47851,0xF07A021E,0x9ECB2A3D,0xE0C39F38,0x99714F95,0x3A5BEA4C,0xB2C4DD25,0xB13D47C0,\r\n\t0xAD418A0B,0x6DEAB81C,0x83EE25F2,0x3B26AE47,0xA8B018D3,0xFF76E5F1,0xA2ED0461,0x26119ED8,\r\n\t0x61EB0A74,0x15A2B187,0x4A93CE2A,0x7943A707,0x29E5B744,0x4E14F02B,0x0A698424,0xD9A03AE6,\r\n\t0xEC87D7C8,0xA94021B8,0x3D95D1CD,0x6E2415BE,0x52E3F592,0x64A83CD9,0x8263C31D,0x41B87EB6,\r\n\t0x8C50FD1A,0x47C80CD7,0xD844008C,0xB812E9AA,0x0B983013,0xFB7C520A,0x4F66FEBB,0x17E982D0,\r\n\t0x00FE6914,0xFE0FD028,0x0C328F93,0x75021AF6,0x3FE6AFB2,0x7E330DE1,0xDF8ADB45,0x14D37B37,\r\n\t0xD04D06A4,0x694B0156,0x0ECF6170,0xC756EBF0,0xF1B76526,0xF348A8B3,0xAE0A79A0,0x54D7B2D4\r\n};\r\n```\r\n\r\n## LocalState\r\nUsed for thumbnail files located in `\"C:\\ProgramData\\SYSTEMAX Software Development\\SAI\\thumbnail\"`\r\n\r\nThumbnail filenames use [printf](http://en.cppreference.com/w/cpp/io/c/fprintf) pattern `\"%08x.ssd\"`.\r\nNamed `LocalState` as it describes an active user context.\r\n\r\n```cpp\r\nconst std::uint32_t LocalStateKey[256] =\r\n{\r\n\t0x021CF107,0xE9253648,0x8AFBA619,0x8CF31842,0xBF40F860,0xA672F03E,0xFA2756AC,0x927B2E7E,\r\n\t0x1E37D3C4,0x7C3A0524,0x4F284D1B,0xD8A31E9D,0xBA73B6E6,0xF399710D,0xBD8B1937,0x70FFE130,\r\n\t0x056DAA4A,0xDC509CA1,0x07358DFF,0xDF30A2DC,0x67E7349F,0x49532C31,0x2393EBAA,0xE54DF202,\r\n\t0x3A2C7EC9,0x98AB13EF,0x7FA52975,0x83E4792E,0x7485DA08,0x4A1823A8,0x77812011,0x8710BB89,\r\n\t0x9B4E0C68,0x64125D8E,0x5F174A0E,0x33EA50E7,0xA5E168B0,0x1BD9B944,0x6D7D8FE0,0xEE66B84C,\r\n\t0xF0DB530C,0xF8B06B72,0x97ED7DF8,0x126E0122,0x364BED23,0xA103B75C,0x3BC844FA,0xD0946501,\r\n\t0x4E2F70F1,0x79A6F413,0x60B9E977,0xC1582F10,0x759B286A,0xE723EEF5,0x8BAC4B39,0xB074B188,\r\n\t0xCC528E64,0x698700EE,0x44F9E5BB,0x7E336153,0xE2413AFD,0x91DCE2BE,0xFDCE9EC1,0xCAB2DE4F,\r\n\t0x46C5A486,0xA0D630DB,0x1FCD5FCA,0xEA110891,0x3F20C6F9,0xE8F1B25D,0x6EFD10C8,0x889027AF,\r\n\t0xF284AF3F,0x89EE9A61,0x58AF1421,0xE41B9269,0x260C6D71,0x5079D96E,0xD959E465,0x519CD72C,\r\n\t0x73B64F5A,0x40BE5535,0x78386CBC,0x0A1A02CF,0xDBC126B6,0xAD02BC8D,0x22A85BC5,0xA28ABEC3,\r\n\t0x5C643952,0xE35BC9AD,0xCBDACA63,0x4CA076A4,0x4B6121CB,0x9500BF7D,0x6F8E32BF,0xC06587E5,\r\n\t0x21FAEF46,0x9C2AD2F6,0x7691D4A2,0xB13E4687,0xC7460AD6,0xDDFE54D5,0x81F516F3,0xC60D7438,\r\n\t0xB9CB3BC7,0xC4770D94,0xF4571240,0x06862A50,0x30D343D3,0x5ACF52B2,0xACF4E68A,0x0FC2A59B,\r\n\t0xB70AEACD,0x53AA5E80,0xCF624E8F,0xF1214CEB,0x936072DF,0x62193F18,0xF5491CDA,0x5D476958,\r\n\t0xDA7A852D,0x5B053E12,0xC5A9F6D0,0xABD4A7D1,0xD25E6E82,0xA4D17314,0x2E148C4E,0x6B9F6399,\r\n\t0xBC26DB47,0x8296DDCE,0x3E71D616,0x350E4083,0x2063F503,0x167833F2,0x115CDC5E,0x4208E715,\r\n\t0x03A49B66,0x43A724BA,0xA3B71B8C,0x107584AE,0xC24AE0C6,0xB3FC6273,0x280F3795,0x1392C5D4,\r\n\t0xD5BAC762,0xB46B5A3B,0xC9480B8B,0xC39783FC,0x17F2935B,0x9DB482F4,0xA7E9CC09,0x553F4734,\r\n\t0x8DB5C3A3,0x7195EC7A,0xA8518A9A,0x0CE6CB2A,0x14D50976,0x99C077A5,0x012E1733,0x94EC3D7C,\r\n\t0x3D825805,0x0E80A920,0x1D39D1AB,0xFCD85126,0x3C7F3C79,0x7A43780B,0xB26815D9,0xAF1F7F1C,\r\n\t0xBB8D7C81,0xAAE5250F,0x34BC670A,0x1929C8D2,0xD6AE9FC0,0x1AE07506,0x416F3155,0x9EB38698,\r\n\t0x8F22CF29,0x04E8065F,0xE07CFBDE,0x2AEF90E8,0x6CAD049C,0x4DC3A8CC,0x597E3596,0x08562B92,\r\n\t0x52A21D6F,0xB6C9881D,0xFBD75784,0xF613FC32,0x54C6F757,0x66E2D57B,0xCD69FE9E,0x478CA13D,\r\n\t0x2F5F6428,0x8E55913C,0xF9091185,0x0089E8B3,0x1C6A48BD,0x3844946D,0x24CC8B6B,0x6524AC2B,\r\n\t0xD1F6A0F0,0x32980E51,0x8634CE17,0xED67417F,0x250BAEB9,0x84D2FD1A,0xEC6C4593,0x29D0C0B1,\r\n\t0xEBDF42A9,0x0D3DCD45,0x72BF963A,0x27F0B590,0x159D5978,0x3104ABD7,0x903B1F27,0x9F886A56,\r\n\t0x80540FA6,0x18F8AD1F,0xEF5A9870,0x85016FC2,0xC8362D41,0x6376C497,0xE1A15C67,0x6ABD806C,\r\n\t0x569AC1E2,0xFE5D1AF7,0x61CADF59,0xCE063874,0xD4F722DD,0x37DEC2EC,0xAE70BDEA,0x0B2D99B4,\r\n\t0x39B895FE,0x091E9DFB,0xA9150754,0x7D1D7A36,0x9A07B41E,0x5E8FE3B5,0xD34503A0,0xBE2BFAB7,\r\n\t0x5742D0A7,0x48DDBA25,0x7BE3604D,0x2D4C66E9,0xB831FFB8,0xF7BBA343,0x451697E4,0x2C4FD84B,\r\n\t0x96B17B00,0xB5C789E3,0xFFEBF9ED,0xD7C4B349,0xDE3281D8,0x689E4904,0xE683F32F,0x2B3CB0E1\r\n};\r\n```\r\n\r\n## sai.ssd\r\n\r\nUsed only for `sai.ssd`\r\nHandled the same as user-files but with a different block size of `1024` and `Table-blocks` indexes at every multiple of `128`.\r\n\r\n`sai.ssd` seems to have multiple log files stored with symbolic headers:\r\n- \"++FSIF logfile++\"\r\n  - Seems to be related to file-security and encryption\r\n- \"++VFS logfile++\"\r\n  - Everything related to the virtual file system\r\n- \"++SCDF logfile++\"\r\n  - Unknown\r\n\r\n```cpp\r\nconst std::uint32_t SystemKey[256] =\r\n{\r\n\t0x724FB987,0x4A3E70BE,0xCA549C50,0x34E263E1,0x2D5ED2FF,0x127F0E11,0x58A42B78,0x5F6D14AE,\r\n\t0x7E2F745D,0xC3450384,0xCFBB15DE,0xDF0A6D8A,0xEF2545F3,0x6D8919DB,0xBC413C94,0xCCB0A198,\r\n\t0xE42DBBD2,0x361C0B8C,0x8359731F,0x13D61E9F,0x7505F7CE,0x271D7957,0x429C0699,0xD84EC85F,\r\n\t0x953391DD,0xB25DE567,0xC1BA2F97,0x2309B605,0x69A134D1,0x14A092F2,0x681500EF,0xB90148A7,\r\n\t0x01AF398B,0x16FD5168,0x9E572161,0x0F7405E3,0x56AC576D,0xF275A349,0x1E8120C0,0x4BF64E3A,\r\n\t0x5A90E85E,0xD27BC4F1,0x3BD2FFB1,0xD6B40FDC,0x26EC61CF,0xF744AD3F,0xCDE7C548,0x8AFFE60A,\r\n\t0xE382CA47,0x87DA3E1B,0x8FA3DB36,0x5737C7E0,0xACD8CC17,0xD0CC3B66,0xD93D776B,0x37E5BE2B,\r\n\t0xD38A1129,0x037E81D0,0x15B15072,0xA6493052,0x35BCD4B9,0xC4538D32,0xEC66C1D5,0xA20DF513,\r\n\t0x5524EB75,0x92C10488,0xDA03D9FD,0x65168F4B,0x1902BA24,0x7439FA7D,0x1D8CB46F,0xFBC39389,\r\n\t0xC5DF6A58,0x89E8FB00,0x50DBE0A1,0xAAE98AF8,0x6A7C6C9C,0x7712D6EC,0x4030D0CD,0x6052B585,\r\n\t0x6132AA77,0xEB4A38C3,0x673AB1E6,0x1C3C07C6,0x91EA2C76,0x7A4C7EA0,0x10B3DCFC,0xBE7DF402,\r\n\t0x2817D87A,0x25632264,0xBD8D02B0,0xF6D0F8A8,0xB1ED3AF0,0xE6C4F1CA,0x99E028B5,0xE5D48674,\r\n\t0x09CF47B8,0x9D6EAF0E,0x0A721AFE,0xB6109E54,0x8D642344,0x9FEFC27C,0xF0CA520F,0x2C6BDA7E,\r\n\t0x2E9DB06A,0x97DEFC2E,0x53C5F0EE,0xAD4B8C60,0xE9F36696,0xA8C68907,0x70B70A20,0x3D9F82AA,\r\n\t0x7604A595,0x441A563B,0x39193D4A,0x33BF1DC7,0x31B283FB,0xA399F25B,0x642CE39E,0xF9E3B204,\r\n\t0x79A87534,0x5DBE2943,0x9813E93E,0x47864AD6,0xD420D1BF,0x24A6C986,0xFE386EF7,0xD1B65AB7,\r\n\t0x3A96BF2F,0x006FE1AB,0x22938E90,0x78FE7A40,0x5CE1319B,0x46F5EEF5,0xBB064BE4,0xB7271C22,\r\n\t0xC0225D21,0xFA145B10,0x7C58BC33,0xF84654C2,0xEEF4691E,0x021BEC16,0xE16C1737,0x1BCB2603,\r\n\t0x48A2954D,0xDD56A8FA,0xB8C8A48D,0x5277590B,0x1194E7A9,0x590F42B4,0x7B97C0D8,0x7142B714,\r\n\t0xAEDD6BC8,0xBA116212,0x6B0E642C,0xF42ABDC5,0x6E76AC81,0xBF348819,0xCB790C59,0xDC6718AD,\r\n\t0x80471230,0x84DC985C,0x2AEE32C1,0x4D35964F,0x0C6894AC,0x3EF2CDE5,0xB59B37A5,0x9BC9729D,\r\n\t0x186A41AF,0xEA98A970,0x21F8A291,0x5487E2C9,0xE05F3F42,0xA523B86E,0x8C1E4062,0xA962F6CB,\r\n\t0x0D4816E8,0x9A4DF92D,0x20439DCC,0xA0713645,0x43506FE9,0xC2EB4651,0xB4780D6C,0xAFC29B28,\r\n\t0x1FCE5FD4,0x9C7385D3,0xCE00E463,0x38CD997F,0x452933DA,0xC9F7DEBA,0x0840A093,0xDB287B41,\r\n\t0x90E48479,0x66FC6709,0x6C884C65,0x3FB56082,0xF5B87123,0xED367D1D,0x6F0C44F9,0x8270DD38,\r\n\t0x0E314F83,0x1AE69F35,0xD5A51FB3,0xA761A671,0x850B4DED,0x06AE0892,0x5EAA2A06,0xC7FA80F6,\r\n\t0xB0692D4E,0x81657F8F,0x948B0980,0xB3D97C01,0xFC80C3EA,0xFF9E53A4,0x30BD784C,0xF3AD970C,\r\n\t0xA12E9A31,0x04D37646,0x072655A3,0xE8D5F353,0x4CA98BDF,0x7391FE56,0x7D5BEDA6,0x2BD7650D,\r\n\t0x862B5C73,0x8B60A726,0x7F8ECB3C,0x517A49B6,0xD7B9CF5A,0x6308D5BC,0x0B3F68D7,0x62A7EA15,\r\n\t0xC65AFD3D,0xAB8525B2,0xA451B308,0xE7C7AB18,0x88F91369,0x1783279A,0x4F95DF2A,0x41F158BD,\r\n\t0xC8D1CEBB,0x325CD3E2,0xF1928739,0x9355AE8E,0x2FC05EC4,0x4E0735E7,0xDE3B10D9,0x8E18C61A,\r\n\t0xE29AEF25,0x4984D7A2,0x051F247B,0x29AB9055,0xFD2101F4,0x96FB2E1C,0x5BF04327,0x3C8F1BEB\r\n};\r\n```\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwunkolo%2Flibsai","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwunkolo%2Flibsai","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwunkolo%2Flibsai/lists"}