https://github.com/wunkolo/libsai
Library for interfacing with SYSTEMAX's Easy Paint Tool Sai.
https://github.com/wunkolo/libsai
painttool-sai
Last synced: about 1 year ago
JSON representation
Library for interfacing with SYSTEMAX's Easy Paint Tool Sai.
- Host: GitHub
- URL: https://github.com/wunkolo/libsai
- Owner: Wunkolo
- License: mit
- Created: 2016-08-04T06:13:58.000Z (almost 10 years ago)
- Default Branch: main
- Last Pushed: 2025-03-22T22:02:01.000Z (about 1 year ago)
- Last Synced: 2025-03-30T03:11:04.462Z (about 1 year ago)
- Topics: painttool-sai
- Language: C++
- Size: 354 KB
- Stars: 111
- Watchers: 16
- Forks: 15
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
- [Cracking PaintTool Sai documents](#cracking-painttool-sai-documents)
- [Decryption](#decryption)
- [Caching](#caching)
- [File System](#file-system)
- [Folder structure](#folder-structure)
- [Serialization Streams](#serialization-streams)
- [Files](#files)
- [".XXXXXXXXXXXXXXXX"](#xxxxxxxxxxxxxxxx)
- ["canvas"](#canvas)
- ["laytbl" "subtbl"](#laytbl-subtbl)
- ["/layers" "/sublayers"](#layers-sublayers)
- [Raster Layers](#raster-layers)
- [Linework Layers](#linework-layers)
- [Decryption Keys](#decryption-keys)
- [UserKey](#userkey)
- [NotRemoveMe](#notremoveme)
- [LocalState](#localstate)
- [sai.ssd](#saissd)
# Cracking PaintTool Sai documents
This 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.
> PaintTool SAI Ver.1
>
> 
>
> `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.`
>
> SYSTEMAX Software Development
>
> Details:
> - Fully digitizer support with pressure.
> - Amazing anti-aliased drawings.
> - Highly accurate composition with 16bit ARGB channels.
> - Simple but powerful user interface, easy to learn.
> - Fully support Intel MMX Technology.
> - Data protection function to avoid abnormal termination such as bugs.
>
> Copyright 1996-2016 SYSTEMAX Software Development
# Decryption
Sai 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.
An 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`:
```cpp
// Gets the Table-Block index appropriate for the current block index
std::size_t NearestTable(std::size_t BlockIndex)
{
return BlockIndex & ~(0x1FF);
}
// Demonstrating how to quickly determine if a block Index is a data-block or a table-block
bool IsTableBlock(std::size_t BlockIndex)
{
return (BlockIndex & 0x1FF) ? false:true;
}
bool IsDataBlock(std::size_t BlockIndex)
{
return (BlockIndex & 0x1FF) ? true:false;
}
```
All 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`.
`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`.
```cpp
// Ensure BlockIndex is a valid Table-Block index
void DecryptTable(std::uint32_t BlockIndex, std::uint32_t* Data)
{
// see "IsTableBlock" above on making sure BlockIndex
// is a table or use:
// BlockNumber &= (~0x1FF);
for( std::size_t i = 0; i < 1024; i++ )
{
std::uint32_t CurCipher = Data[i];
std::uint32_t X = BlockIndex ^ CurCipher ^ (
UserKey[(BlockIndex >> 24) & 0xFF]
+ UserKey[(BlockIndex >> 16) & 0xFF]
+ UserKey[(BlockIndex >> 8) & 0xFF]
+ UserKey[BlockIndex & 0xFF]);
Data[i] = static_cast((X << 16) | (X >> 16));
BlockIndex = CurCipher;
};
}
void DecryptData(std::uint32_t Vector, std::uint32_t* Data)
{
for( std::size_t i = 0; i < 1024; i++ )
{
std::uint32_t CurCipher = Data[i];
Data[i] =
CurCipher
- (Vector ^ (
UserKey[Vector & 0xFF]
+ UserKey[(Vector >> 8) & 0xFF]
+ UserKey[(Vector >> 16) & 0xFF]
+ UserKey[(Vector >> 24) & 0xFF]));
Vector = CurCipher;
}
}
```
`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.
```cpp
struct TableEntry
{
std::uint32_t Checksum;
std::uint32_t NextBlock;
} TableEntries[512];
```
```
~ ~
Table-Block | |
+----------+----------+<----+---------+
0 |0xChecksum|0xPrelimin| |XXXX|XXXX| Block 512
Checksum used to+--> 1 |0xChecksum|0xPrelimin| |XXXX|XXXX| 0x200200
decrypt block 513 2 |0xChecksum|0xPrelimin| |XXXX|XXXX|
3 |0xChecksum|0xPrelimin| +---------+
4 |0xChecksum|0xPrelimin| /| | Block 513
512 entries 5 |0xChecksum|0xPrelimin| / | | 0x200400
6 |0xChecksum|0xPrelimi.| / | |
7 |0xChecksum|0xPrelim..| / +---------+
8 |0xChecksum|0xPreli...|< | | Block 514
9 |0xChecksum|0xPrel....| | | 0x200600
10 |0xChecksu.| | | |
~ ~ ~ +---------+
| |
~ ~
```
The 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.
The `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.
```cpp
// If your block number is a multiple of 512, set `Table` to true.
std::uint32_t Checksum(bool Table, std::uint32_t* Data)
{
std::uint32_t Sum = 0;
for( std::size_t i = (Table ? 1 : 0); i < 1024; i++ )
{
Sum = ( ( Sum << 1 ) | (Sum >> 31)) ^ Data[i];
}
return Sum | 1;
}
// Generic version for both Table-Blocks and Data-Blocks
// Works on tables if you set the first 32-bit integer to 0 before running.
std::uint32_t Checksum(std::uint32_t* Data)
{
std::uint32_t Sum = 0;
for( std::size_t i = 0; i < 1024; i++ )
{
Sum = ( ( Sum << 1 ) | (Sum >> 31)) ^ Data[i];
}
return Sum | 1;
}
```
A 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.
## Caching
Sai 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`.
# File System
Now 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.
```cpp
enum class EntryType : std::uint8_t
{
Folder = 0x10,
File = 0x80
};
struct FATEntry
{
std::uint32_t Flags;
char Name[32];
std::uint8_t Pad1;
std::uint8_t Pad2;
std::uint8_t Type; // EntryType enum
std::uint8_t Pad4;
std::uint32_t Block;
std::uint32_t Size;
// Windows FILETIME structure
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms724284(v=vs.85).aspx
std::uint64_t TimeStamp;
std::uint64_t UnknownB;
};
struct FATBlock
{
FATEntry Entries[64];
}
```
>**Note:** When reading file-data of an FATEntry, files are **not** stored continuously.
>
>`TableBlocks` may intercept the file stream and must be skipped. So when reading filedata you must abstract away table blocks.
>This 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
>
>So offsets such as:
>
>[0,4096],[2097152,2101248],[4194304,4198400],...,**[TableIndex * 4096,TableIndex * 4096 + 4096]**
>
>must be skipped over
Some 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.
```cpp
time_t filetime_to_time_t(std::uint64_t Time)
{
return Time / 10000000ULL - 11644473600ULL;
}
```
The `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`.
From 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.
# Folder structure
The 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.
```
/.a1541b366925e034 | 32 bytes | 2016/10/12 03:53:53
/canvas | 56 bytes | 2016/10/12 03:53:53
/laytbl | 60 bytes | 2016/10/12 03:53:53
/layers/ | --- | 2016/10/12 03:53:53
/0000000a | 464007 bytes | 2016/10/12 03:53:53
/00000010 | 452 bytes | 2016/10/12 03:53:53
/0000000e | 361 bytes | 2016/10/12 03:53:53
/00000011 | 373 bytes | 2016/10/12 03:53:53
/00000012 | 373 bytes | 2016/10/12 03:53:53
/0000000f | 538 bytes | 2016/10/12 03:53:53
/0000000b | 82454 bytes | 2016/10/12 03:53:53
/subtbl | 12 bytes | 2016/10/12 03:53:53
/sublayers/ | --- | 2016/10/12 03:53:53
/0000000d | 87213 bytes | 2016/10/12 03:53:53
/thumbnail | 90012 bytes | 2016/10/12 03:53:53
```
the first entry `.a1541b366925e034` will vary in name but will always be the first entry. See [.xxxxxxxxxxxxxxxx](#xxxxxxxxxxxxxxxx) for more info on this file.
## Serialization Streams
Before going into the file formats a specific format of serialization needs to be explained that is found across the internal files.
Sai.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.
Format of the Serial-Table found within Sai.exe for the `reso` identifier.
```
Serialization Table for `reso` identifier
0-0x00000004 Serial Entry+-----+------------------------+
0x0000014C <-----------+ | |
1-0x00000002 Serial Entry \ | Size in Bytes |
0x00000150 <-----------+ \ +------------------------+
2-0x00000002 Serial Entry \ | |
0x00000152 <-----------+ \ | Runtime Offset |
0x00000000 End +------------------------+
```
`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.
SYSTEMAX Source code, probably
```cpp
struct ResData
{
...
std::uint32_t DPI;//0x14C bytes within some class/struct/etc
std::uint16_t Unknown150;
std::uint16_t Unknown152;
...
};
std::uint32_t ResDataStream[] =
{
sizeof(ResData.DPI),
offsetof(ResData, DPI),
sizeof(ResData.Unknown150),
offsetof(ResData, Unknown152)
};
```
Output written by the Serial-Table for some arbitrary runtime ResData object
```
6F 73 65 72 08 00 00 00 00 00 48 00 00 00 00 00
^ ^ ^ ^ ^ ^ ^ ^ ^ ^
+---------+ +---------+ +---------+ +---+ +---+
`oser` Size Serial Ser. Ser.
Data Data Data
0 1 2
```
`oser` is the little endian storage of `reso`. In code the identifier `oser` is actually defined as something along the lines of:
```cpp
const std::uint32_t ResDataMagic = `reso`;
```
`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`.
Sample code for reading a serial stream.
```cpp
std::uint32_t CurTag;
std::uint32_t CurTagSize;
while( File.Read(CurTag) && CurTag )
{
File.Read(CurTagSize);
switch( CurTag )
{
case 'reso':
{
//Handle 'reso' data
File.Read(...);
File.Read(...);
File.Read(...);
break;
}
case 'lyid':
{
//...
break;
}
case 'layr':
{
//...
break;
}
default:
{
// for any streams that we do not handle,
// we just skip forward in the stream
File.Seek(File.Tell() + CurTagSize);
break;
}
}
}
```
Serial streams from here on out will be depicted as an enumeration of the four-byte identifier and the formatted data that it contains.
# Files
## ".XXXXXXXXXXXXXXXX"
This 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.
Three strings are queried from Windows Management Instrumentation(WMI) first with the query
```
SELECT * FROM Win32_BaseBoard
```
and 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:
```
ASUSTeK COMPUTER INC./Z87-DELUXE/130410781704124
```
The machine-identifying hash is then calculated with this from this string.
Within the hash function this null-terminated string is repeated continuously until it fits a 256 byte span.
```
ASUSTeK COMPUTER INC./Z87-DELUXE
/130410781704124\ASUSTeK COMPUTE
R INC./Z87-DELUXE/13041078170412
4\0ASUSTeK COMPUTER INC./Z87-DELU
XE/130410781704124\0ASUSTeK COMPU
TER INC./Z87-DELUXE/130410781704
124\0ASUSTeK COMPUTER INC./Z87-DE
LUXE/130410781704124\0ASUSTeK COM
```
This 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.
```cpp
std::uint64_t MachineHash(const char* MachineIdentifier)
{
std::uint32_t StringBlock[64];
const char* ReadPoint = MachineIdentifier;
for(std::size_t i = 0; i < 256; i++)
{
reinterpret_cast(StringBlock)[i] = *ReadPoint;
ReadPoint = *ReadPoint ? ++ReadPoint : MachineIdentifier;
}
std::uint32_t UpperHash = 0;
std::uint32_t LowerHash = 0;
std::uint32_t Temp1 = 0;
for(std::size_t i = 0; i < 64; i++)
{
std::uint32_t CurUpper = UpperHash + StringBlock[i % 64];
std::uint32_t CurLower = LowerHash + StringBlock[(i + 1) % 64];
for( std::size_t j = 0; j < 4; j++ )
{
CurUpper = CurLower + ((CurUpper << CurLower) | (CurUpper >> (32 - CurLower)));
CurLower = CurUpper + ((CurLower << CurUpper) | (CurLower >> (32 - CurUpper)));
}
LowerHash = CurLower ^ Temp1;
UpperHash ^= CurUpper;
Temp1 ^= CurLower;
}
return (static_cast(UpperHash) << 32) | LowerHash;
}
```
The 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.
The file itself is only 32 bytes long.
```cpp
struct AuthorSystemInfo
{
std::uint32_t BitFlag; // always 0x08000000
std::uint32_t Unknown4;
std::uint64_t DateCreated; // Date Created
std::uint64_t DateModified; // Date Modified
std::uint64_t MachineHash; // Calculated using the above routine
}
```
Timestamps 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&MSPPError=-2147217396) and then dividing the 64-bit result by `10000000` to convert from 100-nanosecond-intervals into seconds.
## "canvas"
This file contains metadata involving the dimensions of the canvas. The first three integers are a static structure:
```cpp
struct CanvasInfo
{
std::uint32_t Unknown0; // Always 0x10(16), possibly bpc or alignment
std::uint32_t Width;
std::uint32_t Height
};
```
After this, a [Serial Stream](#serialization-streams):
- `reso`
```cpp
// 16.16 fixed point integer
std::uint32_t DotsPerInch;
// 0 = pixels, 1 = inch, 2 = cm, 3 = mm
std::uint16_t SizeUnits;
// 0 = pixel/inch, 1 = pixel/cm
std::uint16_t ResolutionUnits;
```
- `wsrc`
Layer marked as the selection source
```cpp
std::uint32_t SelectionSourceID;
```
- `layr`
```cpp
std::uint32_t SelectedLayerID;
```
- `lyid`
Seems to be a duplication of `layr`
```cpp
std::uint32_t SelectedLayerID;
```
## "laytbl" "subtbl"
These 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.
The 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`.
```cpp
enum class LayerType : std::uint16_t
{
Null = 0x00,
Layer = 0x03, // Regular Layer
Unknown4 = 0x4, // Unknown
Linework = 0x05, // Vector Linework Layer
Mask = 0x06, // Masks applied to any layer object
Unknown7 = 0x07, //Unknown
Set = 0x08//Layer Folder
};
struct LayerTableEntry
{
std::uint32_t Identifier;
std::uint16_t Type; // LayerType enum
std::uint16_t Unknown6; // Gets sent as windows message 0x80CA for some reason
};
```
Sample routine:
```cpp
// First integer is number of layer entires
std::uint32_t LayerCount = File.Read();
while( LayerCount-- ) // Read each layer entry
{
// Read current layer entry into above structure
LayerTableEntry CurrentLayer = File.Read();
// Do something with this layer
//...
}
```
---
## "/layers" "/sublayers"
The 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.
```cpp
enum BlendingModes : std::uint32_t
{
PassThrough = 'pass',
Normal = 'norm',
Multiply = 'mul\0',
Screen = 'scrn',
Overlay = 'over',
Luminosity = 'add\0',
Shade = 'sub\0',
LumiShade = 'adsb',
Binary = 'cbin'
};
// Rectangular bounds
// Can be off-canvas or larger than canvas if the user moves
// The layer outside of the "canvas window" without cropping
// similar to photoshop
// 0,0 is top-left corner of image
struct LayerBounds
{
// Can be negative, rounded to nearest multiple of 32
std::int32_t X;
std::int32_t Y;
std::uint32_t Width;
std::uint32_t Height;
};
struct LayerHeader
{
std::uint32_t Type; // LayerType enum
std::uint32_t Identifier;
LayerBounds Bounds;
std::uint32_t Unknown18;
std::uint8_t Opacity;
std::uint8_t Visible;
std::uint8_t PreserveOpacity;
std::uint8_t Clipping;
std::uint8_t Unknown1C;
std::uint32_t Blending; // BlendingModes enum
};
```
Immediately after the `LayerHeader` is a [Serial Stream](#serialization-streams).
>**Note:**
>Not all streams might be present depending on the type of layer the file is referencing.
>Streams such as `texp` and `peff` may not exist if the layer is a lineart layer or folder
- `lorg`
```cpp
std::uint32_t Unknown0;
std::uint32_t Unknown4;
```
- `name`
Zero terminated string of the layer's name.
```cpp
std::uint8_t LayerName[256];
```
- `pfid`
Parent Set ID. If this layer is a child of a folder this will be a layer identifier of the parent container layer.
```cpp
std::uint32_t ParentSetID;
```
- `plid`
Parent 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.
```cpp
std::uint32_t ParentLayerID;
```
- `lmfl`
Only appears in mask layers
```cpp
// 0b01 = Nonzero blending mode?
// 0b10 = Opacity is greater than 0
std::uint32_t Unknown0; // Bitmask, only the bottom two bits are used
```
- `fopn`
Present only in a layer that is a Set/Folder.
A single `bool` variable for if the folder is expanded within the layers panel or not
```cpp
std::uint8_t Open;
```
- `texn`
Name of the overlay-texture assigned to a layer. Ex: `Watercolor A`
Only appears in layers that have an overlay enabled
```cpp
std::uint8_t TextureName[64]; // UTF16 string
```
- `texp`
Options related to the overlay-texture
```cpp
std::uint16_t TextureScale;
std::uint8_t TextureOpacity;
```
- `peff`
Options related to the watercolor fringe assigned to a layer
```cpp
std::uint8_t Enabled; // bool
std::uint8_t Opacity; // 100
std::uint8_t Width; // 1 - 15
```
- `vmrk`
```cpp
std::uint8_t Unknown0;
```
Immediately 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.
Sample layer file reading procedure
```cpp
// Read header
LayerHeader CurHeader = LayerFile.Read(LayerHead);
// Read Serial Stream
std::uint32_t CurTag, CurTagSize;
CurTag = CurTagSize = 0;
char Name[256];
while( LayerFile.Read(CurTag) && CurTag )
{
LayerFile.Read(CurTagSize);
switch( CurTag )
{
case 'name':
{
LayerFile.Read(Name);
break;
}
// any other cases you care for
case 'pfid': // Parent folder ID
{
// ...
break;
}
default:
{
LayerFile.Seek(LayerFile.Tell() + CurTagSize);
break;
}
}
}
if( CurHeader.Type == LayerType::Layer )
{
// Read Raster data
}
else if( CurHeader.Type == LayerType::Linework )
{
// Read Linework data
}
```
## Raster Layers
Raster 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.
RLE 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
1. First, load in the array of `(LayerWidth / 32) * (LayerHeight / 32)` bytes immediately following the layer's Serial Stream as `BlockMap`
2. Iterate both Y and X dimensions by `LayerHeight / 32` and `LayerWidth / 32` times respectively
- **Be sure to iterate the Y dimension first, then the X to ensure a row-by-row iteration.**
- Access the the boolean at index `(LayerWidth/32) * Y + X` from `BlockMap`
- If the boolean is true(1)
- Read a 16 bit integer
- 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.
- 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.
- 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
- If zero, no more streams to read. Move on to the next tile by iterating at step 2.
Here 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
Routine for decompressing an RLE stream and placing resulting data into the appropriate interleaved 32bpp 8bpc channel index.
```cpp
void RLEDecompress32(void* Destination, const std::uint8_t *Source, std::size_t SourceSize, std::size_t IntCount, std::size_t Channel)
{
std::uint8_t *Write = reinterpret_cast(Destination) + Channel;
std::size_t WriteCount = 0;
while( WriteCount < IntCount )
{
std::uint8_t Length = *Source++;
if( Length == 128 ) // No-op
{
}
else if( Length < 128 ) // Copy
{
// Copy the next Length+1 bytes
Length++;
WriteCount += Length;
while( Length )
{
*Write = *Source++;
Write += 4;
Length--;
}
}
else if( Length > 128 ) // Repeating byte
{
// Repeat next byte exactly "-Length + 1" times
Length ^= 0xFF;
Length += 2;
WriteCount += Length;
std::uint8_t Value = *Source++;
while( Length )
{
*Write = Value;
Write += 4;
Length--;
}
}
}
}
```
```cpp
// Read BlockMap
// Do not use a vector 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
std::vector BlockMap;
TileData.resize((LayerHead.Bounds.Width / 32) * (LayerHead.Bounds.Height / 32));
// Read Block Map
LayerFile.Read(BlockMap.data(), (LayerHead.Bounds.Width / 32) * (LayerHead.Bounds.Height / 32));
// the resulting raster image data for this layer, RGBA 32bpp interleaved
// Use a vector to ensure that tiles with no data are still initialized
// to #00000000
// Also note that the claim that SystemMax has made involving 16bit color depth
// may actually only be true at run-time. All raster data found in files are stored at
// 8bpc while only some run-time color arithmetic converts to 16-bit
std::vector LayerImage;
LayerImage.resize(LayerHead.Bounds.Width * LayerHead.Bounds.Height * 4);
// iterate 32x32 tile chunks row by row
for( std::size_t y = 0; y < (LayerHead.Bounds.Height / 32); y++ )
{
for( std::size_t x = 0; x < (LayerHead.Bounds.Width / 32); x++ )
{
if( BlockMap[(LayerHead.Bounds.Width / 32) * y + x] ) // if tile is active
{
// Decompress Tile
std::array CompressedTile;
// Aligned memory for simd
alignas(sizeof(__m128i)) std::array DecompressedTile;
std::uint8_t Channel = 0;
std::uint16_t Size = 0;
while( LayerFile.Read(Size) ) // Get Current RLE stream size
{
LayerFile.Read(CompressedTile.data(), Size);
// decompress and place into the appropriate interleaved channel
RLEDecompress32(
DecompressedTile.data(),
CompressedTile.data(),
Size,
1024,
Channel
);
Channel++; // Move on to next channel
if( Channel >= 4 ) // skip all other channels besides the RGBA ones we care about
{
for( std::size_t i = 0; i < 4; i++ )
{
std::uint16_t Size = LayerFile.Read();
LayerFile.Seek(LayerFile.Tell() + Size);
}
break;
}
}
// Current 32x32 tile within final image
std::uint32_t *ImageBlock = reinterpret_cast(LayerImage.data()) + (x * 32) + ((y * LayerHead.Bounds.Width) * 32);
for( std::size_t i = 0; i < (32 * 32) / 4; i++ ) // Process 4 pixels at a time
{
__m128i QuadPixel = _mm_load_si128(
reinterpret_cast<__m128i*>(DecompressedTile.data()) + i
);
// ABGR to ARGB, if you want.
// Do your swizzling here
QuadPixel = _mm_shuffle_epi8(
QuadPixel,
_mm_set_epi8(
15, 12, 13, 14,
11, 8, 9, 10,
7, 4, 5, 6,
3, 0, 1, 2)
);
/// Alpha is pre-multiplied, convert to straight
// Get Alpha into [0.0,1.0] range
__m128 Scale = _mm_div_ps(
_mm_cvtepi32_ps(
_mm_shuffle_epi8(
QuadPixel,
_mm_set_epi8(
-1, -1, -1, 15,
-1, -1, -1, 11,
-1, -1, -1, 7,
-1, -1, -1, 3
)
)
), _mm_set1_ps(255.0f));
// Normalize each channel into straight color
for( std::uint8_t i = 0; i < 3; i++ )
{
__m128i CurChannel = _mm_srli_epi32(QuadPixel, i * 8);
CurChannel = _mm_and_si128(CurChannel, _mm_set1_epi32(0xFF));
__m128 ChannelFloat = _mm_cvtepi32_ps(CurChannel);
ChannelFloat = _mm_div_ps(ChannelFloat, _mm_set1_ps(255.0));// [0,255] to [0,1]
ChannelFloat = _mm_div_ps(ChannelFloat, Scale);
ChannelFloat = _mm_mul_ps(ChannelFloat, _mm_set1_ps(255.0));// [0,1] to [0,255]
CurChannel = _mm_cvtps_epi32(ChannelFloat);
CurChannel = _mm_and_si128(CurChannel, _mm_set1_epi32(0xff));
CurChannel = _mm_slli_epi32(CurChannel, i * 8);
QuadPixel = _mm_andnot_si128(_mm_set1_epi32(0xFF << (i * 8)), QuadPixel);
QuadPixel = _mm_or_si128(QuadPixel, CurChannel);
}
// Write directly to final image
_mm_store_si128(
reinterpret_cast<__m128i*>(ImageBlock) + (i % 8) + ((i / 8) * (LayerHead.Bounds.Width / 4)),
QuadPixel
);
}
}
}
}
```
---
## Mask Layers
Mask layers consist of 16bpc grayscale pixels, stored in big endian. They can be read with the same procedure that `raster` data uses.
This is a snippet with the current implementation of `ReadRasterLayer` on `Document.cpp`, but using `int16_t` and a smaller `Compress` and `Decompressed` buffer instead:
```cpp
std::unique_ptr ReadMaskLayer(
const sai::LayerHeader& LayerHeader, sai::VirtualFileEntry& LayerFile
)
{
const std::size_t TileSize = 32u;
const std::size_t LayerTilesX = LayerHeader.Bounds.Width / TileSize;
const std::size_t LayerTilesY = LayerHeader.Bounds.Height / TileSize;
const auto Index2D = [](std::size_t X, std::size_t Y, std::size_t Stride
) -> std::size_t { return X + (Y * Stride); };
// Do not use a std::vector as this is implemented as a specialized
// type that does not implement individual bool values as bytes, but rather
// as packed bits within a word.
std::unique_ptr TileMap
= std::make_unique(LayerTilesX * LayerTilesY);
LayerFile.Read(TileMap.get(), LayerTilesX * LayerTilesY);
std::unique_ptr LayerImage
= std::make_unique(
LayerHeader.Bounds.Width * LayerHeader.Bounds.Height
);
// 32 x 32 Tile of G8A8 pixels
std::array CompressedTile = {};
std::array DecompressedTile = {};
// Iterate 32x32 tile chunks row by row
for( std::size_t y = 0; y < LayerTilesY; ++y )
{
for( std::size_t x = 0; x < LayerTilesX; ++x )
{
// Process active Tiles
if( !TileMap[Index2D(x, y, LayerTilesX)] )
continue;
std::uint8_t CurChannel = 0;
std::uint16_t RLESize = 0;
// Iterate RLE streams for each channel
while( LayerFile.Read(RLESize)
== sizeof(std::uint16_t) )
{
assert(RLESize <= CompressedTile.size());
if( LayerFile.Read(CompressedTile.data(), RLESize) != RLESize )
{
// Error reading RLE stream
break;
}
// Decompress and place into the appropriate interleaved channel
RLEDecompressStride(
DecompressedTile.data(), CompressedTile.data(),
sizeof(std::int16_t), 0x1000 / sizeof(std::uint32_t),
CurChannel
);
++CurChannel;
if( CurChannel == 2 )
{
break;
}
}
// Write 32x32 tile into final image
const std::int16_t* ImageSource
= reinterpret_cast(DecompressedTile.data()
);
// Current 32x32 tile within final image
std::int16_t* ImageDest
= LayerImage.get()
+ Index2D(x * TileSize, y * LayerHeader.Bounds.Width, TileSize);
for( std::size_t i = 0; i < (TileSize * TileSize); i++ )
{
std::int16_t CurPixel = ImageSource[i];
///
// Do any Per-Pixel processing you need to do here
///
ImageDest[Index2D(
i % TileSize, i / TileSize, LayerHeader.Bounds.Width
)] = CurPixel;
}
}
}
return LayerImage;
}
```
### LayerType::Unknown4 and LayerType::Unknown7
Both 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 ).
---
## Linework Layers
Todo
---
# Decryption Keys
## UserKey
This is the key that we care for. Used to encrypt/decrypt all user-created files.
Decrypts `.sai` files.
```cpp
const std::uint32_t UserKey[256] =
{
0x9913D29E,0x83F58D3D,0xD0BE1526,0x86442EB7,0x7EC69BFB,0x89D75F64,0xFB51B239,0xFF097C56,
0xA206EF1E,0x973D668D,0xC383770D,0x1CB4CCEB,0x36F7108B,0x40336BCD,0x84D123BD,0xAFEF5DF3,
0x90326747,0xCBFFA8DD,0x25B94703,0xD7C5A4BA,0xE40A17A0,0xEADAE6F2,0x6B738250,0x76ECF24A,
0x6F2746CC,0x9BF95E24,0x1ECA68C5,0xE71C5929,0x7817E56C,0x2F99C471,0x395A32B9,0x61438343,
0x5E3E4F88,0x80A9332C,0x1879C69F,0x7A03D354,0x12E89720,0xF980448E,0x03643576,0x963C1D7B,
0xBBED01D6,0xC512A6B1,0x51CB492B,0x44BADEC9,0xB2D54BC1,0x4E7C2893,0x1531C9A3,0x43A32CA5,
0x55B25A87,0x70D9FA79,0xEF5B4AE3,0x8AE7F495,0x923A8505,0x1D92650C,0xC94A9A5C,0x27D4BB14,
0x1372A9F7,0x0C19A7FE,0x64FA1A53,0xF1A2EB6D,0x9FEB910F,0x4CE10C4E,0x20825601,0x7DFC98C4,
0xA046C808,0x8E90E7BE,0x601DE357,0xF360F37C,0x00CD6F77,0xCC6AB9D4,0x24CC4E78,0xAB1E0BFC,
0x6A8BC585,0xFD70ABF0,0xD4A75261,0x1ABF5834,0x45DCFE17,0x5F67E136,0x948FD915,0x65AD9EF5,
0x81AB20E9,0xD36EAF42,0x0F7F45C7,0x1BAE72D9,0xBE116AC6,0xDF58B4D5,0x3F0B960E,0xC2613F98,
0xB065F8B0,0x6259F975,0xC49AEE84,0x29718963,0x0B6D991D,0x09CF7A37,0x692A6DF8,0x67B68B02,
0x2E10DBC2,0x6C34E93C,0xA84B50A1,0xAC6FC0BB,0x5CA6184C,0x34E46183,0x42B379A9,0x79883AB6,
0x08750921,0x35AF2B19,0xF7AA886A,0x49F281D3,0xA1768059,0x14568CFD,0x8B3625F6,0x3E1B2D9D,
0xF60E14CE,0x1157270A,0xDB5C7EB3,0x738A0AFA,0x19C248E5,0x590CBD62,0x7B37C312,0xFC00B148,
0xD808CF07,0xD6BD1C82,0xBD50F1D8,0x91DEA3B8,0xFA86B340,0xF5DF2A80,0x9A7BEA6E,0x1720B8F1,
0xED94A56B,0xBF02BE28,0x0D419FA8,0x073B4DBC,0x829E3144,0x029F43E1,0x71E6D51F,0xA9381F09,
0x583075E0,0xE398D789,0xF0E31106,0x75073EB5,0x5704863E,0x6EF1043B,0xBC407F33,0x8DBCFB25,
0x886C8F22,0x5AF4DD7A,0x2CEACA35,0x8FC969DC,0x9DB8D6B4,0xC65EDC2F,0xE60F9316,0x0A84519A,
0x3A294011,0xDCF3063F,0x41621623,0x228CB75B,0x28E9D166,0xAE631B7F,0x06D8C267,0xDA693C94,
0x54A5E860,0x7C2170F4,0xF2E294CB,0x5B77A0F9,0xB91522A6,0xEC549500,0x10DD78A7,0x3823E458,
0x77D3635A,0x018E3069,0xE039D055,0xD5C341BF,0x9C2400EA,0x85C0A1D1,0x66059C86,0x0416FF1A,
0xE27E05C8,0xB19C4C2D,0xFE4DF58F,0xD2F0CE2A,0x32E013C0,0xEED637D7,0xE9FEC1E8,0xA4890DCA,
0xF4180313,0x7291738C,0xE1B053A2,0x9801267E,0x2DA15BDB,0xADC4DA4F,0xCF95D474,0xC0265781,
0x1F226CED,0xA7472952,0x3C5F0273,0xC152BA68,0xDD66F09B,0x93C7EDCF,0x4F147404,0x3193425D,
0x26B5768A,0x0E683B2E,0x952FDF30,0x2A6BAE46,0xA3559270,0xB781D897,0xEB4ECB51,0xDE49394D,
0x483F629C,0x2153845E,0xB40D64E2,0x47DB0ED0,0x302D8E4B,0x4BF8125F,0x2BD2B0AC,0x3DC836EC,
0xC7871965,0xB64C5CDE,0x9EA8BC27,0xD1853490,0x3B42EC6F,0x63A4FD91,0xAA289D18,0x4D2B1E49,
0xB8A060AD,0xB5F6C799,0x6D1F7D1C,0xBA8DAAE6,0xE51A0FC3,0xD94890E7,0x167DF6D2,0x879BCD41,
0x5096AC1B,0x05ACB5DA,0x375D24EE,0x7F2EB6AA,0xA535F738,0xCAD0AD10,0xF8456E3A,0x23FD5492,
0xB3745532,0x53C1A272,0x469DFCDF,0xE897BF7D,0xA6BBE2AE,0x68CE38AF,0x5D783D0B,0x524F21E4,
0x4A257B31,0xCE7A07B2,0x562CE045,0x33B708A4,0x8CEE8AEF,0xC8FB71FF,0x74E52FAB,0xCDB18796
};
```
## NotRemoveMe
Seems to only be used for the `Notremoveme.ssd` file located in `"C:\ProgramData\SYSTEMAX Software Development\SAI"`
Appears to contain log data similar to `sai.ssd`
```cpp
const std::uint32_t NotRemoveMeKey[256] =
{
0xA0C62B54,0x0374CB94,0xB3A53F76,0x5B772C6B,0xF2B92931,0x80F923A9,0x7A22EF7A,0x216C7582,
0xEDFF8B71,0x8B0C6642,0xAF81AD2F,0x8E095A62,0x02926C0C,0xDD2F56B9,0xA3614155,0xF9AED6E4,
0x079C3E5E,0xE6D9E1FD,0x256F165C,0x77280767,0x5D2037A1,0x3019B3CE,0xFC13CC15,0xF457C85F,
0x728DF4E9,0x4405AA18,0x2AE0B950,0xE847316F,0xD69FA172,0x62F658E2,0xB0F21F89,0x8AFB852E,
0x1A3E924A,0xDBAD0B48,0x88ECBD5A,0xC53FC908,0x81251757,0x57D53685,0x73F463A3,0x048F4B58,
0xC36A46AC,0x9A8B6FBD,0x35DC9DC1,0xF76EABF5,0x9280D935,0xBFCC93FB,0x4B2BCA7D,0x60861DFC,
0x7C548877,0x2EA46821,0x7136998F,0x5AD45EDF,0x019BA6EF,0x6FC598C7,0x1DF383EC,0x39BAC06D,
0x5C3A5B1F,0x7827FB39,0x27FCA953,0x8601E843,0x6C429623,0xBA5DC127,0xCE659075,0x48291378,
0x5EDA6B5B,0xE355AC99,0xCF8C704D,0x965E6A29,0xF5035103,0x20582702,0x1B7909DB,0xCA974452,
0x7DB20E30,0x2807326C,0x2DF56D0E,0x084E9C41,0xA42DE39C,0x9170A5C3,0x9DB4F95D,0x53CA2068,
0x3488FC6E,0xD1BB7AE8,0xC61F81C5,0x310857E5,0xEF1694EE,0xF63067B1,0x3E621B8B,0x22523BFF,
0x0D37A4BA,0xCB83BECA,0x9BE78691,0xB7D84E2C,0x45A676DD,0x1F31F636,0x7FAB97C6,0x3CA15F33,
0xFA6DB6FE,0x67DD72DC,0x6B8948FA,0x9849FF4B,0xBE452E79,0x38AF6E7F,0x8FE211A7,0x941728B4,
0x63217749,0x70EF1280,0x13A9F201,0xACDB14A2,0x1184E73A,0x337E87B5,0xB6008EB7,0xC868C43C,
0x85F7DC83,0xD35AD519,0xF87310ED,0xA7C0D29B,0x361D2DCF,0xC1D27C3F,0x9C78DFE0,0x2C4FD8C4,
0x05357D9D,0x2B398964,0x182AC610,0xFD4A3873,0xE71E6416,0x842C4A05,0x5946F70F,0xB95FA366,
0x1C0B71CB,0x50CEFA06,0xAB9DC211,0x659ABCAE,0xD2E17FE7,0x581A0365,0xA61BE0B0,0xD460B084,
0xE21C5CF9,0x87B1D460,0x4DF8CF04,0x4C1573EA,0xCD967432,0xD58EBA12,0x5F2E9A3B,0x6A9955EB,
0x55A391AF,0xEBC1EED5,0xB59E8C7C,0x1E825946,0xAA18A04E,0x6891EDF3,0x663C542D,0xC459D37E,
0xC06453BC,0x460D223E,0x1690F8DE,0xC97580F7,0xA1F08D4F,0x56DE4381,0xEE06B5E3,0xC2FA05D1,
0x3794B488,0xEACD428E,0x7B2362C2,0xE97FDE9F,0xBB4C60D2,0xE4B3E2AB,0x74C93909,0x76AA2FDA,
0x9F049B7B,0x93BCDA8A,0x51BEC790,0x0FD6E4CC,0x8972E6AD,0xBCA70F40,0x405C2469,0x10673486,
0xBD104C97,0x49381E0D,0x063B456A,0x23D02634,0x43ACEC9E,0xE50E49F8,0x197DBF1B,0x8DF1BB9A,
0xB46B1CA6,0xD7E895A5,0xCC51A217,0xE1C2F196,0xDEB533C9,0x24FDC58D,0x32850822,0x12DF4DA8,
0x90BD3500,0x97C7F320,0xDA3450F4,0x2F534059,0xDC7B3D63,0x95B6CD98,0x09BF19D6,0xA5D15DBF,
0x42E47851,0xF07A021E,0x9ECB2A3D,0xE0C39F38,0x99714F95,0x3A5BEA4C,0xB2C4DD25,0xB13D47C0,
0xAD418A0B,0x6DEAB81C,0x83EE25F2,0x3B26AE47,0xA8B018D3,0xFF76E5F1,0xA2ED0461,0x26119ED8,
0x61EB0A74,0x15A2B187,0x4A93CE2A,0x7943A707,0x29E5B744,0x4E14F02B,0x0A698424,0xD9A03AE6,
0xEC87D7C8,0xA94021B8,0x3D95D1CD,0x6E2415BE,0x52E3F592,0x64A83CD9,0x8263C31D,0x41B87EB6,
0x8C50FD1A,0x47C80CD7,0xD844008C,0xB812E9AA,0x0B983013,0xFB7C520A,0x4F66FEBB,0x17E982D0,
0x00FE6914,0xFE0FD028,0x0C328F93,0x75021AF6,0x3FE6AFB2,0x7E330DE1,0xDF8ADB45,0x14D37B37,
0xD04D06A4,0x694B0156,0x0ECF6170,0xC756EBF0,0xF1B76526,0xF348A8B3,0xAE0A79A0,0x54D7B2D4
};
```
## LocalState
Used for thumbnail files located in `"C:\ProgramData\SYSTEMAX Software Development\SAI\thumbnail"`
Thumbnail filenames use [printf](http://en.cppreference.com/w/cpp/io/c/fprintf) pattern `"%08x.ssd"`.
Named `LocalState` as it describes an active user context.
```cpp
const std::uint32_t LocalStateKey[256] =
{
0x021CF107,0xE9253648,0x8AFBA619,0x8CF31842,0xBF40F860,0xA672F03E,0xFA2756AC,0x927B2E7E,
0x1E37D3C4,0x7C3A0524,0x4F284D1B,0xD8A31E9D,0xBA73B6E6,0xF399710D,0xBD8B1937,0x70FFE130,
0x056DAA4A,0xDC509CA1,0x07358DFF,0xDF30A2DC,0x67E7349F,0x49532C31,0x2393EBAA,0xE54DF202,
0x3A2C7EC9,0x98AB13EF,0x7FA52975,0x83E4792E,0x7485DA08,0x4A1823A8,0x77812011,0x8710BB89,
0x9B4E0C68,0x64125D8E,0x5F174A0E,0x33EA50E7,0xA5E168B0,0x1BD9B944,0x6D7D8FE0,0xEE66B84C,
0xF0DB530C,0xF8B06B72,0x97ED7DF8,0x126E0122,0x364BED23,0xA103B75C,0x3BC844FA,0xD0946501,
0x4E2F70F1,0x79A6F413,0x60B9E977,0xC1582F10,0x759B286A,0xE723EEF5,0x8BAC4B39,0xB074B188,
0xCC528E64,0x698700EE,0x44F9E5BB,0x7E336153,0xE2413AFD,0x91DCE2BE,0xFDCE9EC1,0xCAB2DE4F,
0x46C5A486,0xA0D630DB,0x1FCD5FCA,0xEA110891,0x3F20C6F9,0xE8F1B25D,0x6EFD10C8,0x889027AF,
0xF284AF3F,0x89EE9A61,0x58AF1421,0xE41B9269,0x260C6D71,0x5079D96E,0xD959E465,0x519CD72C,
0x73B64F5A,0x40BE5535,0x78386CBC,0x0A1A02CF,0xDBC126B6,0xAD02BC8D,0x22A85BC5,0xA28ABEC3,
0x5C643952,0xE35BC9AD,0xCBDACA63,0x4CA076A4,0x4B6121CB,0x9500BF7D,0x6F8E32BF,0xC06587E5,
0x21FAEF46,0x9C2AD2F6,0x7691D4A2,0xB13E4687,0xC7460AD6,0xDDFE54D5,0x81F516F3,0xC60D7438,
0xB9CB3BC7,0xC4770D94,0xF4571240,0x06862A50,0x30D343D3,0x5ACF52B2,0xACF4E68A,0x0FC2A59B,
0xB70AEACD,0x53AA5E80,0xCF624E8F,0xF1214CEB,0x936072DF,0x62193F18,0xF5491CDA,0x5D476958,
0xDA7A852D,0x5B053E12,0xC5A9F6D0,0xABD4A7D1,0xD25E6E82,0xA4D17314,0x2E148C4E,0x6B9F6399,
0xBC26DB47,0x8296DDCE,0x3E71D616,0x350E4083,0x2063F503,0x167833F2,0x115CDC5E,0x4208E715,
0x03A49B66,0x43A724BA,0xA3B71B8C,0x107584AE,0xC24AE0C6,0xB3FC6273,0x280F3795,0x1392C5D4,
0xD5BAC762,0xB46B5A3B,0xC9480B8B,0xC39783FC,0x17F2935B,0x9DB482F4,0xA7E9CC09,0x553F4734,
0x8DB5C3A3,0x7195EC7A,0xA8518A9A,0x0CE6CB2A,0x14D50976,0x99C077A5,0x012E1733,0x94EC3D7C,
0x3D825805,0x0E80A920,0x1D39D1AB,0xFCD85126,0x3C7F3C79,0x7A43780B,0xB26815D9,0xAF1F7F1C,
0xBB8D7C81,0xAAE5250F,0x34BC670A,0x1929C8D2,0xD6AE9FC0,0x1AE07506,0x416F3155,0x9EB38698,
0x8F22CF29,0x04E8065F,0xE07CFBDE,0x2AEF90E8,0x6CAD049C,0x4DC3A8CC,0x597E3596,0x08562B92,
0x52A21D6F,0xB6C9881D,0xFBD75784,0xF613FC32,0x54C6F757,0x66E2D57B,0xCD69FE9E,0x478CA13D,
0x2F5F6428,0x8E55913C,0xF9091185,0x0089E8B3,0x1C6A48BD,0x3844946D,0x24CC8B6B,0x6524AC2B,
0xD1F6A0F0,0x32980E51,0x8634CE17,0xED67417F,0x250BAEB9,0x84D2FD1A,0xEC6C4593,0x29D0C0B1,
0xEBDF42A9,0x0D3DCD45,0x72BF963A,0x27F0B590,0x159D5978,0x3104ABD7,0x903B1F27,0x9F886A56,
0x80540FA6,0x18F8AD1F,0xEF5A9870,0x85016FC2,0xC8362D41,0x6376C497,0xE1A15C67,0x6ABD806C,
0x569AC1E2,0xFE5D1AF7,0x61CADF59,0xCE063874,0xD4F722DD,0x37DEC2EC,0xAE70BDEA,0x0B2D99B4,
0x39B895FE,0x091E9DFB,0xA9150754,0x7D1D7A36,0x9A07B41E,0x5E8FE3B5,0xD34503A0,0xBE2BFAB7,
0x5742D0A7,0x48DDBA25,0x7BE3604D,0x2D4C66E9,0xB831FFB8,0xF7BBA343,0x451697E4,0x2C4FD84B,
0x96B17B00,0xB5C789E3,0xFFEBF9ED,0xD7C4B349,0xDE3281D8,0x689E4904,0xE683F32F,0x2B3CB0E1
};
```
## sai.ssd
Used only for `sai.ssd`
Handled the same as user-files but with a different block size of `1024` and `Table-blocks` indexes at every multiple of `128`.
`sai.ssd` seems to have multiple log files stored with symbolic headers:
- "++FSIF logfile++"
- Seems to be related to file-security and encryption
- "++VFS logfile++"
- Everything related to the virtual file system
- "++SCDF logfile++"
- Unknown
```cpp
const std::uint32_t SystemKey[256] =
{
0x724FB987,0x4A3E70BE,0xCA549C50,0x34E263E1,0x2D5ED2FF,0x127F0E11,0x58A42B78,0x5F6D14AE,
0x7E2F745D,0xC3450384,0xCFBB15DE,0xDF0A6D8A,0xEF2545F3,0x6D8919DB,0xBC413C94,0xCCB0A198,
0xE42DBBD2,0x361C0B8C,0x8359731F,0x13D61E9F,0x7505F7CE,0x271D7957,0x429C0699,0xD84EC85F,
0x953391DD,0xB25DE567,0xC1BA2F97,0x2309B605,0x69A134D1,0x14A092F2,0x681500EF,0xB90148A7,
0x01AF398B,0x16FD5168,0x9E572161,0x0F7405E3,0x56AC576D,0xF275A349,0x1E8120C0,0x4BF64E3A,
0x5A90E85E,0xD27BC4F1,0x3BD2FFB1,0xD6B40FDC,0x26EC61CF,0xF744AD3F,0xCDE7C548,0x8AFFE60A,
0xE382CA47,0x87DA3E1B,0x8FA3DB36,0x5737C7E0,0xACD8CC17,0xD0CC3B66,0xD93D776B,0x37E5BE2B,
0xD38A1129,0x037E81D0,0x15B15072,0xA6493052,0x35BCD4B9,0xC4538D32,0xEC66C1D5,0xA20DF513,
0x5524EB75,0x92C10488,0xDA03D9FD,0x65168F4B,0x1902BA24,0x7439FA7D,0x1D8CB46F,0xFBC39389,
0xC5DF6A58,0x89E8FB00,0x50DBE0A1,0xAAE98AF8,0x6A7C6C9C,0x7712D6EC,0x4030D0CD,0x6052B585,
0x6132AA77,0xEB4A38C3,0x673AB1E6,0x1C3C07C6,0x91EA2C76,0x7A4C7EA0,0x10B3DCFC,0xBE7DF402,
0x2817D87A,0x25632264,0xBD8D02B0,0xF6D0F8A8,0xB1ED3AF0,0xE6C4F1CA,0x99E028B5,0xE5D48674,
0x09CF47B8,0x9D6EAF0E,0x0A721AFE,0xB6109E54,0x8D642344,0x9FEFC27C,0xF0CA520F,0x2C6BDA7E,
0x2E9DB06A,0x97DEFC2E,0x53C5F0EE,0xAD4B8C60,0xE9F36696,0xA8C68907,0x70B70A20,0x3D9F82AA,
0x7604A595,0x441A563B,0x39193D4A,0x33BF1DC7,0x31B283FB,0xA399F25B,0x642CE39E,0xF9E3B204,
0x79A87534,0x5DBE2943,0x9813E93E,0x47864AD6,0xD420D1BF,0x24A6C986,0xFE386EF7,0xD1B65AB7,
0x3A96BF2F,0x006FE1AB,0x22938E90,0x78FE7A40,0x5CE1319B,0x46F5EEF5,0xBB064BE4,0xB7271C22,
0xC0225D21,0xFA145B10,0x7C58BC33,0xF84654C2,0xEEF4691E,0x021BEC16,0xE16C1737,0x1BCB2603,
0x48A2954D,0xDD56A8FA,0xB8C8A48D,0x5277590B,0x1194E7A9,0x590F42B4,0x7B97C0D8,0x7142B714,
0xAEDD6BC8,0xBA116212,0x6B0E642C,0xF42ABDC5,0x6E76AC81,0xBF348819,0xCB790C59,0xDC6718AD,
0x80471230,0x84DC985C,0x2AEE32C1,0x4D35964F,0x0C6894AC,0x3EF2CDE5,0xB59B37A5,0x9BC9729D,
0x186A41AF,0xEA98A970,0x21F8A291,0x5487E2C9,0xE05F3F42,0xA523B86E,0x8C1E4062,0xA962F6CB,
0x0D4816E8,0x9A4DF92D,0x20439DCC,0xA0713645,0x43506FE9,0xC2EB4651,0xB4780D6C,0xAFC29B28,
0x1FCE5FD4,0x9C7385D3,0xCE00E463,0x38CD997F,0x452933DA,0xC9F7DEBA,0x0840A093,0xDB287B41,
0x90E48479,0x66FC6709,0x6C884C65,0x3FB56082,0xF5B87123,0xED367D1D,0x6F0C44F9,0x8270DD38,
0x0E314F83,0x1AE69F35,0xD5A51FB3,0xA761A671,0x850B4DED,0x06AE0892,0x5EAA2A06,0xC7FA80F6,
0xB0692D4E,0x81657F8F,0x948B0980,0xB3D97C01,0xFC80C3EA,0xFF9E53A4,0x30BD784C,0xF3AD970C,
0xA12E9A31,0x04D37646,0x072655A3,0xE8D5F353,0x4CA98BDF,0x7391FE56,0x7D5BEDA6,0x2BD7650D,
0x862B5C73,0x8B60A726,0x7F8ECB3C,0x517A49B6,0xD7B9CF5A,0x6308D5BC,0x0B3F68D7,0x62A7EA15,
0xC65AFD3D,0xAB8525B2,0xA451B308,0xE7C7AB18,0x88F91369,0x1783279A,0x4F95DF2A,0x41F158BD,
0xC8D1CEBB,0x325CD3E2,0xF1928739,0x9355AE8E,0x2FC05EC4,0x4E0735E7,0xDE3B10D9,0x8E18C61A,
0xE29AEF25,0x4984D7A2,0x051F247B,0x29AB9055,0xFD2101F4,0x96FB2E1C,0x5BF04327,0x3C8F1BEB
};
```