├── .gitignore ├── samples ├── stb_image_write.c ├── Benchmark.hpp ├── Thumbnail.cpp ├── Decrypt.cpp ├── Tree.cpp └── Document.cpp ├── source ├── sai.cpp ├── ifstream.cpp ├── virtualfileentry.cpp ├── document.cpp ├── virtualpage.cpp ├── virtualfilesystem.cpp ├── ifstreambuf.cpp └── keys.cpp ├── LICENSE ├── .github └── workflows │ └── build.yml ├── sai.bt ├── .clang-format ├── CMakeLists.txt ├── depreciated └── libsai │ ├── sai.hpp │ └── sai.cpp ├── include └── sai.hpp └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.vscode -------------------------------------------------------------------------------- /samples/stb_image_write.c: -------------------------------------------------------------------------------- 1 | #define STB_IMAGE_WRITE_IMPLEMENTATION 2 | #include "stb_image_write.h" -------------------------------------------------------------------------------- /samples/Benchmark.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2017-2023 Wunkolo 2 | // SPDX-License-Identifier: MIT 3 | 4 | #pragma once 5 | #include 6 | #include 7 | 8 | template 9 | struct Benchmark 10 | { 11 | template 12 | static TickType Run(F func, Args&&... args) 13 | { 14 | auto StartPoint = std::chrono::system_clock::now(); 15 | 16 | func(std::forward(args)...); 17 | 18 | auto Duration 19 | = std::chrono::duration_cast(std::chrono::system_clock::now() - StartPoint); 20 | 21 | return Duration; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /source/sai.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2017-2023 Wunkolo 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | namespace sai 10 | { 11 | 12 | VirtualFileVisitor::~VirtualFileVisitor() 13 | { 14 | } 15 | 16 | bool VirtualFileVisitor::VisitFolderBegin(VirtualFileEntry& /*Entry*/) 17 | { 18 | return true; 19 | } 20 | 21 | bool VirtualFileVisitor::VisitFolderEnd(VirtualFileEntry& /*Entry*/) 22 | { 23 | return true; 24 | } 25 | 26 | bool VirtualFileVisitor::VisitFile(VirtualFileEntry& /*Entry*/) 27 | { 28 | return true; 29 | } 30 | 31 | } // namespace sai 32 | -------------------------------------------------------------------------------- /source/ifstream.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2017-2023 Wunkolo 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include 5 | 6 | namespace sai 7 | { 8 | /// ifstream 9 | ifstream::ifstream(const std::filesystem::path& Path) : std::istream(new ifstreambuf()) 10 | { 11 | reinterpret_cast(rdbuf())->open(Path); 12 | } 13 | 14 | void ifstream::open(const std::filesystem::path& Path) const 15 | { 16 | reinterpret_cast(rdbuf())->close(); 17 | reinterpret_cast(rdbuf())->open(Path); 18 | } 19 | 20 | bool ifstream::is_open() const 21 | { 22 | return reinterpret_cast(rdbuf())->is_open(); 23 | } 24 | 25 | ifstream::~ifstream() 26 | { 27 | if( rdbuf() ) 28 | { 29 | delete rdbuf(); 30 | } 31 | } 32 | 33 | } // namespace sai -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2023 Wunkolo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /samples/Thumbnail.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2017-2023 Wunkolo 2 | // SPDX-License-Identifier: MIT 3 | 4 | /* 5 | Sample code for extracting the thumbnail image from a user-created sai file 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "stb_image_write.h" 16 | 17 | const char* const Help 18 | = "Extract thumbnail images from user-created .sai documents\n" 19 | "\tThumbnail (filename) (output)\n" 20 | "\tWunkolo - Wunkolo@gmail.com"; 21 | 22 | int main(int argc, char* argv[]) 23 | { 24 | if( argc < 3 ) 25 | { 26 | puts(Help); 27 | return EXIT_FAILURE; 28 | } 29 | 30 | sai::Document FileIn(argv[1]); 31 | 32 | if( !FileIn.IsOpen() ) 33 | { 34 | std::cout << "Error opening file for reading: " << argv[1] << std::endl; 35 | return EXIT_FAILURE; 36 | } 37 | 38 | uint32_t Width, Height; 39 | Width = Height = 0; 40 | std::unique_ptr Pixels = {}; 41 | std::tie(Pixels, Width, Height) = FileIn.GetThumbnail(); 42 | 43 | stbi_write_png(argv[2], Width, Height, 4, Pixels.get(), 0); 44 | 45 | return EXIT_SUCCESS; 46 | } 47 | -------------------------------------------------------------------------------- /samples/Decrypt.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2017-2023 Wunkolo 2 | // SPDX-License-Identifier: MIT 3 | 4 | /* 5 | Sample code to decrypt any user-created .sai file 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "Benchmark.hpp" 16 | 17 | const char* const Help 18 | = "Decrypt user-created .sai files:\n" 19 | "\tDecrypt.exe (filename) (output)\n" 20 | "\tWunkolo - Wunkolo@gmail.com"; 21 | 22 | int main(int argc, char* argv[]) 23 | { 24 | if( argc < 3 ) 25 | { 26 | puts(Help); 27 | return EXIT_FAILURE; 28 | } 29 | 30 | sai::ifstreambuf FileIn; 31 | FileIn.open(argv[1]); 32 | 33 | if( !FileIn.is_open() ) 34 | { 35 | std::cout << "Error opening file for reading: " << argv[1] << std::endl; 36 | return EXIT_FAILURE; 37 | } 38 | 39 | std::ofstream FileOut; 40 | FileOut.open(argv[2], std::ios::binary); 41 | 42 | if( !FileOut.is_open() ) 43 | { 44 | std::cout << "Error opening file for writing: " << argv[2] << std::endl; 45 | return EXIT_FAILURE; 46 | } 47 | 48 | std::cout << "File decrypted in:" << Benchmark::Run([&]() -> void { 49 | FileOut << &FileIn; 50 | }).count() 51 | << "ns" << std::endl; 52 | 53 | return EXIT_SUCCESS; 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - dev 8 | pull_request: 9 | branches: 10 | - main 11 | paths-ignore: 12 | - "**.md" 13 | release: 14 | 15 | env: 16 | BUILD_TYPE: Release 17 | 18 | jobs: 19 | macos-build: 20 | runs-on: macos-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | with: 25 | submodules: "recursive" 26 | 27 | - name: Configure CMake 28 | run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} 29 | 30 | - name: Build 31 | run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel 8 32 | 33 | linux-build: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v3 38 | with: 39 | submodules: "recursive" 40 | 41 | - name: Configure CMake 42 | run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} 43 | 44 | - name: Build 45 | run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel 8 46 | 47 | windows-build: 48 | runs-on: windows-latest 49 | strategy: 50 | matrix: 51 | arch: [x86, x64, x64_arm64] 52 | 53 | steps: 54 | - name: Checkout 55 | uses: actions/checkout@v3 56 | with: 57 | submodules: "recursive" 58 | 59 | - name: Configure msvc 60 | uses: ilammy/msvc-dev-cmd@v1 61 | with: 62 | arch: ${{matrix.arch}} 63 | 64 | - name: Configure CMake 65 | run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -G "NMake Makefiles" 66 | 67 | - name: Build 68 | run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel 8 69 | -------------------------------------------------------------------------------- /samples/Tree.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2017-2023 Wunkolo 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include "Benchmark.hpp" 15 | 16 | class SaiTreeView : public sai::VirtualFileVisitor 17 | { 18 | public: 19 | SaiTreeView() : FolderDepth(0) 20 | { 21 | } 22 | ~SaiTreeView() 23 | { 24 | } 25 | bool VisitFolderBegin(sai::VirtualFileEntry& Entry) override 26 | { 27 | PrintVirtualFileEntry(Entry); 28 | ++FolderDepth; 29 | return true; 30 | } 31 | bool VisitFolderEnd(sai::VirtualFileEntry& /*Entry*/) override 32 | { 33 | --FolderDepth; 34 | return true; 35 | } 36 | bool VisitFile(sai::VirtualFileEntry& Entry) override 37 | { 38 | PrintVirtualFileEntry(Entry); 39 | return true; 40 | } 41 | 42 | private: 43 | void PrintVirtualFileEntry(const sai::VirtualFileEntry& Entry) const 44 | { 45 | const std::time_t TimeStamp = Entry.GetTimeStamp(); 46 | char TimeString[32]; 47 | std::strftime(TimeString, 32, "%D %R", std::localtime(&TimeStamp)); 48 | PrintNestedFolder(); 49 | std::printf( 50 | "\u251C\u2500\u2500 [%12zu %s] %s\n", Entry.GetSize(), TimeString, Entry.GetName() 51 | ); 52 | } 53 | void PrintNestedFolder() const 54 | { 55 | for( std::size_t i = 0; i < FolderDepth; ++i ) 56 | { 57 | std::fputs("\u2502 ", stdout); 58 | } 59 | } 60 | std::uint32_t FolderDepth; 61 | }; 62 | 63 | const char* const Help 64 | = "Show virtual file system tree of a user-created .sai files:\n" 65 | "\t./Tree (filenames)\n" 66 | "\tWunkolo - Wunkolo@gmail.com"; 67 | 68 | int main(int argc, char* argv[]) 69 | { 70 | if( argc < 2 ) 71 | { 72 | std::puts(Help); 73 | return EXIT_FAILURE; 74 | } 75 | 76 | for( std::size_t i = 1; i < std::size_t(argc); ++i ) 77 | { 78 | sai::Document CurDocument(argv[i]); 79 | 80 | if( !CurDocument.IsOpen() ) 81 | { 82 | std::cout << "Error opening file for reading: " << argv[i] << std::endl; 83 | return EXIT_FAILURE; 84 | } 85 | 86 | const auto Bench = Benchmark::Run([&CurDocument]() -> void { 87 | SaiTreeView TreeVisitor; 88 | CurDocument.IterateFileSystem(TreeVisitor); 89 | }); 90 | std::printf("Iterated VFS of %s in %" PRId64 " ns\n", argv[i], Bench.count()); 91 | } 92 | 93 | return EXIT_SUCCESS; 94 | } 95 | -------------------------------------------------------------------------------- /sai.bt: -------------------------------------------------------------------------------- 1 | //------------------------------------------------ 2 | //--- 010 Editor v10.0 Binary Template 3 | // 4 | // File: 5 | // Authors: Wunkolo 6 | // Version: 0.01 7 | // Purpose: Parsing decrypted .sai files 8 | // Category: 9 | // File Mask: 10 | // ID Bytes: 11 | // History: 12 | //------------------------------------------------ 13 | 14 | const uint32 PageSize = 0x1000; 15 | const uint32 TableSpan = PageSize / 8; 16 | 17 | uint NearestTableIndex(uint PageIndex) 18 | { 19 | return (PageIndex / TableSpan) * TableSpan; 20 | } 21 | uint IsTableIndex(uint PageIndex) 22 | { 23 | return (PageIndex % TableSpan) ? false : true; 24 | } 25 | uint IsDataIndex(uint PageIndex) 26 | { 27 | return (PageIndex % TableSpan) ? true : false; 28 | } 29 | 30 | struct FATEntry 31 | { 32 | enum EntryType 33 | { 34 | Folder = 0x10, 35 | File = 0x80 36 | }; 37 | 38 | uint32 Flags; 39 | char Name[32]; 40 | uchar Pad1; 41 | uchar Pad2; 42 | EntryType Type; 43 | uchar Pad4; 44 | uint32 PageIndex; 45 | uint32 Size; 46 | FILETIME TimeStamp; // Windows FILETIME 47 | uint64 UnknownB; 48 | }; 49 | 50 | union VirtualPage 51 | { 52 | uchar u8[PageSize]; 53 | uint32 u32[PageSize / sizeof(uint32)]; 54 | 55 | struct PageEntry 56 | { 57 | uint32 Checksum; 58 | uint32 NextPageIndex; 59 | } PageEntries[PageSize / sizeof(PageEntry)]; 60 | FATEntry FATEntries[64]; 61 | }; 62 | 63 | //FileSize() 64 | //int FSeek( int64 pos ) 65 | 66 | void ParseTable(int64 Offset) 67 | { 68 | FSeek(Offset); 69 | VirtualPage CurTable; 70 | local uint i; 71 | local uint CurBlockIndex; 72 | local uint SpanStart; 73 | local uint SpanEnd; 74 | local uint NextPageIndex; 75 | local uint CurTableIndex; 76 | for( i = 0; i < PageSize / sizeof(PageEntry); ++i ) 77 | { 78 | SpanStart = Offset + i * PageSize; 79 | SpanEnd = SpanStart + PageSize; 80 | FSeek(SpanStart); 81 | VirtualPage DataBlock; 82 | NextPageIndex = CurTable.PageEntries[i].NextPageIndex; 83 | if(CurTable.PageEntries[i].Checksum == 0) break; 84 | while( NextPageIndex != 0 ) 85 | { 86 | CurTableIndex = NearestTableIndex(NextPageIndex); 87 | NextPageIndex = ReadUInt( CurTableIndex + NextPageIndex * 8 + 4); 88 | FSeek(NextPageIndex * PageSize); 89 | VirtualPage DataSegment; 90 | } 91 | //CurBlockIndex = CurPage.PageEntries[i].NextPageIndex; 92 | //DefinePages(i * PageSize); 93 | } 94 | } 95 | 96 | ParseTable(0); -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | AccessModifierOffset: -4 4 | AlignAfterOpenBracket: BlockIndent 5 | AlignConsecutiveAssignments: true 6 | AlignConsecutiveDeclarations: true 7 | AlignConsecutiveBitFields: true 8 | AlignEscapedNewlines: Right 9 | AlignOperands: AlignAfterOperator 10 | AlignTrailingComments: true 11 | AllowAllParametersOfDeclarationOnNextLine: true 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: None 15 | AllowShortIfStatementsOnASingleLine: false 16 | AllowShortLoopsOnASingleLine: false 17 | AlwaysBreakAfterReturnType: None 18 | AlwaysBreakBeforeMultilineStrings: true 19 | AlwaysBreakTemplateDeclarations: true 20 | BinPackArguments: true 21 | BinPackParameters: true 22 | BitFieldColonSpacing: Both 23 | BreakBeforeBraces: Custom 24 | BraceWrapping: 25 | AfterCaseLabel: true 26 | AfterClass: true 27 | AfterControlStatement: true 28 | AfterEnum: true 29 | AfterFunction: true 30 | AfterNamespace: true 31 | AfterObjCDeclaration: true 32 | AfterStruct: true 33 | AfterUnion: true 34 | BeforeCatch: true 35 | BeforeElse: true 36 | IndentBraces: false 37 | SplitEmptyFunction: true 38 | SplitEmptyRecord: true 39 | SplitEmptyNamespace: true 40 | BreakBeforeBinaryOperators: All 41 | BreakBeforeInheritanceComma: false 42 | BreakBeforeTernaryOperators: true 43 | BreakConstructorInitializers: BeforeColon 44 | BreakStringLiterals: true 45 | ColumnLimit: 100 46 | CompactNamespaces: false 47 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 48 | ConstructorInitializerIndentWidth: 4 49 | ContinuationIndentWidth: 4 50 | Cpp11BracedListStyle: true 51 | DerivePointerAlignment: false 52 | DisableFormat: false 53 | ExperimentalAutoDetectBinPacking: false 54 | FixNamespaceComments: true 55 | IndentCaseLabels: false 56 | IndentWidth: 4 57 | IndentWrappedFunctionNames: true 58 | KeepEmptyLinesAtTheStartOfBlocks: true 59 | MacroBlockBegin: "" 60 | MacroBlockEnd: "" 61 | MaxEmptyLinesToKeep: 1 62 | NamespaceIndentation: None 63 | PenaltyBreakAssignment: 4 64 | PenaltyBreakBeforeFirstCallParameter: 19 65 | PenaltyBreakComment: 300 66 | PenaltyBreakFirstLessLess: 120 67 | PenaltyBreakString: 1000 68 | PenaltyExcessCharacter: 1000000 69 | PenaltyReturnTypeOnItsOwnLine: 60 70 | PointerAlignment: Left 71 | ReflowComments: true 72 | SortIncludes: true 73 | SortUsingDeclarations: true 74 | SpaceAfterCStyleCast: false 75 | SpaceAfterTemplateKeyword: false 76 | SpaceBeforeAssignmentOperators: true 77 | SpaceBeforeParens: Never 78 | SpaceInEmptyParentheses: false 79 | SpacesBeforeTrailingComments: 1 80 | SpacesInAngles: false 81 | SpacesInContainerLiterals: true 82 | SpacesInCStyleCastParentheses: false 83 | SpacesInParentheses: false 84 | SpacesInConditionalStatement: true 85 | SpacesInSquareBrackets: false 86 | Standard: c++20 87 | TabWidth: 4 88 | UseTab: ForContinuationAndIndentation 89 | -------------------------------------------------------------------------------- /source/virtualfileentry.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2017-2023 Wunkolo 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace sai 9 | { 10 | VirtualFileEntry::VirtualFileEntry(std::weak_ptr Stream, const FATEntry& EntryData) 11 | : FATData(EntryData), FileStream(Stream) 12 | { 13 | Offset = 0; 14 | PageIndex = EntryData.PageIndex; 15 | PageOffset = 0; 16 | } 17 | 18 | VirtualFileEntry::~VirtualFileEntry() 19 | { 20 | } 21 | 22 | VirtualPage VirtualFileEntry::GetTablePage(std::size_t Index) const 23 | { 24 | VirtualPage TablePage = {}; 25 | if( std::shared_ptr Stream = FileStream.lock() ) 26 | { 27 | Stream->seekg(VirtualPage::NearestTableIndex(Index) * VirtualPage::PageSize, std::ios::beg); 28 | Stream->read(reinterpret_cast(TablePage.u8.data()), VirtualPage::PageSize); 29 | } 30 | return TablePage; 31 | } 32 | 33 | const char* VirtualFileEntry::GetName() const 34 | { 35 | return FATData.Name; 36 | } 37 | 38 | FATEntry::EntryType VirtualFileEntry::GetType() const 39 | { 40 | return FATData.Type; 41 | } 42 | 43 | std::time_t VirtualFileEntry::GetTimeStamp() const 44 | { 45 | return FATData.TimeStamp / 10000000ULL - 11644473600ULL; 46 | } 47 | 48 | std::size_t VirtualFileEntry::GetSize() const 49 | { 50 | return static_cast(FATData.Size); 51 | } 52 | 53 | std::size_t VirtualFileEntry::GetPageIndex() const 54 | { 55 | return static_cast(FATData.PageIndex); 56 | } 57 | 58 | std::size_t VirtualFileEntry::Tell() const 59 | { 60 | return Offset; 61 | } 62 | 63 | void VirtualFileEntry::Seek(std::size_t NewOffset) 64 | { 65 | if( std::shared_ptr Stream = FileStream.lock() ) 66 | { 67 | if( NewOffset >= FATData.Size ) 68 | { 69 | // Invalid offset 70 | return; 71 | } 72 | Offset = NewOffset; 73 | PageOffset = NewOffset % VirtualPage::PageSize; 74 | PageIndex = FATData.PageIndex; 75 | for( std::size_t i = 0; i < NewOffset / VirtualPage::PageSize; ++i ) 76 | { 77 | // Get the next page index in the page-chain 78 | const std::uint32_t NextPageIndex = GetTablePage(PageIndex) 79 | .PageEntries[PageIndex % VirtualPage::TableSpan] 80 | .NextPageIndex; 81 | if( NextPageIndex ) 82 | { 83 | PageIndex = NextPageIndex; 84 | } 85 | else 86 | { 87 | break; 88 | } 89 | } 90 | } 91 | } 92 | 93 | std::size_t VirtualFileEntry::Read(std::span Destination) 94 | { 95 | std::size_t LeftToRead = Destination.size(); 96 | bool NeedsNextPage = false; 97 | 98 | if( std::shared_ptr Stream = FileStream.lock() ) 99 | { 100 | std::unique_ptr ReadBuffer 101 | = std::make_unique(VirtualPage::PageSize); 102 | 103 | while( Stream ) 104 | { 105 | Stream->seekg(PageIndex * VirtualPage::PageSize + PageOffset, std::ios::beg); 106 | 107 | std::size_t Read; 108 | if( LeftToRead + PageOffset >= VirtualPage::PageSize ) 109 | { 110 | Read = VirtualPage::PageSize - PageOffset; 111 | PageOffset = 0; 112 | NeedsNextPage = true; 113 | } 114 | else 115 | { 116 | Read = LeftToRead; 117 | PageOffset += Read; 118 | } 119 | 120 | Stream->read(reinterpret_cast(ReadBuffer.get()), Read); 121 | const std::size_t BytesWritten = Destination.size() - LeftToRead; 122 | std::memcpy(Destination.data() + BytesWritten, ReadBuffer.get(), Read); 123 | 124 | Offset += Read; 125 | LeftToRead -= Read; 126 | 127 | if( NeedsNextPage ) 128 | { 129 | // NOTE: The reason this is here, instead of moving it into the 130 | // `if (LeftToRead...)` is because `GetTablePage` seeks the 131 | // stream which mess-ups its position. 132 | PageIndex = GetTablePage(PageIndex) 133 | .PageEntries[PageIndex % VirtualPage::TableSpan] 134 | .NextPageIndex; 135 | NeedsNextPage = false; 136 | } 137 | else if( LeftToRead == 0 ) 138 | break; 139 | } 140 | } 141 | 142 | return Destination.size() - LeftToRead; 143 | } 144 | } // namespace sai -------------------------------------------------------------------------------- /source/document.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2017-2023 Wunkolo 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include 5 | 6 | #ifdef __x86_64__ 7 | #include 8 | #endif 9 | namespace sai 10 | { 11 | 12 | Document::Document(const std::filesystem::path& Path) : VirtualFileSystem(Path) 13 | { 14 | } 15 | 16 | Document::~Document() 17 | { 18 | } 19 | 20 | std::tuple Document::GetCanvasSize() 21 | { 22 | if( std::optional Canvas = GetEntry("canvas"); Canvas ) 23 | { 24 | std::uint32_t Alignment; // Always seems to be 0x10, bpc? Alignment? 25 | std::uint32_t Width, Height; 26 | 27 | Canvas->Read(Alignment); 28 | Canvas->Read(Width); 29 | Canvas->Read(Height); 30 | return std::make_tuple(Width, Height); 31 | } 32 | return std::make_tuple(0, 0); 33 | } 34 | 35 | std::tuple, std::uint32_t, std::uint32_t> Document::GetThumbnail() 36 | { 37 | if( std::optional Thumbnail = GetEntry("thumbnail"); Thumbnail ) 38 | { 39 | ThumbnailHeader Header; 40 | Thumbnail->Read(Header.Width); 41 | Thumbnail->Read(Header.Height); 42 | Thumbnail->Read(Header.Magic); 43 | 44 | if( Header.Magic != sai::Tag("BM32") ) 45 | { 46 | return std::make_tuple(nullptr, 0, 0); 47 | } 48 | 49 | const std::size_t PixelCount = Header.Height * Header.Width; 50 | std::unique_ptr Pixels 51 | = std::make_unique(PixelCount * sizeof(std::uint32_t)); 52 | 53 | Thumbnail->Read({Pixels.get(), PixelCount * sizeof(std::uint32_t)}); 54 | 55 | #if 0 56 | //// BGRA to RGBA 57 | std::size_t i = 0; 58 | 59 | //// Simd speedup, four pixels at a time 60 | while( i < ((PixelCount * sizeof(std::uint32_t)) & ~0xF) ) 61 | { 62 | const __m128i Swizzle 63 | = _mm_set_epi8(15, 12, 13, 14, 11, 8, 9, 10, 7, 4, 5, 6, 3, 0, 1, 2); 64 | 65 | __m128i QuadPixel = _mm_loadu_si128(reinterpret_cast<__m128i*>(&Pixels[i])); 66 | 67 | QuadPixel = _mm_shuffle_epi8(QuadPixel, Swizzle); 68 | 69 | _mm_store_si128(reinterpret_cast<__m128i*>(&Pixels[i]), QuadPixel); 70 | 71 | i += (sizeof(std::uint32_t) * 4); 72 | } 73 | 74 | for( ; i < PixelCount * sizeof(std::uint32_t); i += sizeof(std::uint32_t) ) 75 | { 76 | std::swap(Pixels[i], Pixels[i + 2]); 77 | } 78 | #endif 79 | 80 | return std::make_tuple(std::move(Pixels), Header.Width, Header.Height); 81 | } 82 | return std::make_tuple(nullptr, 0, 0); 83 | } 84 | 85 | void Document::IterateLayerFiles(const std::function& LayerProc) 86 | { 87 | if( std::optional LayerTableFile = GetEntry("laytbl"); LayerTableFile ) 88 | { 89 | std::uint32_t LayerCount = LayerTableFile->Read(); 90 | while( LayerCount-- ) // Read each layer entry 91 | { 92 | const LayerTableEntry CurLayerEntry = LayerTableFile->Read(); 93 | char LayerPath[32] = {}; 94 | std::snprintf(LayerPath, 32u, "/layers/%08x", CurLayerEntry.Identifier); 95 | if( std::optional LayerFile = GetEntry(LayerPath); LayerFile ) 96 | { 97 | if( !LayerProc(*LayerFile) ) 98 | break; 99 | } 100 | } 101 | } 102 | } 103 | 104 | void Document::IterateSubLayerFiles(const std::function& SubLayerProc) 105 | { 106 | if( std::optional SubLayerTableFile = GetEntry("subtbl"); SubLayerTableFile ) 107 | { 108 | std::uint32_t SubLayerCount = SubLayerTableFile->Read(); 109 | while( SubLayerCount-- ) // Read each layer entry 110 | { 111 | const LayerTableEntry CurSubLayerEntry = SubLayerTableFile->Read(); 112 | char SubLayerPath[32] = {}; 113 | std::snprintf(SubLayerPath, 32u, "/sublayers/%08x", CurSubLayerEntry.Identifier); 114 | if( std::optional SubLayerFile = GetEntry(SubLayerPath); 115 | SubLayerFile ) 116 | { 117 | if( !SubLayerProc(*SubLayerFile) ) 118 | break; 119 | } 120 | } 121 | } 122 | } 123 | } // namespace sai -------------------------------------------------------------------------------- /source/virtualpage.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2017-2023 Wunkolo 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include 5 | 6 | #ifdef __x86_64__ 7 | #include 8 | #endif 9 | 10 | namespace sai 11 | { 12 | #if defined(__AVX2__) 13 | inline __m256i KeySum8(__m256i Vector8, std::span Key) 14 | { 15 | __m256i Sum = _mm256_i32gather_epi32( 16 | (const std::int32_t*)Key.data(), _mm256_and_si256(Vector8, _mm256_set1_epi32(0xFF)), 17 | sizeof(std::uint32_t) 18 | ); 19 | 20 | Sum = _mm256_add_epi32( 21 | Sum, _mm256_i32gather_epi32( 22 | (const std::int32_t*)Key.data(), 23 | _mm256_and_si256(_mm256_srli_epi32(Vector8, 8), _mm256_set1_epi32(0xFF)), 24 | sizeof(std::uint32_t) 25 | ) 26 | ); 27 | Sum = _mm256_add_epi32( 28 | Sum, _mm256_i32gather_epi32( 29 | (const std::int32_t*)Key.data(), 30 | _mm256_and_si256(_mm256_srli_epi32(Vector8, 16), _mm256_set1_epi32(0xFF)), 31 | sizeof(std::uint32_t) 32 | ) 33 | ); 34 | Sum = _mm256_add_epi32( 35 | Sum, 36 | _mm256_i32gather_epi32( 37 | (const std::int32_t*)Key.data(), _mm256_srli_epi32(Vector8, 24), sizeof(std::uint32_t) 38 | ) 39 | ); 40 | return Sum; 41 | } 42 | #endif 43 | 44 | void VirtualPage::DecryptTable(std::uint32_t PageIndex) 45 | { 46 | std::uint32_t PrevData = PageIndex & (~0x1FF); 47 | #if defined(__AVX2__) 48 | __m256i PrevData8 = _mm256_set1_epi32(PrevData); 49 | for( std::size_t i = 0; i < (PageSize / sizeof(std::uint32_t)); i += 8 ) 50 | { 51 | const __m256i CurData8 = _mm256_loadu_si256((__m256i*)(u32 + i)); 52 | // There is no true _mm_alignr_epi8 for AVX2 53 | // An extra _mm256_permute2x128_si256 is needed 54 | PrevData8 = _mm256_alignr_epi8( 55 | CurData8, _mm256_permute2x128_si256(PrevData8, CurData8, _MM_SHUFFLE(0, 2, 0, 1)), 56 | sizeof(std::uint32_t) * 3 57 | ); 58 | __m256i CurPlain8 = _mm256_xor_si256( 59 | _mm256_xor_si256(CurData8, PrevData8), KeySum8(PrevData8, Keys::User) 60 | ); 61 | CurPlain8 = _mm256_shuffle_epi8( 62 | CurPlain8, _mm256_set_epi8( 63 | 13, 12, 15, 14, 9, 8, 11, 10, 5, 4, 7, 6, 1, 0, 3, 2, 13, 12, 15, 14, 9, 64 | 8, 11, 10, 5, 4, 7, 6, 1, 0, 3, 2 65 | ) 66 | ); 67 | _mm256_storeu_si256((__m256i*)(u32 + i), CurPlain8); 68 | PrevData8 = CurData8; 69 | }; 70 | #else 71 | for( std::uint32_t& CurData : u32 ) 72 | { 73 | std::uint32_t X = PrevData ^ CurData; 74 | X ^= Keys::User[(PrevData >> 24) & 0xFF] + Keys::User[(PrevData >> 16) & 0xFF] 75 | + Keys::User[(PrevData >> 8) & 0xFF] + Keys::User[(PrevData >> 0) & 0xFF]; 76 | PrevData = CurData; 77 | // CurData = static_cast((X << 16) | (X >> 16)); 78 | CurData = std::rotl(X, 16); 79 | }; 80 | #endif 81 | } 82 | 83 | void VirtualPage::DecryptData(std::uint32_t PageChecksum) 84 | { 85 | std::uint32_t PrevData = PageChecksum; 86 | #if defined(__AVX2__) 87 | __m256i PrevData8 = _mm256_set1_epi32(PrevData); 88 | for( std::size_t i = 0; i < (PageSize / sizeof(std::uint32_t)); i += 8 ) 89 | { 90 | const __m256i CurData8 = _mm256_loadu_si256((__m256i*)(u32 + i)); 91 | // There is no true _mm_alignr_epi8 for AVX2 92 | // An extra _mm256_permute2x128_si256 is needed 93 | PrevData8 = _mm256_alignr_epi8( 94 | CurData8, _mm256_permute2x128_si256(PrevData8, CurData8, _MM_SHUFFLE(0, 2, 0, 1)), 95 | sizeof(std::uint32_t) * 3 96 | ); 97 | __m256i CurPlain8 = _mm256_sub_epi32( 98 | CurData8, _mm256_xor_si256(PrevData8, KeySum8(PrevData8, Keys::User)) 99 | ); 100 | _mm256_storeu_si256((__m256i*)(u32 + i), CurPlain8); 101 | PrevData8 = CurData8; 102 | }; 103 | #else 104 | for( std::uint32_t& CurData : u32 ) 105 | { 106 | const std::uint32_t LastData = CurData; 107 | CurData -= PrevData 108 | ^ (Keys::User[(PrevData >> 24) & 0xFF] + Keys::User[(PrevData >> 16) & 0xFF] 109 | + Keys::User[(PrevData >> 8) & 0xFF] + Keys::User[(PrevData >> 0) & 0xFF]); 110 | PrevData = LastData; 111 | } 112 | #endif 113 | } 114 | 115 | std::uint32_t VirtualPage::Checksum() 116 | { 117 | std::uint32_t Sum = 0; 118 | for( const std::uint32_t& CurData : u32 ) 119 | { 120 | Sum = std::rotl(Sum, 1) ^ CurData; 121 | } 122 | return Sum | 1; 123 | } 124 | } // namespace sai -------------------------------------------------------------------------------- /source/virtualfilesystem.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2017-2023 Wunkolo 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace sai 9 | { 10 | VirtualFileSystem::VirtualFileSystem(const std::filesystem::path& Path) 11 | : FileStream(std::make_shared(Path)) 12 | { 13 | } 14 | 15 | VirtualFileSystem::~VirtualFileSystem() 16 | { 17 | } 18 | 19 | bool VirtualFileSystem::IsOpen() const 20 | { 21 | return FileStream->is_open(); 22 | } 23 | 24 | bool VirtualFileSystem::Exists(const char* Path) 25 | { 26 | return GetEntry(Path).has_value(); 27 | } 28 | 29 | std::optional VirtualFileSystem::GetEntry(const char* Path) 30 | { 31 | VirtualPage CurPage = {}; 32 | Read(2 * VirtualPage::PageSize, CurPage); 33 | 34 | std::string CurPath(Path); 35 | const char* PathDelim = "./"; 36 | const char* CurToken = std::strtok(&CurPath[0], PathDelim); 37 | std::size_t CurEntry = 0; 38 | std::size_t CurPageIndex = 0; 39 | 40 | while( CurEntry < 64 && CurPage.FATEntries[CurEntry].Flags && CurToken ) 41 | { 42 | if( std::strcmp(CurToken, CurPage.FATEntries[CurEntry].Name) == 0 ) 43 | { 44 | // Match 45 | if( (CurToken = std::strtok(nullptr, PathDelim)) == nullptr ) 46 | { 47 | // No more tokens, done 48 | return std::make_optional( 49 | FileStream, CurPage.FATEntries[CurEntry] 50 | ); 51 | } 52 | // Try to go further 53 | if( CurPage.FATEntries[CurEntry].Type != FATEntry::EntryType::Folder ) 54 | { 55 | // Part of the path was not a folder, cant go further 56 | return std::nullopt; 57 | } 58 | 59 | const std::uint32_t PageIndex = CurPage.FATEntries[CurEntry].PageIndex; 60 | Read(PageIndex * VirtualPage::PageSize, CurPage); 61 | CurEntry = 0; 62 | CurPageIndex = PageIndex; 63 | continue; 64 | } 65 | 66 | // Last entry ( of this Page ), check if there are more after this. 67 | if( CurEntry == 63 && CurPageIndex ) 68 | { 69 | // If a folder has more than 64 `FATEntries`, the `NextPageIndex` 70 | // field on the `PageEntry` will indicate on what `Page` the extra 71 | // entries are located. 72 | VirtualPage TablePage = {}; 73 | Read(VirtualPage::NearestTableIndex(CurPageIndex) * VirtualPage::PageSize, TablePage); 74 | 75 | const std::uint32_t NextPageIndex = TablePage.PageEntries[CurPageIndex].NextPageIndex; 76 | if( NextPageIndex ) 77 | { 78 | Read(NextPageIndex * VirtualPage::PageSize, CurPage); 79 | CurEntry = 0; 80 | CurPageIndex = NextPageIndex; 81 | continue; 82 | } 83 | } 84 | 85 | CurEntry++; 86 | } 87 | 88 | return std::nullopt; 89 | } 90 | 91 | std::size_t VirtualFileSystem::Read(std::size_t Offset, std::span Destination) const 92 | { 93 | FileStream->seekg(Offset, std::ios::beg); 94 | FileStream->read(reinterpret_cast(Destination.data()), Destination.size()); 95 | return Destination.size(); 96 | } 97 | 98 | void VirtualFileSystem::IterateFileSystem(VirtualFileVisitor& Visitor) 99 | { 100 | IterateFATBlock(2, Visitor); 101 | } 102 | 103 | void VirtualFileSystem::IterateFATBlock(std::size_t PageIndex, VirtualFileVisitor& Visitor) 104 | { 105 | VirtualPage CurPage = {}; 106 | Read(PageIndex * VirtualPage::PageSize, CurPage); 107 | 108 | for( const FATEntry& CurFATEntry : CurPage.FATEntries ) 109 | { 110 | if( !CurFATEntry.Flags ) 111 | { 112 | break; 113 | } 114 | 115 | VirtualFileEntry CurEntry(FileStream, CurFATEntry); 116 | switch( CurEntry.GetType() ) 117 | { 118 | case FATEntry::EntryType::File: 119 | { 120 | Visitor.VisitFile(CurEntry); 121 | break; 122 | } 123 | case FATEntry::EntryType::Folder: 124 | { 125 | Visitor.VisitFolderBegin(CurEntry); 126 | IterateFATBlock(CurEntry.GetPageIndex(), Visitor); 127 | Visitor.VisitFolderEnd(CurEntry); 128 | break; 129 | } 130 | } 131 | } 132 | 133 | VirtualPage TablePage = {}; 134 | Read(VirtualPage::NearestTableIndex(PageIndex) * VirtualPage::PageSize, TablePage); 135 | 136 | if( TablePage.PageEntries[PageIndex % VirtualPage::TableSpan].NextPageIndex ) 137 | { 138 | IterateFATBlock( 139 | TablePage.PageEntries[PageIndex % VirtualPage::TableSpan].NextPageIndex, Visitor 140 | ); 141 | } 142 | } 143 | } // namespace sai -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required( VERSION 3.8.0 ) 2 | project( sai ) 3 | 4 | set( CMAKE_CXX_STANDARD 20 ) 5 | set( CMAKE_CXX_STANDARD_REQUIRED ON ) 6 | set( CMAKE_CXX_EXTENSIONS OFF ) 7 | 8 | set( CMAKE_COLOR_MAKEFILE ON ) 9 | set( CMAKE_VERBOSE_MAKEFILE ON ) 10 | set( CMAKE_EXPORT_COMPILE_COMMANDS ON ) 11 | 12 | option( BUILD_SHARED_LIBS "Build using shared libraries" OFF ) 13 | 14 | # This provides standard installation directory variables. 15 | include(GNUInstallDirs) 16 | 17 | # Determine if we're built as a subproject (using add_subdirectory) 18 | # or if this is the master project. 19 | set( MASTER_PROJECT OFF ) 20 | if( CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR ) 21 | set( MASTER_PROJECT ON ) 22 | endif() 23 | 24 | # Create Universal Binary on macOS 25 | set( CMAKE_OSX_ARCHITECTURES "arm64;x86_64" ) 26 | 27 | if( MSVC ) 28 | add_compile_options( 29 | /MP # Parallel builds 30 | /permissive- # Stricter C++ conformance 31 | 32 | # Warnings 33 | /W3 34 | 35 | # Consider these warnings as errors 36 | /we4018 # 'expression': signed/unsigned mismatch 37 | /we4062 # Enumerator 'identifier' in a switch of enum 'enumeration' is not handled 38 | /we4101 # 'identifier': unreferenced local variable 39 | /we4265 # 'class': class has virtual functions, but destructor is not virtual 40 | /we4305 # 'context': truncation from 'type1' to 'type2' 41 | /we4388 # 'expression': signed/unsigned mismatch 42 | /we4389 # 'operator': signed/unsigned mismatch 43 | 44 | /we4456 # Declaration of 'identifier' hides previous local declaration 45 | /we4457 # Declaration of 'identifier' hides function parameter 46 | /we4458 # Declaration of 'identifier' hides class member 47 | /we4459 # Declaration of 'identifier' hides global declaration 48 | 49 | /we4505 # 'function': unreferenced local function has been removed 50 | /we4547 # 'operator': operator before comma has no effect; expected operator with side-effect 51 | /we4549 # 'operator1': operator before comma has no effect; did you intend 'operator2'? 52 | /we4555 # Expression has no effect; expected expression with side-effect 53 | /we4715 # 'function': not all control paths return a value 54 | /we4834 # Discarding return value of function with 'nodiscard' attribute 55 | /we5038 # data member 'member1' will be initialized after data member 'member2' 56 | /we5245 # 'function': unreferenced function with internal linkage has been removed 57 | 58 | ) 59 | elseif( CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang" ) 60 | add_compile_options( 61 | -Wall 62 | -Warray-bounds 63 | -Wextra 64 | -Wimplicit-fallthrough 65 | -Wmissing-declarations 66 | -Wmissing-declarations 67 | -Wmissing-field-initializers 68 | -Wno-attributes 69 | -Wno-invalid-offsetof 70 | -Wno-unused-parameter 71 | -Wreorder 72 | -Wshadow 73 | -Wsign-compare 74 | -Wswitch 75 | -Wuninitialized 76 | -Wunused-function 77 | -Wunused-result 78 | -Wunused-variable 79 | ) 80 | endif() 81 | 82 | ### libsai 83 | add_library( 84 | sai 85 | source/document.cpp 86 | source/ifstream.cpp 87 | source/ifstreambuf.cpp 88 | source/keys.cpp 89 | source/sai.cpp 90 | source/virtualfileentry.cpp 91 | source/virtualfilesystem.cpp 92 | source/virtualpage.cpp 93 | ) 94 | target_include_directories( 95 | sai 96 | PUBLIC 97 | include 98 | ) 99 | 100 | if( MASTER_PROJECT ) 101 | ### Decryption sample 102 | add_executable( 103 | Thumbnail 104 | samples/Thumbnail.cpp 105 | samples/stb_image_write.c 106 | ) 107 | 108 | target_link_libraries( 109 | Thumbnail 110 | PRIVATE 111 | sai 112 | ) 113 | 114 | ### Decryption sample 115 | add_executable( 116 | Decrypt 117 | samples/Decrypt.cpp 118 | ) 119 | 120 | target_link_libraries( 121 | Decrypt 122 | PRIVATE 123 | sai 124 | ) 125 | 126 | ### VFS tree sample 127 | add_executable( 128 | Tree 129 | samples/Tree.cpp 130 | ) 131 | 132 | target_link_libraries( 133 | Tree 134 | PRIVATE 135 | sai 136 | ) 137 | 138 | ### Document sample 139 | add_executable( 140 | Document 141 | samples/Document.cpp 142 | samples/stb_image_write.c 143 | ) 144 | 145 | target_link_libraries( 146 | Document 147 | PRIVATE 148 | sai 149 | ) 150 | 151 | install( 152 | TARGETS sai 153 | DESTINATION ${CMAKE_INSTALL_LIBDIR} 154 | ) 155 | 156 | install( 157 | FILES include/sai.hpp 158 | DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} 159 | ) 160 | 161 | endif() 162 | -------------------------------------------------------------------------------- /depreciated/libsai/sai.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace sai 8 | { 9 | /// Prototypes 10 | class VirtualFileSystem; 11 | union VirtualBlock; 12 | 13 | /// File Entry 14 | class VirtualFileEntry 15 | { 16 | friend class VirtualFileSystem; 17 | friend union VirtualBlock; 18 | 19 | public: 20 | VirtualFileEntry(); 21 | VirtualFileEntry(VirtualFileSystem& FileSystem); 22 | ~VirtualFileEntry(); 23 | uint32_t GetFlags() const; 24 | const char* GetName() const; 25 | 26 | enum class EntryType : uint8_t 27 | { 28 | Folder = 0x10, 29 | File = 0x80 30 | }; 31 | 32 | EntryType GetType() const; 33 | size_t GetBlock() const; 34 | size_t GetSize() const; 35 | time_t GetTimeStamp() const; 36 | 37 | size_t Tell() const; 38 | void Seek(size_t Offset); 39 | 40 | bool Read(void* Destination, size_t Size); 41 | 42 | template 43 | inline bool Read(T& Destination) 44 | { 45 | return Read(&Destination, sizeof(T)); 46 | } 47 | 48 | template 49 | inline T Read() 50 | { 51 | T temp; 52 | Read(&temp, sizeof(T)); 53 | return temp; 54 | } 55 | 56 | private: 57 | VirtualFileSystem* FileSystem; 58 | size_t Position; 59 | 60 | #pragma pack(push, 1) 61 | struct FATEntry 62 | { 63 | uint32_t Flags; 64 | char Name[32]; 65 | uint8_t Pad1; 66 | uint8_t Pad2; 67 | EntryType Type; 68 | uint8_t Pad4; 69 | uint32_t Block; 70 | uint32_t Size; 71 | uint64_t TimeStamp; // Windows FILETIME 72 | uint64_t UnknownB; 73 | } Data; 74 | #pragma pack(pop) 75 | }; 76 | 77 | typedef VirtualFileEntry FileEntry; 78 | 79 | /// File System Visitor 80 | class VFSVisitor 81 | { 82 | public: 83 | virtual ~VFSVisitor(){}; 84 | 85 | // Visit a Folder 86 | virtual void VisitFolderBegin(FileEntry& Entry) = 0; 87 | virtual void VisitFolderEnd() = 0; 88 | 89 | // Visit a File 90 | virtual void VisitFile(FileEntry& Entry) = 0; 91 | }; 92 | 93 | typedef VFSVisitor FileSystemVisitor; 94 | 95 | /// File system Block (4096 bytes) 96 | #pragma pack(push, 1) 97 | union VirtualBlock 98 | { 99 | static const size_t BlockSize = 0x1000; 100 | // Decryption key 101 | static const uint32_t DecryptionKey[256]; 102 | 103 | // Data 104 | uint8_t u8[4096]; 105 | uint32_t u32[1024]; 106 | 107 | // Block Table entries 108 | struct TableEntry 109 | { 110 | uint32_t Checksum; 111 | uint32_t Flags; 112 | } TableEntries[512]; 113 | 114 | // File allocation Entries 115 | FileEntry::FATEntry FATEntries[64]; 116 | 117 | void DecryptTable(uint32_t Index); 118 | void DecryptData(uint32_t Key); 119 | 120 | uint32_t Checksum(bool Table = false); 121 | }; 122 | #pragma pack(pop) 123 | 124 | typedef VirtualBlock FileSystemBlock; 125 | 126 | /// Canvas Visitor 127 | class CanvasVisitor 128 | { 129 | public: 130 | virtual ~CanvasVisitor(){}; 131 | 132 | // Ran before and after a canvas is being iterated 133 | virtual void VisitCanvasBegin(size_t Width, size_t Height) = 0; 134 | virtual void VisitCanvasEnd() = 0; 135 | 136 | // Visit a Layer folder/set 137 | virtual void VisitFolderBegin(const char* FolderName, bool Open) = 0; 138 | virtual void VisitFolderEnd() = 0; 139 | 140 | // Visit Layer 141 | virtual void VisitLayer(const char* LayerName) = 0; 142 | 143 | // Visit Lineart 144 | virtual void VisitLineart(const char* LayerName) = 0; 145 | }; 146 | 147 | /// File System 148 | class VirtualFileSystem 149 | { 150 | public: 151 | VirtualFileSystem(); 152 | 153 | /// Noncopyable 154 | VirtualFileSystem(const VirtualFileSystem&) = delete; 155 | VirtualFileSystem& operator=(const VirtualFileSystem&) = delete; 156 | 157 | ~VirtualFileSystem(); 158 | 159 | bool Mount(const char* FileName); 160 | 161 | size_t GetBlockCount() const; 162 | 163 | size_t GetSize() const; 164 | 165 | bool GetEntry(const char* Path, FileEntry& Entry); 166 | 167 | bool Read(size_t Offset, size_t Size, void* Destination); 168 | 169 | template 170 | inline bool Read(size_t Offset, T& Data) 171 | { 172 | return Read(Offset, sizeof(T), &Data); 173 | } 174 | 175 | /// Iterators 176 | void IterateFileSystem(FileSystemVisitor& Visitor); 177 | void IterateCanvas(CanvasVisitor& Visitor); 178 | 179 | private: 180 | void VisitBlock(size_t BlockNumber, FileSystemVisitor& Visitor); 181 | bool GetBlock(size_t BlockNum, FileSystemBlock* Block); 182 | 183 | /// Current VFS context 184 | size_t BlockCount; 185 | std::ifstream FileStream; 186 | 187 | // Block Caching 188 | intmax_t CacheTableNum = -1; 189 | std::unique_ptr CacheTable; 190 | std::unique_ptr CacheBuffer; 191 | }; 192 | 193 | typedef VirtualFileSystem FileSystem; 194 | } // namespace sai -------------------------------------------------------------------------------- /source/ifstreambuf.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2017-2023 Wunkolo 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace sai 9 | { 10 | ifstreambuf::ifstreambuf(std::span DecryptionKey) : Key(DecryptionKey) 11 | { 12 | setg(nullptr, nullptr, nullptr); 13 | setp(nullptr, nullptr); 14 | 15 | PageCache = std::make_unique(); 16 | TableCache = std::make_unique(); 17 | } 18 | 19 | ifstreambuf* ifstreambuf::open(const std::filesystem::path& Path) 20 | { 21 | if( is_open() == true ) 22 | { 23 | return nullptr; 24 | } 25 | 26 | FileIn.open(Path, std::ios::binary | std::ios::ate); 27 | 28 | if( FileIn.is_open() == false ) 29 | { 30 | close(); 31 | return nullptr; 32 | } 33 | 34 | const std::ifstream::pos_type FileSize = FileIn.tellg(); 35 | 36 | if( FileSize % VirtualPage::PageSize != 0 ) 37 | { 38 | // File size is not pagealigned 39 | close(); 40 | return nullptr; 41 | } 42 | 43 | PageCount = static_cast(FileSize) / VirtualPage::PageSize; 44 | 45 | seekpos(0); 46 | 47 | return this; 48 | } 49 | 50 | ifstreambuf* ifstreambuf::close() 51 | { 52 | if( FileIn.is_open() ) 53 | { 54 | FileIn.close(); 55 | return this; 56 | } 57 | return nullptr; 58 | } 59 | 60 | bool ifstreambuf::is_open() const 61 | { 62 | return FileIn.is_open(); 63 | } 64 | 65 | std::streambuf::int_type ifstreambuf::underflow() 66 | { 67 | if( FileIn.eof() ) 68 | { 69 | return traits_type::eof(); 70 | } 71 | 72 | if( gptr() == egptr() ) 73 | { 74 | // buffer depleated, get next block 75 | if( seekpos((CurrentPage + 1) * VirtualPage::PageSize) 76 | == std::streampos(std::streamoff(-1)) ) 77 | { 78 | // Seek position error 79 | return traits_type::eof(); 80 | } 81 | } 82 | 83 | return traits_type::to_int_type(*gptr()); 84 | } 85 | 86 | std::streambuf::pos_type ifstreambuf::seekoff( 87 | std::streambuf::off_type Offset, std::ios::seekdir Direction, std::ios::openmode /*Mode*/ 88 | ) 89 | { 90 | std::streambuf::pos_type Position; 91 | 92 | if( Direction == std::ios::beg ) 93 | { 94 | Position = Offset; 95 | } 96 | else if( Direction == std::ios::cur ) 97 | { 98 | Position = (CurrentPage * VirtualPage::PageSize); // Current Page 99 | Position += (gptr() - egptr()); // Offset within page 100 | Position += Offset; 101 | } 102 | else if( Direction == std::ios::end ) 103 | { 104 | Position = (PageCount * VirtualPage::PageSize) + Offset; 105 | } 106 | 107 | return seekpos(Position); 108 | } 109 | 110 | std::streambuf::pos_type 111 | ifstreambuf::seekpos(std::streambuf::pos_type Position, std::ios::openmode Mode) 112 | { 113 | if( Mode & std::ios::in ) 114 | { 115 | CurrentPage = static_cast(Position) / VirtualPage::PageSize; 116 | 117 | if( CurrentPage < PageCount ) 118 | { 119 | if( FetchPage(CurrentPage, &Buffer) ) 120 | { 121 | setg( 122 | reinterpret_cast(Buffer.u8.data()), 123 | reinterpret_cast(Buffer.u8.data()) + (Position % VirtualPage::PageSize), 124 | reinterpret_cast(Buffer.u8.data()) + VirtualPage::PageSize 125 | ); 126 | return true; 127 | } 128 | } 129 | } 130 | setg(nullptr, nullptr, nullptr); 131 | return std::streampos(std::streamoff(-1)); 132 | } 133 | 134 | bool ifstreambuf::FetchPage(std::uint32_t PageIndex, VirtualPage* Dest) 135 | { 136 | if( FileIn.fail() ) 137 | { 138 | return false; 139 | } 140 | 141 | if( VirtualPage::IsTableIndex(PageIndex) ) // Table Block 142 | { 143 | if( PageIndex == TableCacheIndex ) 144 | { 145 | // Cache Hit 146 | if( Dest != nullptr ) 147 | { 148 | std::memcpy(Dest, TableCache.get(), VirtualPage::PageSize); 149 | } 150 | return true; 151 | } 152 | // Cache Miss 153 | // Get table cache 154 | FileIn.seekg(PageIndex * VirtualPage::PageSize, std::ios::beg); 155 | FileIn.read(reinterpret_cast(TableCache.get()), VirtualPage::PageSize); 156 | if( FileIn.fail() ) 157 | { 158 | return false; 159 | } 160 | TableCache.get()->DecryptTable(PageIndex); 161 | TableCacheIndex = PageIndex; 162 | if( Dest != nullptr ) 163 | { 164 | std::memcpy(Dest, TableCache.get(), VirtualPage::PageSize); 165 | } 166 | } 167 | else // Data Block 168 | { 169 | if( PageIndex == PageCacheIndex ) 170 | { 171 | // Cache Hit 172 | if( Dest != nullptr ) 173 | { 174 | std::memcpy(Dest, PageCache.get(), VirtualPage::PageSize); 175 | } 176 | return true; 177 | } 178 | // Prefetch nearest table 179 | // Ensure it is in the cache 180 | const std::uint32_t NearestTable = VirtualPage::NearestTableIndex(PageIndex); 181 | 182 | if( FetchPage(NearestTable, nullptr) == false ) 183 | { 184 | // Failed to fetch table 185 | return false; 186 | } 187 | FileIn.seekg(PageIndex * VirtualPage::PageSize, std::ios::beg); 188 | FileIn.read(reinterpret_cast(PageCache.get()), VirtualPage::PageSize); 189 | if( FileIn.fail() ) 190 | { 191 | return false; 192 | } 193 | PageCache.get()->DecryptData( 194 | TableCache.get()->PageEntries[PageIndex % VirtualPage::TableSpan].Checksum 195 | ); 196 | 197 | if( PageCache.get()->Checksum() 198 | != TableCache.get()->PageEntries[PageIndex % VirtualPage::TableSpan].Checksum ) 199 | { 200 | // Checksum mismatch, file corrupt 201 | return false; 202 | } 203 | 204 | PageCacheIndex = PageIndex; 205 | if( Dest != nullptr ) 206 | { 207 | std::memcpy(Dest, PageCache.get(), VirtualPage::PageSize); 208 | } 209 | } 210 | return true; 211 | } 212 | } // namespace sai -------------------------------------------------------------------------------- /samples/Document.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2017-2023 Wunkolo 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | #include "Benchmark.hpp" 17 | 18 | #include "stb_image_write.h" 19 | 20 | const char* const Help 21 | = "Show .sai document information:\n" 22 | "\tDocument (filenames)\n" 23 | "\tWunkolo - Wunkolo@gmail.com"; 24 | 25 | void ProcessLayerFile(sai::VirtualFileEntry& LayerFile); 26 | std::unique_ptr 27 | ReadRasterLayer(const sai::LayerHeader& LayerHeader, sai::VirtualFileEntry& LayerFile); 28 | 29 | int main(int argc, char* argv[]) 30 | { 31 | if( argc < 2 ) 32 | { 33 | std::puts(Help); 34 | return EXIT_FAILURE; 35 | } 36 | 37 | for( std::size_t i = 1; i < std::size_t(argc); ++i ) 38 | { 39 | sai::Document CurDocument(argv[i]); 40 | 41 | if( !CurDocument.IsOpen() ) 42 | { 43 | std::cout << "Error opening file for reading: " << argv[i] << std::endl; 44 | return EXIT_FAILURE; 45 | } 46 | 47 | const std::tuple CanvasSize = CurDocument.GetCanvasSize(); 48 | std::printf( 49 | "\033[1mWidth: %u Height: %u\033[0m\n", std::get<0>(CanvasSize), std::get<1>(CanvasSize) 50 | ); 51 | 52 | const auto Bench = Benchmark::Run([&CurDocument]() -> void { 53 | CurDocument.IterateLayerFiles([](sai::VirtualFileEntry& LayerFile) { 54 | ProcessLayerFile(LayerFile); 55 | return true; 56 | }); 57 | CurDocument.IterateSubLayerFiles([](sai::VirtualFileEntry& SubLayerFile) { 58 | ProcessLayerFile(SubLayerFile); 59 | return true; 60 | }); 61 | }); 62 | std::printf( 63 | "\033[1mIterated Document of %s in %" PRId64 " ns\033[0m\n", argv[i], Bench.count() 64 | ); 65 | } 66 | return EXIT_SUCCESS; 67 | } 68 | 69 | void ProcessLayerFile(sai::VirtualFileEntry& LayerFile) 70 | { 71 | const sai::LayerHeader LayerHeader = LayerFile.Read(); 72 | 73 | std::printf("\t\033[1m- \033[93m\"%08x\"\033[0m\n", LayerHeader.Identifier); 74 | 75 | char Name[256] = {}; 76 | std::snprintf(Name, 256, "%08x", LayerHeader.Identifier); 77 | 78 | std::printf( 79 | "\t\tBlending: '%c%c%c%c'(0x%08x)\n", (LayerHeader.Blending >> 24) & 0xFF, 80 | (LayerHeader.Blending >> 16) & 0xFF, (LayerHeader.Blending >> 8) & 0xFF, 81 | (LayerHeader.Blending >> 0) & 0xFF, LayerHeader.Blending 82 | ); 83 | 84 | // Read serialization stream 85 | std::uint32_t CurTag; 86 | std::uint32_t CurTagSize; 87 | while( LayerFile.Read(CurTag) && CurTag ) 88 | { 89 | LayerFile.Read(CurTagSize); 90 | switch( CurTag ) 91 | { 92 | case sai::Tag("name"): 93 | { 94 | std::array LayerName = {}; 95 | LayerFile.Read(std::as_writable_bytes(std::span(LayerName))); 96 | std::printf("\t\tName: %.256s\n", LayerName.data()); 97 | break; 98 | } 99 | case sai::Tag("lorg"): 100 | case sai::Tag("pfid"): 101 | case sai::Tag("plid"): 102 | case sai::Tag("lmfl"): 103 | case sai::Tag("fopn"): 104 | case sai::Tag("texn"): 105 | case sai::Tag("texp"): 106 | case sai::Tag("peff"): 107 | case sai::Tag("vmrk"): 108 | default: 109 | { 110 | std::printf( 111 | "\t\tUnhandledTag: '%c%c%c%c'(0x%08x)\n", (CurTag >> 24) & 0xFF, 112 | (CurTag >> 16) & 0xFF, (CurTag >> 8) & 0xFF, (CurTag >> 0) & 0xFF, CurTag 113 | ); 114 | // for any streams that we do not handle, 115 | // we just skip forward in the stream 116 | LayerFile.Seek(LayerFile.Tell() + CurTagSize); 117 | break; 118 | } 119 | } 120 | } 121 | 122 | switch( static_cast(LayerHeader.Type) ) 123 | { 124 | case sai::LayerType::Layer: 125 | { 126 | if( auto LayerPixels = ReadRasterLayer(LayerHeader, LayerFile); LayerPixels ) 127 | { 128 | stbi_write_png( 129 | (std::string(Name) + ".png").c_str(), LayerHeader.Bounds.Width, 130 | LayerHeader.Bounds.Height, 4, LayerPixels.get(), 0 131 | ); 132 | } 133 | break; 134 | } 135 | case sai::LayerType::Unknown4: 136 | case sai::LayerType::Linework: 137 | case sai::LayerType::Mask: 138 | case sai::LayerType::Unknown7: 139 | case sai::LayerType::Set: 140 | default: 141 | break; 142 | } 143 | } 144 | 145 | static void RLEDecompressStride( 146 | std::byte* Destination, const std::byte* Source, std::size_t Stride, std::size_t StrideCount, 147 | std::size_t Channel 148 | ) 149 | { 150 | Destination += Channel; 151 | std::size_t WriteCount = 0; 152 | 153 | while( WriteCount < StrideCount ) 154 | { 155 | std::uint8_t Length = std::to_integer(*Source++); 156 | if( Length == 128 ) // No-op 157 | { 158 | } 159 | else if( Length < 128 ) // Copy 160 | { 161 | // Copy the next Length+1 bytes 162 | Length++; 163 | WriteCount += Length; 164 | while( Length ) 165 | { 166 | *Destination = *Source++; 167 | Destination += Stride; 168 | Length--; 169 | } 170 | } 171 | else if( Length > 128 ) // Repeating byte 172 | { 173 | // Repeat next byte exactly "-Length + 1" times 174 | Length ^= 0xFF; 175 | Length += 2; 176 | WriteCount += Length; 177 | std::byte Value = *Source++; 178 | while( Length ) 179 | { 180 | *Destination = Value; 181 | Destination += Stride; 182 | Length--; 183 | } 184 | } 185 | } 186 | } 187 | 188 | std::unique_ptr 189 | ReadRasterLayer(const sai::LayerHeader& LayerHeader, sai::VirtualFileEntry& LayerFile) 190 | { 191 | const std::size_t TileSize = 32u; 192 | const std::size_t LayerTilesX = LayerHeader.Bounds.Width / TileSize; 193 | const std::size_t LayerTilesY = LayerHeader.Bounds.Height / TileSize; 194 | const auto Index2D = [](std::size_t X, std::size_t Y, std::size_t Stride) -> std::size_t { 195 | return X + (Y * Stride); 196 | }; 197 | // Do not use a std::vector as this is implemented as a specialized 198 | // type that does not implement individual bool values as bytes, but rather 199 | // as packed bits within a word. 200 | 201 | // Read TileMap 202 | std::unique_ptr TileMap = std::make_unique(LayerTilesX * LayerTilesY); 203 | LayerFile.Read({TileMap.get(), LayerTilesX * LayerTilesY}); 204 | 205 | // The resulting raster image data for this layer, RGBA 32bpp interleaved 206 | // Use a vector to ensure that tiles with no data are still initialized 207 | // to #00000000 208 | // Also note that the claim that SystemMax has made involving 16bit color 209 | // depth may actually only be true at run-time. All raster data found in 210 | // files are stored at 8bpc while only some run-time color arithmetic 211 | // converts to 16-bit 212 | std::unique_ptr LayerImage 213 | = std::make_unique(LayerHeader.Bounds.Width * LayerHeader.Bounds.Height); 214 | 215 | // 32 x 32 Tile of B8G8R8A8 pixels 216 | std::array CompressedTile = {}; 217 | std::array DecompressedTile = {}; 218 | 219 | // Iterate 32x32 tile chunks row by row 220 | for( std::size_t y = 0; y < LayerTilesY; ++y ) 221 | { 222 | for( std::size_t x = 0; x < LayerTilesX; ++x ) 223 | { 224 | // Process active Tiles 225 | if( !std::to_integer(TileMap[Index2D(x, y, LayerTilesX)]) ) 226 | continue; 227 | 228 | std::uint8_t CurChannel = 0; 229 | std::uint16_t RLESize = 0; 230 | // Iterate RLE streams for each channel 231 | while( LayerFile.Read(RLESize) == sizeof(std::uint16_t) ) 232 | { 233 | assert(RLESize <= CompressedTile.size()); 234 | if( LayerFile.Read(std::span(CompressedTile).first(RLESize)) != RLESize ) 235 | { 236 | // Error reading RLE stream 237 | break; 238 | } 239 | // Decompress and place into the appropriate interleaved channel 240 | RLEDecompressStride( 241 | DecompressedTile.data(), CompressedTile.data(), sizeof(std::uint32_t), 242 | 0x1000 / sizeof(std::uint32_t), CurChannel 243 | ); 244 | ++CurChannel; 245 | // Skip all other channels besides the RGBA ones we care about 246 | if( CurChannel >= 4 ) 247 | { 248 | for( std::size_t i = 0; i < 4; i++ ) 249 | { 250 | RLESize = LayerFile.Read(); 251 | LayerFile.Seek(LayerFile.Tell() + RLESize); 252 | } 253 | break; 254 | } 255 | } 256 | 257 | // Write 32x32 tile into final image 258 | const std::uint32_t* ImageSource 259 | = reinterpret_cast(DecompressedTile.data()); 260 | // Current 32x32 tile within final image 261 | std::uint32_t* ImageDest 262 | = LayerImage.get() + Index2D(x * TileSize, y * LayerHeader.Bounds.Width, TileSize); 263 | for( std::size_t i = 0; i < (TileSize * TileSize); i++ ) 264 | { 265 | std::uint32_t CurPixel = ImageSource[i]; 266 | /// 267 | // Do any Per-Pixel processing you need to do here 268 | /// 269 | ImageDest[Index2D(i % TileSize, i / TileSize, LayerHeader.Bounds.Width)] = CurPixel; 270 | } 271 | } 272 | } 273 | return LayerImage; 274 | } 275 | -------------------------------------------------------------------------------- /include/sai.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2017-2023 Wunkolo 2 | // SPDX-License-Identifier: MIT 3 | 4 | #pragma once 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace sai 19 | { 20 | 21 | template 22 | inline constexpr std::uint32_t Tag(const char (&TagString)[N]) 23 | { 24 | static_assert(N == 5, "Tag must be 4 characters"); 25 | if constexpr( Endianness == std::endian::big ) 26 | { 27 | return ( 28 | (TagString[3] << 0) | (TagString[2] << 8) | (TagString[1] << 16) | (TagString[0] << 24) 29 | ); 30 | } 31 | else 32 | { 33 | return ( 34 | (TagString[3] << 24) | (TagString[2] << 16) | (TagString[1] << 8) | (TagString[0] << 0) 35 | ); 36 | } 37 | } 38 | 39 | enum class LayerType 40 | { 41 | RootLayer = 0x00, // Canvas pseudo-layer 42 | Layer = 0x03, 43 | Unknown4 = 0x04, 44 | Linework = 0x05, 45 | Mask = 0x06, 46 | Unknown7 = 0x07, 47 | Set = 0x08 48 | }; 49 | 50 | enum class BlendingModes : std::uint32_t 51 | { 52 | PassThrough = Tag("pass"), 53 | Normal = Tag("norm"), 54 | Multiply = Tag("mul "), 55 | Screen = Tag("scrn"), 56 | Overlay = Tag("over"), 57 | Luminosity = Tag("add "), 58 | Shade = Tag("sub "), 59 | LumiShade = Tag("adsb"), 60 | Binary = Tag("cbin") 61 | }; 62 | 63 | #pragma pack(push, 1) 64 | 65 | struct FATEntry 66 | { 67 | enum class EntryType : std::uint8_t 68 | { 69 | Folder = 0x10, 70 | File = 0x80 71 | }; 72 | 73 | std::uint32_t Flags; 74 | char Name[32]; 75 | std::uint8_t Pad1; 76 | std::uint8_t Pad2; 77 | EntryType Type; 78 | std::uint8_t Pad4; 79 | std::uint32_t PageIndex; 80 | std::uint32_t Size; 81 | std::uint64_t TimeStamp; // Windows FILETIME 82 | std::uint64_t UnknownB; 83 | }; 84 | 85 | union VirtualPage 86 | { 87 | static constexpr std::size_t PageSize = 0x1000; 88 | static constexpr std::size_t TableSpan = PageSize / 8; 89 | 90 | static constexpr std::size_t NearestTableIndex(std::size_t PageIndex) 91 | { 92 | return (PageIndex / TableSpan) * TableSpan; 93 | } 94 | static constexpr bool IsTableIndex(std::size_t PageIndex) 95 | { 96 | return (PageIndex % TableSpan) == 0; 97 | } 98 | static constexpr bool IsDataIndex(std::size_t PageIndex) 99 | { 100 | return (PageIndex % TableSpan) != 0; 101 | } 102 | 103 | // Data 104 | std::array u8; 105 | std::array i8; 106 | std::array u32; 107 | std::array i32; 108 | 109 | // Page Table entries 110 | struct PageEntry 111 | { 112 | std::uint32_t Checksum; 113 | std::uint32_t NextPageIndex; 114 | }; 115 | std::array PageEntries; 116 | 117 | void DecryptTable(std::uint32_t PageIndex); 118 | void DecryptData(std::uint32_t PageChecksum); 119 | 120 | // FAT Table Entries 121 | std::array FATEntries; 122 | 123 | /* 124 | To checksum a table be sure to do "u32[0] = 0" first 125 | */ 126 | std::uint32_t Checksum(); 127 | }; 128 | 129 | static_assert( 130 | sizeof(VirtualPage) == VirtualPage::PageSize, "Size of `VirtualPage` does not match PageSize" 131 | ); 132 | 133 | struct ThumbnailHeader 134 | { 135 | std::uint32_t Width; 136 | std::uint32_t Height; 137 | std::uint32_t Magic; // BM32 138 | }; 139 | 140 | using LayerID = std::uint32_t; 141 | 142 | struct LayerReference 143 | { 144 | std::uint32_t Identifier; 145 | std::uint16_t LayerType; 146 | // These all get added and sent as a windows message 0x80CA for some reason 147 | std::uint16_t Unknown; 148 | }; 149 | 150 | struct LayerBounds 151 | { 152 | std::int32_t X; // (X / 32) * 32 153 | std::int32_t Y; // (Y / 32) * 32 154 | std::uint32_t Width; // Width - 31 155 | std::uint32_t Height; // Height - 31 156 | }; 157 | 158 | struct LayerHeader 159 | { 160 | std::uint32_t Type; // LayerType enum 161 | LayerID Identifier; 162 | LayerBounds Bounds; 163 | std::uint32_t Unknown; 164 | std::uint8_t Opacity; 165 | std::uint8_t Visible; 166 | std::uint8_t PreserveOpacity; 167 | std::uint8_t Clipping; 168 | std::uint8_t Unknown4; 169 | std::uint32_t Blending; 170 | }; 171 | 172 | struct LayerTableEntry 173 | { 174 | LayerID Identifier; 175 | std::uint16_t Type; // LayerType enum 176 | std::uint16_t Unknown6; // Gets sent as windows message 0x80CA for some reason 177 | }; 178 | 179 | #pragma pack(pop) 180 | 181 | /* 182 | Symmetric keys for decrupting and encrypting the virtual file system 183 | */ 184 | namespace Keys 185 | { 186 | extern const std::array User; 187 | extern const std::array NotRemoveMe; 188 | extern const std::array LocalState; 189 | extern const std::array System; 190 | } // namespace Keys 191 | 192 | // Streambuf to read from an encrypted file 193 | class ifstreambuf : public std::streambuf 194 | { 195 | public: 196 | explicit ifstreambuf(std::span DecryptionKey = Keys::User); 197 | 198 | // No copy 199 | ifstreambuf(const ifstreambuf&) = delete; 200 | ifstreambuf& operator=(const ifstreambuf&) = delete; 201 | 202 | // Adhere similarly to std::basic_filebuf 203 | ifstreambuf* open(const std::filesystem::path& Path); 204 | ifstreambuf* close(); 205 | bool is_open() const; 206 | 207 | // std::streambuf overrides 208 | virtual std::streambuf::int_type underflow() override; 209 | virtual std::streambuf::pos_type seekoff( 210 | std::streambuf::off_type Offset, std::ios_base::seekdir Direction, 211 | std::ios_base::openmode Mode = std::ios_base::in 212 | ) override; 213 | virtual std::streambuf::pos_type seekpos( 214 | std::streambuf::pos_type Position, std::ios_base::openmode Mode = std::ios_base::in 215 | ) override; 216 | 217 | private: 218 | std::ifstream FileIn; 219 | 220 | VirtualPage Buffer; 221 | 222 | // Decryption Key 223 | std::span Key; 224 | 225 | std::uint32_t CurrentPage = ~0u; 226 | 227 | // Caching 228 | 229 | bool FetchPage(std::uint32_t PageIndex, VirtualPage* Dest); 230 | 231 | std::unique_ptr PageCache = {}; 232 | std::uint32_t PageCacheIndex = ~0u; 233 | 234 | std::unique_ptr TableCache = {}; 235 | std::uint32_t TableCacheIndex = ~0u; 236 | 237 | std::uint32_t PageCount = 0; 238 | }; 239 | 240 | class ifstream : public std::istream 241 | { 242 | public: 243 | explicit ifstream(const std::filesystem::path& Path); 244 | 245 | // Similar to ifstream member functions 246 | void open(const std::filesystem::path& Path) const; 247 | bool is_open() const; 248 | 249 | virtual ~ifstream(); 250 | 251 | private: 252 | }; 253 | 254 | // Forward declarations 255 | 256 | class VirtualFileEntry; 257 | class VirtualFileSystem; 258 | 259 | // Visitors 260 | 261 | class VirtualFileVisitor 262 | { 263 | public: 264 | virtual ~VirtualFileVisitor(); 265 | 266 | // Return false to stop iteration 267 | 268 | virtual bool VisitFolderBegin(VirtualFileEntry&); 269 | 270 | virtual bool VisitFolderEnd(VirtualFileEntry&); 271 | 272 | virtual bool VisitFile(VirtualFileEntry&); 273 | }; 274 | 275 | class VirtualFileSystem 276 | { 277 | public: 278 | explicit VirtualFileSystem(const std::filesystem::path& Path); 279 | ~VirtualFileSystem(); 280 | 281 | // No Copy 282 | VirtualFileSystem(const VirtualFileSystem&) = delete; 283 | VirtualFileSystem& operator=(const VirtualFileSystem&) = delete; 284 | 285 | bool IsOpen() const; 286 | 287 | bool Exists(const char* Path); 288 | 289 | std::optional GetEntry(const char* Path); 290 | 291 | std::size_t Read(std::size_t Offset, std::span Destination) const; 292 | 293 | template 294 | inline std::size_t Read(std::size_t Offset, T& Destination) 295 | { 296 | return Read(Offset, {reinterpret_cast(&Destination), sizeof(T)}); 297 | } 298 | 299 | void IterateFileSystem(VirtualFileVisitor& Visitor); 300 | 301 | private: 302 | void IterateFATBlock(std::size_t Index, VirtualFileVisitor& Visitor); 303 | 304 | std::shared_ptr FileStream; 305 | }; 306 | 307 | class VirtualFileEntry 308 | { 309 | public: 310 | VirtualFileEntry(std::weak_ptr FileSystem, const FATEntry& EntryData); 311 | ~VirtualFileEntry(); 312 | 313 | // No Copy 314 | VirtualFileEntry(const VirtualFileEntry&) = delete; 315 | VirtualFileEntry& operator=(const VirtualFileEntry&) = delete; 316 | 317 | const char* GetName() const; 318 | 319 | FATEntry::EntryType GetType() const; 320 | std::time_t GetTimeStamp() const; 321 | std::size_t GetSize() const; 322 | std::size_t GetPageIndex() const; 323 | 324 | std::size_t Tell() const; 325 | void Seek(std::size_t NewOffset); 326 | 327 | std::size_t Read(std::span Destination); 328 | 329 | template 330 | inline std::size_t Read(T& Destination) 331 | { 332 | return Read({reinterpret_cast(&Destination), sizeof(T)}); 333 | } 334 | 335 | template 336 | inline T Read() 337 | { 338 | T temp; 339 | Read({reinterpret_cast(&temp), sizeof(T)}); 340 | return temp; 341 | } 342 | 343 | FATEntry FATData; 344 | 345 | private: 346 | VirtualPage GetTablePage(std::size_t Offset) const; 347 | 348 | std::weak_ptr FileStream; 349 | 350 | // "Flat" offset within file 351 | std::size_t Offset; 352 | // Index of the page we are currently in 353 | std::size_t PageIndex; 354 | // Offset within the page 355 | std::size_t PageOffset; 356 | }; 357 | 358 | class Document : public VirtualFileSystem 359 | { 360 | public: 361 | explicit Document(const std::filesystem::path& Path); 362 | ~Document(); 363 | 364 | // No Copy 365 | Document(const Document&) = delete; 366 | Document& operator=(const Document&) = delete; 367 | 368 | // Returns (Width, Height) 369 | // Returns (0,0) if an error has occured 370 | std::tuple GetCanvasSize(); 371 | 372 | // Returns (RGBA Pixel Data, Width, Height). 373 | // Returns (null,0,0) if an error has occured. 374 | std::tuple, std::uint32_t, std::uint32_t> GetThumbnail(); 375 | 376 | void IterateLayerFiles(const std::function& LayerProc); 377 | void IterateSubLayerFiles(const std::function& SubLayerProc); 378 | 379 | private: 380 | }; 381 | } // namespace sai 382 | -------------------------------------------------------------------------------- /source/keys.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2017-2023 Wunkolo 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include 5 | 6 | namespace sai 7 | { 8 | namespace Keys 9 | { 10 | const std::array User 11 | = {{0x9913D29E, 0x83F58D3D, 0xD0BE1526, 0x86442EB7, 0x7EC69BFB, 0x89D75F64, 0xFB51B239, 12 | 0xFF097C56, 0xA206EF1E, 0x973D668D, 0xC383770D, 0x1CB4CCEB, 0x36F7108B, 0x40336BCD, 13 | 0x84D123BD, 0xAFEF5DF3, 0x90326747, 0xCBFFA8DD, 0x25B94703, 0xD7C5A4BA, 0xE40A17A0, 14 | 0xEADAE6F2, 0x6B738250, 0x76ECF24A, 0x6F2746CC, 0x9BF95E24, 0x1ECA68C5, 0xE71C5929, 15 | 0x7817E56C, 0x2F99C471, 0x395A32B9, 0x61438343, 0x5E3E4F88, 0x80A9332C, 0x1879C69F, 16 | 0x7A03D354, 0x12E89720, 0xF980448E, 0x03643576, 0x963C1D7B, 0xBBED01D6, 0xC512A6B1, 17 | 0x51CB492B, 0x44BADEC9, 0xB2D54BC1, 0x4E7C2893, 0x1531C9A3, 0x43A32CA5, 0x55B25A87, 18 | 0x70D9FA79, 0xEF5B4AE3, 0x8AE7F495, 0x923A8505, 0x1D92650C, 0xC94A9A5C, 0x27D4BB14, 19 | 0x1372A9F7, 0x0C19A7FE, 0x64FA1A53, 0xF1A2EB6D, 0x9FEB910F, 0x4CE10C4E, 0x20825601, 20 | 0x7DFC98C4, 0xA046C808, 0x8E90E7BE, 0x601DE357, 0xF360F37C, 0x00CD6F77, 0xCC6AB9D4, 21 | 0x24CC4E78, 0xAB1E0BFC, 0x6A8BC585, 0xFD70ABF0, 0xD4A75261, 0x1ABF5834, 0x45DCFE17, 22 | 0x5F67E136, 0x948FD915, 0x65AD9EF5, 0x81AB20E9, 0xD36EAF42, 0x0F7F45C7, 0x1BAE72D9, 23 | 0xBE116AC6, 0xDF58B4D5, 0x3F0B960E, 0xC2613F98, 0xB065F8B0, 0x6259F975, 0xC49AEE84, 24 | 0x29718963, 0x0B6D991D, 0x09CF7A37, 0x692A6DF8, 0x67B68B02, 0x2E10DBC2, 0x6C34E93C, 25 | 0xA84B50A1, 0xAC6FC0BB, 0x5CA6184C, 0x34E46183, 0x42B379A9, 0x79883AB6, 0x08750921, 26 | 0x35AF2B19, 0xF7AA886A, 0x49F281D3, 0xA1768059, 0x14568CFD, 0x8B3625F6, 0x3E1B2D9D, 27 | 0xF60E14CE, 0x1157270A, 0xDB5C7EB3, 0x738A0AFA, 0x19C248E5, 0x590CBD62, 0x7B37C312, 28 | 0xFC00B148, 0xD808CF07, 0xD6BD1C82, 0xBD50F1D8, 0x91DEA3B8, 0xFA86B340, 0xF5DF2A80, 29 | 0x9A7BEA6E, 0x1720B8F1, 0xED94A56B, 0xBF02BE28, 0x0D419FA8, 0x073B4DBC, 0x829E3144, 30 | 0x029F43E1, 0x71E6D51F, 0xA9381F09, 0x583075E0, 0xE398D789, 0xF0E31106, 0x75073EB5, 31 | 0x5704863E, 0x6EF1043B, 0xBC407F33, 0x8DBCFB25, 0x886C8F22, 0x5AF4DD7A, 0x2CEACA35, 32 | 0x8FC969DC, 0x9DB8D6B4, 0xC65EDC2F, 0xE60F9316, 0x0A84519A, 0x3A294011, 0xDCF3063F, 33 | 0x41621623, 0x228CB75B, 0x28E9D166, 0xAE631B7F, 0x06D8C267, 0xDA693C94, 0x54A5E860, 34 | 0x7C2170F4, 0xF2E294CB, 0x5B77A0F9, 0xB91522A6, 0xEC549500, 0x10DD78A7, 0x3823E458, 35 | 0x77D3635A, 0x018E3069, 0xE039D055, 0xD5C341BF, 0x9C2400EA, 0x85C0A1D1, 0x66059C86, 36 | 0x0416FF1A, 0xE27E05C8, 0xB19C4C2D, 0xFE4DF58F, 0xD2F0CE2A, 0x32E013C0, 0xEED637D7, 37 | 0xE9FEC1E8, 0xA4890DCA, 0xF4180313, 0x7291738C, 0xE1B053A2, 0x9801267E, 0x2DA15BDB, 38 | 0xADC4DA4F, 0xCF95D474, 0xC0265781, 0x1F226CED, 0xA7472952, 0x3C5F0273, 0xC152BA68, 39 | 0xDD66F09B, 0x93C7EDCF, 0x4F147404, 0x3193425D, 0x26B5768A, 0x0E683B2E, 0x952FDF30, 40 | 0x2A6BAE46, 0xA3559270, 0xB781D897, 0xEB4ECB51, 0xDE49394D, 0x483F629C, 0x2153845E, 41 | 0xB40D64E2, 0x47DB0ED0, 0x302D8E4B, 0x4BF8125F, 0x2BD2B0AC, 0x3DC836EC, 0xC7871965, 42 | 0xB64C5CDE, 0x9EA8BC27, 0xD1853490, 0x3B42EC6F, 0x63A4FD91, 0xAA289D18, 0x4D2B1E49, 43 | 0xB8A060AD, 0xB5F6C799, 0x6D1F7D1C, 0xBA8DAAE6, 0xE51A0FC3, 0xD94890E7, 0x167DF6D2, 44 | 0x879BCD41, 0x5096AC1B, 0x05ACB5DA, 0x375D24EE, 0x7F2EB6AA, 0xA535F738, 0xCAD0AD10, 45 | 0xF8456E3A, 0x23FD5492, 0xB3745532, 0x53C1A272, 0x469DFCDF, 0xE897BF7D, 0xA6BBE2AE, 46 | 0x68CE38AF, 0x5D783D0B, 0x524F21E4, 0x4A257B31, 0xCE7A07B2, 0x562CE045, 0x33B708A4, 47 | 0x8CEE8AEF, 0xC8FB71FF, 0x74E52FAB, 0xCDB18796}}; 48 | 49 | const std::array NotRemoveMe 50 | = {{0xA0C62B54, 0x0374CB94, 0xB3A53F76, 0x5B772C6B, 0xF2B92931, 0x80F923A9, 0x7A22EF7A, 51 | 0x216C7582, 0xEDFF8B71, 0x8B0C6642, 0xAF81AD2F, 0x8E095A62, 0x02926C0C, 0xDD2F56B9, 52 | 0xA3614155, 0xF9AED6E4, 0x079C3E5E, 0xE6D9E1FD, 0x256F165C, 0x77280767, 0x5D2037A1, 53 | 0x3019B3CE, 0xFC13CC15, 0xF457C85F, 0x728DF4E9, 0x4405AA18, 0x2AE0B950, 0xE847316F, 54 | 0xD69FA172, 0x62F658E2, 0xB0F21F89, 0x8AFB852E, 0x1A3E924A, 0xDBAD0B48, 0x88ECBD5A, 55 | 0xC53FC908, 0x81251757, 0x57D53685, 0x73F463A3, 0x048F4B58, 0xC36A46AC, 0x9A8B6FBD, 56 | 0x35DC9DC1, 0xF76EABF5, 0x9280D935, 0xBFCC93FB, 0x4B2BCA7D, 0x60861DFC, 0x7C548877, 57 | 0x2EA46821, 0x7136998F, 0x5AD45EDF, 0x019BA6EF, 0x6FC598C7, 0x1DF383EC, 0x39BAC06D, 58 | 0x5C3A5B1F, 0x7827FB39, 0x27FCA953, 0x8601E843, 0x6C429623, 0xBA5DC127, 0xCE659075, 59 | 0x48291378, 0x5EDA6B5B, 0xE355AC99, 0xCF8C704D, 0x965E6A29, 0xF5035103, 0x20582702, 60 | 0x1B7909DB, 0xCA974452, 0x7DB20E30, 0x2807326C, 0x2DF56D0E, 0x084E9C41, 0xA42DE39C, 61 | 0x9170A5C3, 0x9DB4F95D, 0x53CA2068, 0x3488FC6E, 0xD1BB7AE8, 0xC61F81C5, 0x310857E5, 62 | 0xEF1694EE, 0xF63067B1, 0x3E621B8B, 0x22523BFF, 0x0D37A4BA, 0xCB83BECA, 0x9BE78691, 63 | 0xB7D84E2C, 0x45A676DD, 0x1F31F636, 0x7FAB97C6, 0x3CA15F33, 0xFA6DB6FE, 0x67DD72DC, 64 | 0x6B8948FA, 0x9849FF4B, 0xBE452E79, 0x38AF6E7F, 0x8FE211A7, 0x941728B4, 0x63217749, 65 | 0x70EF1280, 0x13A9F201, 0xACDB14A2, 0x1184E73A, 0x337E87B5, 0xB6008EB7, 0xC868C43C, 66 | 0x85F7DC83, 0xD35AD519, 0xF87310ED, 0xA7C0D29B, 0x361D2DCF, 0xC1D27C3F, 0x9C78DFE0, 67 | 0x2C4FD8C4, 0x05357D9D, 0x2B398964, 0x182AC610, 0xFD4A3873, 0xE71E6416, 0x842C4A05, 68 | 0x5946F70F, 0xB95FA366, 0x1C0B71CB, 0x50CEFA06, 0xAB9DC211, 0x659ABCAE, 0xD2E17FE7, 69 | 0x581A0365, 0xA61BE0B0, 0xD460B084, 0xE21C5CF9, 0x87B1D460, 0x4DF8CF04, 0x4C1573EA, 70 | 0xCD967432, 0xD58EBA12, 0x5F2E9A3B, 0x6A9955EB, 0x55A391AF, 0xEBC1EED5, 0xB59E8C7C, 71 | 0x1E825946, 0xAA18A04E, 0x6891EDF3, 0x663C542D, 0xC459D37E, 0xC06453BC, 0x460D223E, 72 | 0x1690F8DE, 0xC97580F7, 0xA1F08D4F, 0x56DE4381, 0xEE06B5E3, 0xC2FA05D1, 0x3794B488, 73 | 0xEACD428E, 0x7B2362C2, 0xE97FDE9F, 0xBB4C60D2, 0xE4B3E2AB, 0x74C93909, 0x76AA2FDA, 74 | 0x9F049B7B, 0x93BCDA8A, 0x51BEC790, 0x0FD6E4CC, 0x8972E6AD, 0xBCA70F40, 0x405C2469, 75 | 0x10673486, 0xBD104C97, 0x49381E0D, 0x063B456A, 0x23D02634, 0x43ACEC9E, 0xE50E49F8, 76 | 0x197DBF1B, 0x8DF1BB9A, 0xB46B1CA6, 0xD7E895A5, 0xCC51A217, 0xE1C2F196, 0xDEB533C9, 77 | 0x24FDC58D, 0x32850822, 0x12DF4DA8, 0x90BD3500, 0x97C7F320, 0xDA3450F4, 0x2F534059, 78 | 0xDC7B3D63, 0x95B6CD98, 0x09BF19D6, 0xA5D15DBF, 0x42E47851, 0xF07A021E, 0x9ECB2A3D, 79 | 0xE0C39F38, 0x99714F95, 0x3A5BEA4C, 0xB2C4DD25, 0xB13D47C0, 0xAD418A0B, 0x6DEAB81C, 80 | 0x83EE25F2, 0x3B26AE47, 0xA8B018D3, 0xFF76E5F1, 0xA2ED0461, 0x26119ED8, 0x61EB0A74, 81 | 0x15A2B187, 0x4A93CE2A, 0x7943A707, 0x29E5B744, 0x4E14F02B, 0x0A698424, 0xD9A03AE6, 82 | 0xEC87D7C8, 0xA94021B8, 0x3D95D1CD, 0x6E2415BE, 0x52E3F592, 0x64A83CD9, 0x8263C31D, 83 | 0x41B87EB6, 0x8C50FD1A, 0x47C80CD7, 0xD844008C, 0xB812E9AA, 0x0B983013, 0xFB7C520A, 84 | 0x4F66FEBB, 0x17E982D0, 0x00FE6914, 0xFE0FD028, 0x0C328F93, 0x75021AF6, 0x3FE6AFB2, 85 | 0x7E330DE1, 0xDF8ADB45, 0x14D37B37, 0xD04D06A4, 0x694B0156, 0x0ECF6170, 0xC756EBF0, 86 | 0xF1B76526, 0xF348A8B3, 0xAE0A79A0, 0x54D7B2D4}}; 87 | 88 | const std::array LocalState 89 | = {{0x021CF107, 0xE9253648, 0x8AFBA619, 0x8CF31842, 0xBF40F860, 0xA672F03E, 0xFA2756AC, 90 | 0x927B2E7E, 0x1E37D3C4, 0x7C3A0524, 0x4F284D1B, 0xD8A31E9D, 0xBA73B6E6, 0xF399710D, 91 | 0xBD8B1937, 0x70FFE130, 0x056DAA4A, 0xDC509CA1, 0x07358DFF, 0xDF30A2DC, 0x67E7349F, 92 | 0x49532C31, 0x2393EBAA, 0xE54DF202, 0x3A2C7EC9, 0x98AB13EF, 0x7FA52975, 0x83E4792E, 93 | 0x7485DA08, 0x4A1823A8, 0x77812011, 0x8710BB89, 0x9B4E0C68, 0x64125D8E, 0x5F174A0E, 94 | 0x33EA50E7, 0xA5E168B0, 0x1BD9B944, 0x6D7D8FE0, 0xEE66B84C, 0xF0DB530C, 0xF8B06B72, 95 | 0x97ED7DF8, 0x126E0122, 0x364BED23, 0xA103B75C, 0x3BC844FA, 0xD0946501, 0x4E2F70F1, 96 | 0x79A6F413, 0x60B9E977, 0xC1582F10, 0x759B286A, 0xE723EEF5, 0x8BAC4B39, 0xB074B188, 97 | 0xCC528E64, 0x698700EE, 0x44F9E5BB, 0x7E336153, 0xE2413AFD, 0x91DCE2BE, 0xFDCE9EC1, 98 | 0xCAB2DE4F, 0x46C5A486, 0xA0D630DB, 0x1FCD5FCA, 0xEA110891, 0x3F20C6F9, 0xE8F1B25D, 99 | 0x6EFD10C8, 0x889027AF, 0xF284AF3F, 0x89EE9A61, 0x58AF1421, 0xE41B9269, 0x260C6D71, 100 | 0x5079D96E, 0xD959E465, 0x519CD72C, 0x73B64F5A, 0x40BE5535, 0x78386CBC, 0x0A1A02CF, 101 | 0xDBC126B6, 0xAD02BC8D, 0x22A85BC5, 0xA28ABEC3, 0x5C643952, 0xE35BC9AD, 0xCBDACA63, 102 | 0x4CA076A4, 0x4B6121CB, 0x9500BF7D, 0x6F8E32BF, 0xC06587E5, 0x21FAEF46, 0x9C2AD2F6, 103 | 0x7691D4A2, 0xB13E4687, 0xC7460AD6, 0xDDFE54D5, 0x81F516F3, 0xC60D7438, 0xB9CB3BC7, 104 | 0xC4770D94, 0xF4571240, 0x06862A50, 0x30D343D3, 0x5ACF52B2, 0xACF4E68A, 0x0FC2A59B, 105 | 0xB70AEACD, 0x53AA5E80, 0xCF624E8F, 0xF1214CEB, 0x936072DF, 0x62193F18, 0xF5491CDA, 106 | 0x5D476958, 0xDA7A852D, 0x5B053E12, 0xC5A9F6D0, 0xABD4A7D1, 0xD25E6E82, 0xA4D17314, 107 | 0x2E148C4E, 0x6B9F6399, 0xBC26DB47, 0x8296DDCE, 0x3E71D616, 0x350E4083, 0x2063F503, 108 | 0x167833F2, 0x115CDC5E, 0x4208E715, 0x03A49B66, 0x43A724BA, 0xA3B71B8C, 0x107584AE, 109 | 0xC24AE0C6, 0xB3FC6273, 0x280F3795, 0x1392C5D4, 0xD5BAC762, 0xB46B5A3B, 0xC9480B8B, 110 | 0xC39783FC, 0x17F2935B, 0x9DB482F4, 0xA7E9CC09, 0x553F4734, 0x8DB5C3A3, 0x7195EC7A, 111 | 0xA8518A9A, 0x0CE6CB2A, 0x14D50976, 0x99C077A5, 0x012E1733, 0x94EC3D7C, 0x3D825805, 112 | 0x0E80A920, 0x1D39D1AB, 0xFCD85126, 0x3C7F3C79, 0x7A43780B, 0xB26815D9, 0xAF1F7F1C, 113 | 0xBB8D7C81, 0xAAE5250F, 0x34BC670A, 0x1929C8D2, 0xD6AE9FC0, 0x1AE07506, 0x416F3155, 114 | 0x9EB38698, 0x8F22CF29, 0x04E8065F, 0xE07CFBDE, 0x2AEF90E8, 0x6CAD049C, 0x4DC3A8CC, 115 | 0x597E3596, 0x08562B92, 0x52A21D6F, 0xB6C9881D, 0xFBD75784, 0xF613FC32, 0x54C6F757, 116 | 0x66E2D57B, 0xCD69FE9E, 0x478CA13D, 0x2F5F6428, 0x8E55913C, 0xF9091185, 0x0089E8B3, 117 | 0x1C6A48BD, 0x3844946D, 0x24CC8B6B, 0x6524AC2B, 0xD1F6A0F0, 0x32980E51, 0x8634CE17, 118 | 0xED67417F, 0x250BAEB9, 0x84D2FD1A, 0xEC6C4593, 0x29D0C0B1, 0xEBDF42A9, 0x0D3DCD45, 119 | 0x72BF963A, 0x27F0B590, 0x159D5978, 0x3104ABD7, 0x903B1F27, 0x9F886A56, 0x80540FA6, 120 | 0x18F8AD1F, 0xEF5A9870, 0x85016FC2, 0xC8362D41, 0x6376C497, 0xE1A15C67, 0x6ABD806C, 121 | 0x569AC1E2, 0xFE5D1AF7, 0x61CADF59, 0xCE063874, 0xD4F722DD, 0x37DEC2EC, 0xAE70BDEA, 122 | 0x0B2D99B4, 0x39B895FE, 0x091E9DFB, 0xA9150754, 0x7D1D7A36, 0x9A07B41E, 0x5E8FE3B5, 123 | 0xD34503A0, 0xBE2BFAB7, 0x5742D0A7, 0x48DDBA25, 0x7BE3604D, 0x2D4C66E9, 0xB831FFB8, 124 | 0xF7BBA343, 0x451697E4, 0x2C4FD84B, 0x96B17B00, 0xB5C789E3, 0xFFEBF9ED, 0xD7C4B349, 125 | 0xDE3281D8, 0x689E4904, 0xE683F32F, 0x2B3CB0E1}}; 126 | 127 | const std::array System = {{ 128 | 0x724FB987, 0x4A3E70BE, 0xCA549C50, 0x34E263E1, 0x2D5ED2FF, 0x127F0E11, 0x58A42B78, 0x5F6D14AE, 129 | 0x7E2F745D, 0xC3450384, 0xCFBB15DE, 0xDF0A6D8A, 0xEF2545F3, 0x6D8919DB, 0xBC413C94, 0xCCB0A198, 130 | 0xE42DBBD2, 0x361C0B8C, 0x8359731F, 0x13D61E9F, 0x7505F7CE, 0x271D7957, 0x429C0699, 0xD84EC85F, 131 | 0x953391DD, 0xB25DE567, 0xC1BA2F97, 0x2309B605, 0x69A134D1, 0x14A092F2, 0x681500EF, 0xB90148A7, 132 | 0x01AF398B, 0x16FD5168, 0x9E572161, 0x0F7405E3, 0x56AC576D, 0xF275A349, 0x1E8120C0, 0x4BF64E3A, 133 | 0x5A90E85E, 0xD27BC4F1, 0x3BD2FFB1, 0xD6B40FDC, 0x26EC61CF, 0xF744AD3F, 0xCDE7C548, 0x8AFFE60A, 134 | 0xE382CA47, 0x87DA3E1B, 0x8FA3DB36, 0x5737C7E0, 0xACD8CC17, 0xD0CC3B66, 0xD93D776B, 0x37E5BE2B, 135 | 0xD38A1129, 0x037E81D0, 0x15B15072, 0xA6493052, 0x35BCD4B9, 0xC4538D32, 0xEC66C1D5, 0xA20DF513, 136 | 0x5524EB75, 0x92C10488, 0xDA03D9FD, 0x65168F4B, 0x1902BA24, 0x7439FA7D, 0x1D8CB46F, 0xFBC39389, 137 | 0xC5DF6A58, 0x89E8FB00, 0x50DBE0A1, 0xAAE98AF8, 0x6A7C6C9C, 0x7712D6EC, 0x4030D0CD, 0x6052B585, 138 | 0x6132AA77, 0xEB4A38C3, 0x673AB1E6, 0x1C3C07C6, 0x91EA2C76, 0x7A4C7EA0, 0x10B3DCFC, 0xBE7DF402, 139 | 0x2817D87A, 0x25632264, 0xBD8D02B0, 0xF6D0F8A8, 0xB1ED3AF0, 0xE6C4F1CA, 0x99E028B5, 0xE5D48674, 140 | 0x09CF47B8, 0x9D6EAF0E, 0x0A721AFE, 0xB6109E54, 0x8D642344, 0x9FEFC27C, 0xF0CA520F, 0x2C6BDA7E, 141 | 0x2E9DB06A, 0x97DEFC2E, 0x53C5F0EE, 0xAD4B8C60, 0xE9F36696, 0xA8C68907, 0x70B70A20, 0x3D9F82AA, 142 | 0x7604A595, 0x441A563B, 0x39193D4A, 0x33BF1DC7, 0x31B283FB, 0xA399F25B, 0x642CE39E, 0xF9E3B204, 143 | 0x79A87534, 0x5DBE2943, 0x9813E93E, 0x47864AD6, 0xD420D1BF, 0x24A6C986, 0xFE386EF7, 0xD1B65AB7, 144 | 0x3A96BF2F, 0x006FE1AB, 0x22938E90, 0x78FE7A40, 0x5CE1319B, 0x46F5EEF5, 0xBB064BE4, 0xB7271C22, 145 | 0xC0225D21, 0xFA145B10, 0x7C58BC33, 0xF84654C2, 0xEEF4691E, 0x021BEC16, 0xE16C1737, 0x1BCB2603, 146 | 0x48A2954D, 0xDD56A8FA, 0xB8C8A48D, 0x5277590B, 0x1194E7A9, 0x590F42B4, 0x7B97C0D8, 0x7142B714, 147 | 0xAEDD6BC8, 0xBA116212, 0x6B0E642C, 0xF42ABDC5, 0x6E76AC81, 0xBF348819, 0xCB790C59, 0xDC6718AD, 148 | 0x80471230, 0x84DC985C, 0x2AEE32C1, 0x4D35964F, 0x0C6894AC, 0x3EF2CDE5, 0xB59B37A5, 0x9BC9729D, 149 | 0x186A41AF, 0xEA98A970, 0x21F8A291, 0x5487E2C9, 0xE05F3F42, 0xA523B86E, 0x8C1E4062, 0xA962F6CB, 150 | 0x0D4816E8, 0x9A4DF92D, 0x20439DCC, 0xA0713645, 0x43506FE9, 0xC2EB4651, 0xB4780D6C, 0xAFC29B28, 151 | 0x1FCE5FD4, 0x9C7385D3, 0xCE00E463, 0x38CD997F, 0x452933DA, 0xC9F7DEBA, 0x0840A093, 0xDB287B41, 152 | 0x90E48479, 0x66FC6709, 0x6C884C65, 0x3FB56082, 0xF5B87123, 0xED367D1D, 0x6F0C44F9, 0x8270DD38, 153 | 0x0E314F83, 0x1AE69F35, 0xD5A51FB3, 0xA761A671, 0x850B4DED, 0x06AE0892, 0x5EAA2A06, 0xC7FA80F6, 154 | 0xB0692D4E, 0x81657F8F, 0x948B0980, 0xB3D97C01, 0xFC80C3EA, 0xFF9E53A4, 0x30BD784C, 0xF3AD970C, 155 | 0xA12E9A31, 0x04D37646, 0x072655A3, 0xE8D5F353, 0x4CA98BDF, 0x7391FE56, 0x7D5BEDA6, 0x2BD7650D, 156 | 0x862B5C73, 0x8B60A726, 0x7F8ECB3C, 0x517A49B6, 0xD7B9CF5A, 0x6308D5BC, 0x0B3F68D7, 0x62A7EA15, 157 | 0xC65AFD3D, 0xAB8525B2, 0xA451B308, 0xE7C7AB18, 0x88F91369, 0x1783279A, 0x4F95DF2A, 0x41F158BD, 158 | 0xC8D1CEBB, 0x325CD3E2, 0xF1928739, 0x9355AE8E, 0x2FC05EC4, 0x4E0735E7, 0xDE3B10D9, 0x8E18C61A, 159 | 0xE29AEF25, 0x4984D7A2, 0x051F247B, 0x29AB9055, 0xFD2101F4, 0x96FB2E1C, 0x5BF04327, 0x3C8F1BEB, 160 | }}; 161 | 162 | } // namespace Keys 163 | } // namespace sai -------------------------------------------------------------------------------- /depreciated/libsai/sai.cpp: -------------------------------------------------------------------------------- 1 | #include "sai.hpp" 2 | 3 | #include 4 | #include 5 | 6 | namespace sai 7 | { 8 | /// Internal structures: 9 | #pragma pack(push, 1) 10 | struct LayerTableEntry 11 | { 12 | uint32_t Identifier; 13 | enum class LayerType : uint16_t 14 | { 15 | RootLayer = 0x00, // Parent Canvas layer object 16 | Layer = 0x03, // Regular Layer 17 | Unknown4 = 0x4, // Unknown 18 | Linework = 0x05, // Vector Linework Layer 19 | Mask = 0x06, // Masks applied to any layer object 20 | Unknown7 = 0x07, // Unknown 21 | Set = 0x08 // Layer Folder 22 | } Type; 23 | // These all get added and sent as a windows message 0x80CA for some reason 24 | uint16_t Unknown; 25 | }; 26 | 27 | struct LayerHeader 28 | { 29 | enum class Type : uint32_t 30 | { 31 | RootLayer = 0x00, // Parent Canvas layer object 32 | Layer = 0x03, // Regular Layer 33 | Unknown4 = 0x4, // Unknown 34 | Linework = 0x05, // Vector Linework Layer 35 | Mask = 0x06, // Masks applied to any layer object 36 | Unknown7 = 0x07, // Unknown 37 | Set = 0x08 // Layer Folder 38 | } Type; 39 | uint32_t Identifier; 40 | struct LayerBounds 41 | { 42 | int32_t X; // (X / 32) * 32 43 | int32_t Y; // (Y / 32) * 32 44 | uint32_t Width; // Width - 31 45 | uint32_t Height; // Height - 31 46 | } Bounds; 47 | uint32_t Unknown; 48 | uint8_t Opacity; 49 | uint8_t Visible; 50 | uint8_t PreserveOpacity; 51 | uint8_t Clipping; 52 | uint8_t Unknown4; 53 | char Blending[sizeof(uint32_t)]; 54 | }; 55 | #pragma pack(pop) 56 | 57 | /// File Entry 58 | VirtualFileEntry::VirtualFileEntry() : Position(0), FileSystem(nullptr), Data() 59 | { 60 | } 61 | 62 | VirtualFileEntry::VirtualFileEntry(VirtualFileSystem& FileSystem) 63 | : Position(0), FileSystem(&FileSystem), Data() 64 | { 65 | } 66 | 67 | VirtualFileEntry::~VirtualFileEntry() 68 | { 69 | } 70 | 71 | uint32_t VirtualFileEntry::GetFlags() const 72 | { 73 | return Data.Flags; 74 | } 75 | 76 | const char* VirtualFileEntry::GetName() const 77 | { 78 | return Data.Name; 79 | } 80 | 81 | VirtualFileEntry::EntryType VirtualFileEntry::GetType() const 82 | { 83 | return Data.Type; 84 | } 85 | 86 | size_t VirtualFileEntry::GetBlock() const 87 | { 88 | return static_cast(Data.Block); 89 | } 90 | 91 | size_t VirtualFileEntry::GetSize() const 92 | { 93 | return static_cast(Data.Size); 94 | } 95 | 96 | time_t VirtualFileEntry::GetTimeStamp() const 97 | { 98 | return Data.TimeStamp / 10000000ULL - 11644473600ULL; 99 | } 100 | 101 | size_t VirtualFileEntry::Tell() const 102 | { 103 | return Position; 104 | } 105 | 106 | void VirtualFileEntry::Seek(size_t Offset) 107 | { 108 | Position = Offset; 109 | } 110 | 111 | bool VirtualFileEntry::Read(void* Destination, size_t Size) 112 | { 113 | if( FileSystem ) 114 | { 115 | FileSystem->Read( 116 | (static_cast(Data.Block) * FileSystemBlock::BlockSize) + Position, Size, 117 | Destination 118 | ); 119 | Position += Size; 120 | return true; 121 | } 122 | return false; 123 | } 124 | 125 | /// File System 126 | VirtualFileSystem::VirtualFileSystem() : CacheTable(nullptr), CacheBuffer(nullptr) 127 | { 128 | CacheTable = std::make_unique(); 129 | CacheBuffer = std::make_unique(); 130 | } 131 | 132 | VirtualFileSystem::~VirtualFileSystem() 133 | { 134 | if( FileStream ) 135 | { 136 | FileStream.close(); 137 | } 138 | } 139 | 140 | bool VirtualFileSystem::Mount(const char* FileName) 141 | { 142 | if( FileStream ) 143 | { 144 | FileStream.close(); 145 | } 146 | 147 | FileStream.open(FileName, std::ios::binary | std::ios::ate); 148 | 149 | if( FileStream ) 150 | { 151 | std::ifstream::pos_type FileSize = FileStream.tellg(); 152 | 153 | if( FileSize & 0x1FF ) 154 | { 155 | // File size is not Block-aligned 156 | FileStream.close(); 157 | return false; 158 | } 159 | 160 | BlockCount = static_cast(FileSize) / FileSystemBlock::BlockSize; 161 | 162 | // Verify all Blocks 163 | for( size_t i = 0; i < BlockCount; i++ ) 164 | { 165 | GetBlock(i, CacheBuffer.get()); 166 | if( i & 0x1FF ) // Block is data 167 | { 168 | if( CacheTable->TableEntries[i & 0x1FF].Checksum != CacheBuffer->Checksum(false) ) 169 | { 170 | // Checksum mismatch. Data invalid 171 | FileStream.close(); 172 | return false; 173 | } 174 | } 175 | else // Block is a table 176 | { 177 | if( CacheTable->TableEntries[0].Checksum != CacheTable->Checksum(true) ) 178 | { 179 | // Checksum mismatch. Table invalid 180 | FileStream.close(); 181 | return false; 182 | } 183 | } 184 | } 185 | return true; 186 | } 187 | return false; 188 | } 189 | 190 | size_t VirtualFileSystem::GetBlockCount() const 191 | { 192 | return BlockCount; 193 | } 194 | 195 | size_t VirtualFileSystem::GetSize() const 196 | { 197 | return GetBlockCount() * FileSystemBlock::BlockSize; 198 | } 199 | 200 | bool VirtualFileSystem::GetEntry(const char* Path, FileEntry& Entry) 201 | { 202 | if( FileStream ) 203 | { 204 | GetBlock(2, CacheBuffer.get()); 205 | 206 | std::string CurPath(Path); 207 | 208 | const char* CurToken = std::strtok(&CurPath[0], "./"); 209 | 210 | size_t CurEntry = 0; 211 | while( CurEntry < 64 && CacheBuffer->FATEntries[CurEntry].Flags ) 212 | { 213 | if( std::strcmp(CurToken, CacheBuffer->FATEntries[CurEntry].Name) == 0 ) 214 | { 215 | if( (CurToken = std::strtok(nullptr, "./")) 216 | == nullptr ) // No more tokens to process, done 217 | { 218 | Entry.Data = CacheBuffer->FATEntries[CurEntry]; 219 | Entry.FileSystem = this; 220 | return true; 221 | } 222 | 223 | if( CacheBuffer->FATEntries[CurEntry].Type != VirtualFileEntry::EntryType::Folder ) 224 | { 225 | // Entry is not a folder, cant go further 226 | return false; 227 | } 228 | GetBlock(CacheBuffer->FATEntries[CurEntry].Block, CacheBuffer.get()); 229 | CurEntry = 0; 230 | continue; 231 | } 232 | CurEntry++; 233 | } 234 | } 235 | return false; 236 | } 237 | 238 | bool VirtualFileSystem::Read(size_t Offset, size_t Size, void* Destination) 239 | { 240 | if( FileStream ) 241 | { 242 | uint8_t* WritePoint = reinterpret_cast(Destination); 243 | 244 | while( Size ) 245 | { 246 | size_t CurBlock = Offset / FileSystemBlock::BlockSize; // Nearest Block Offset 247 | size_t CurBlockOffset = Offset % FileSystemBlock::BlockSize; // Offset within Block 248 | size_t CurBlockSize = std::min( 249 | Size, 250 | FileSystemBlock::BlockSize - CurBlockOffset 251 | ); // Size within Block 252 | 253 | // Current Block to read from 254 | GetBlock(CurBlock, CacheBuffer.get()); 255 | 256 | memcpy(WritePoint, CacheBuffer->u8 + CurBlockOffset, CurBlockSize); 257 | 258 | Size -= CurBlockSize; 259 | WritePoint += CurBlockSize; 260 | Offset += CurBlockSize; 261 | CurBlock++; 262 | } 263 | return true; 264 | } 265 | return false; 266 | } 267 | 268 | void VirtualFileSystem::IterateFileSystem(FileSystemVisitor& Visitor) 269 | { 270 | if( FileStream ) 271 | { 272 | VisitBlock(2, Visitor); 273 | } 274 | } 275 | 276 | void VirtualFileSystem::IterateCanvas(CanvasVisitor& Visitor) 277 | { 278 | if( FileStream ) 279 | { 280 | FileEntry Canvas; 281 | if( GetEntry("canvas", Canvas) == false ) 282 | { 283 | return; 284 | } 285 | Canvas.Seek(4); 286 | Visitor.VisitCanvasBegin(Canvas.Read(), Canvas.Read()); 287 | 288 | FileEntry LayerTable; 289 | if( GetEntry("laytbl", LayerTable) == false ) 290 | { 291 | return; 292 | } 293 | 294 | uint32_t LayerCount = LayerTable.Read(); 295 | 296 | LayerTableEntry CurLayer; 297 | 298 | std::stack SetStack; 299 | 300 | for( size_t i = 0; i < LayerCount; i++ ) 301 | { 302 | if( LayerTable.Read(CurLayer) == false ) 303 | { 304 | return; 305 | } 306 | char LayerPath[0xff]; 307 | std::sprintf(LayerPath, "/layers/%08x", CurLayer.Identifier); 308 | 309 | FileEntry LayerFile; 310 | if( GetEntry(LayerPath, LayerFile) == false ) 311 | { 312 | return; 313 | } 314 | 315 | LayerHeader LayerHead; 316 | if( LayerFile.Read(LayerHead) == false ) 317 | { 318 | return; 319 | } 320 | 321 | // Read all tag entries 322 | uint32_t CurTag, CurTagSize; 323 | CurTag = CurTagSize = 0; 324 | 325 | char Name[256]; 326 | int32_t ParentFolder = -1; 327 | uint8_t Open = 0; 328 | 329 | while( LayerFile.Read(CurTag) && CurTag ) 330 | { 331 | LayerFile.Read(CurTagSize); 332 | 333 | switch( CurTag ) 334 | { 335 | case 'name': 336 | { 337 | LayerFile.Read(Name); 338 | break; 339 | } 340 | case 'pfid': // Parent folder ID 341 | { 342 | LayerFile.Read(ParentFolder); 343 | break; 344 | } 345 | case 'fopn': // Folder is open 346 | { 347 | LayerFile.Read(Open); 348 | break; 349 | } 350 | default: 351 | { 352 | printf( 353 | "%c%c%c%c | %u\n", reinterpret_cast(&CurTag)[3], 354 | reinterpret_cast(&CurTag)[2], reinterpret_cast(&CurTag)[1], 355 | reinterpret_cast(&CurTag)[0], CurTagSize 356 | ); 357 | LayerFile.Seek(LayerFile.Tell() + CurTagSize); 358 | break; 359 | } 360 | } 361 | } 362 | 363 | if( LayerHead.Type == LayerHeader::Type::Layer ) 364 | { 365 | while( SetStack.size() && SetStack.top() != ParentFolder ) 366 | { 367 | SetStack.pop(); 368 | Visitor.VisitFolderEnd(); 369 | } 370 | Visitor.VisitLayer(Name); 371 | } 372 | else if( LayerHead.Type == LayerHeader::Type::Linework ) 373 | { 374 | Visitor.VisitLineart(Name); 375 | } 376 | else if( LayerHead.Type == LayerHeader::Type::Set ) 377 | { 378 | Visitor.VisitFolderBegin(Name, Open != 0); 379 | SetStack.push(LayerHead.Identifier); 380 | } 381 | } 382 | 383 | Visitor.VisitCanvasEnd(); 384 | } 385 | } 386 | 387 | void VirtualFileSystem::VisitBlock(size_t BlockNumber, FileSystemVisitor& Visitor) 388 | { 389 | FileSystemBlock CurBlock; 390 | GetBlock(BlockNumber, &CurBlock); 391 | FileEntry CurEntry(*this); 392 | for( size_t i = 0; CurBlock.FATEntries[i].Flags; i++ ) 393 | { 394 | CurEntry.Data = CurBlock.FATEntries[i]; 395 | switch( CurEntry.GetType() ) 396 | { 397 | case FileEntry::EntryType::File: 398 | { 399 | Visitor.VisitFile(CurEntry); 400 | break; 401 | } 402 | case FileEntry::EntryType::Folder: 403 | { 404 | Visitor.VisitFolderBegin(CurEntry); 405 | VisitBlock(CurEntry.GetBlock(), Visitor); 406 | Visitor.VisitFolderEnd(); 407 | break; 408 | } 409 | } 410 | } 411 | } 412 | 413 | bool VirtualFileSystem::GetBlock(size_t BlockNum, FileSystemBlock* Block) 414 | { 415 | if( BlockNum < BlockCount ) 416 | { 417 | if( BlockNum & 0x1FF ) // Block is data 418 | { 419 | size_t NearestTable = BlockNum & ~(0x1FF); 420 | uint32_t Key = 0; 421 | if( CacheTableNum == NearestTable ) // Table Cache Hit 422 | { 423 | Key = CacheTable->TableEntries[BlockNum - NearestTable].Checksum; 424 | } 425 | else // Cache Miss 426 | { 427 | // Read and Decrypt Table 428 | FileStream.seekg(NearestTable * FileSystemBlock::BlockSize); 429 | FileStream.read( 430 | reinterpret_cast(CacheTable->u8), FileSystemBlock::BlockSize 431 | ); 432 | CacheTable->DecryptTable(static_cast(NearestTable)); 433 | Key = CacheTable->TableEntries[BlockNum - NearestTable].Checksum; 434 | } 435 | 436 | // Read and Decrypt Data 437 | FileStream.seekg(BlockNum * FileSystemBlock::BlockSize); 438 | FileStream.read(reinterpret_cast(Block->u8), FileSystemBlock::BlockSize); 439 | Block->DecryptData(Key); 440 | return true; 441 | } 442 | else // Block is a table 443 | { 444 | if( BlockNum == CacheTableNum ) // Cache hit 445 | { 446 | memcpy(Block->u8, CacheTable->u8, FileSystemBlock::BlockSize); 447 | return true; 448 | } 449 | // Read and Decrypt Table 450 | FileStream.seekg(BlockNum * FileSystemBlock::BlockSize); 451 | FileStream.read(reinterpret_cast(CacheTable->u8), FileSystemBlock::BlockSize); 452 | CacheTable->DecryptTable(static_cast(BlockNum)); 453 | CacheTableNum = BlockNum; 454 | memcpy(Block->u8, CacheTable->u8, FileSystemBlock::BlockSize); 455 | return true; 456 | } 457 | } 458 | return false; 459 | } 460 | 461 | // Block 462 | 463 | void VirtualBlock::DecryptTable(uint32_t BlockNumber) 464 | { 465 | BlockNumber &= (~0x1FF); 466 | for( size_t i = 0; i < (BlockSize / sizeof(uint32_t)); i++ ) 467 | { 468 | uint32_t CurCipher = u32[i]; 469 | uint32_t X 470 | = BlockNumber ^ CurCipher 471 | ^ (DecryptionKey[(BlockNumber >> 24) & 0xFF] + DecryptionKey[(BlockNumber >> 16) & 0xFF] 472 | + DecryptionKey[(BlockNumber >> 8) & 0xFF] + DecryptionKey[BlockNumber & 0xFF]); 473 | 474 | u32[i] = static_cast((X << 16) | (X >> 16)); 475 | 476 | BlockNumber = CurCipher; 477 | }; 478 | } 479 | 480 | void VirtualBlock::DecryptData(uint32_t Key) 481 | { 482 | for( size_t i = 0; i < (BlockSize / sizeof(uint32_t)); i++ ) 483 | { 484 | uint32_t CurCipher = u32[i]; 485 | u32[i] = CurCipher 486 | - (Key 487 | ^ (DecryptionKey[Key & 0xFF] + DecryptionKey[(Key >> 8) & 0xFF] 488 | + DecryptionKey[(Key >> 16) & 0xFF] + DecryptionKey[(Key >> 24) & 0xFF])); 489 | Key = CurCipher; 490 | } 491 | } 492 | 493 | uint32_t VirtualBlock::Checksum(bool Table) 494 | { 495 | uint32_t Accumulate = 0; 496 | for( size_t i = (Table ? 1 : 0); i < (BlockSize / sizeof(uint32_t)); i++ ) 497 | { 498 | Accumulate = (2 * Accumulate | (Accumulate >> 31)) ^ u32[i]; 499 | } 500 | return Accumulate | 1; 501 | } 502 | 503 | const uint32_t VirtualBlock::DecryptionKey[256] = { 504 | 0x9913D29E, 0x83F58D3D, 0xD0BE1526, 0x86442EB7, 0x7EC69BFB, 0x89D75F64, 0xFB51B239, 0xFF097C56, 505 | 0xA206EF1E, 0x973D668D, 0xC383770D, 0x1CB4CCEB, 0x36F7108B, 0x40336BCD, 0x84D123BD, 0xAFEF5DF3, 506 | 0x90326747, 0xCBFFA8DD, 0x25B94703, 0xD7C5A4BA, 0xE40A17A0, 0xEADAE6F2, 0x6B738250, 0x76ECF24A, 507 | 0x6F2746CC, 0x9BF95E24, 0x1ECA68C5, 0xE71C5929, 0x7817E56C, 0x2F99C471, 0x395A32B9, 0x61438343, 508 | 0x5E3E4F88, 0x80A9332C, 0x1879C69F, 0x7A03D354, 0x12E89720, 0xF980448E, 0x03643576, 0x963C1D7B, 509 | 0xBBED01D6, 0xC512A6B1, 0x51CB492B, 0x44BADEC9, 0xB2D54BC1, 0x4E7C2893, 0x1531C9A3, 0x43A32CA5, 510 | 0x55B25A87, 0x70D9FA79, 0xEF5B4AE3, 0x8AE7F495, 0x923A8505, 0x1D92650C, 0xC94A9A5C, 0x27D4BB14, 511 | 0x1372A9F7, 0x0C19A7FE, 0x64FA1A53, 0xF1A2EB6D, 0x9FEB910F, 0x4CE10C4E, 0x20825601, 0x7DFC98C4, 512 | 0xA046C808, 0x8E90E7BE, 0x601DE357, 0xF360F37C, 0x00CD6F77, 0xCC6AB9D4, 0x24CC4E78, 0xAB1E0BFC, 513 | 0x6A8BC585, 0xFD70ABF0, 0xD4A75261, 0x1ABF5834, 0x45DCFE17, 0x5F67E136, 0x948FD915, 0x65AD9EF5, 514 | 0x81AB20E9, 0xD36EAF42, 0x0F7F45C7, 0x1BAE72D9, 0xBE116AC6, 0xDF58B4D5, 0x3F0B960E, 0xC2613F98, 515 | 0xB065F8B0, 0x6259F975, 0xC49AEE84, 0x29718963, 0x0B6D991D, 0x09CF7A37, 0x692A6DF8, 0x67B68B02, 516 | 0x2E10DBC2, 0x6C34E93C, 0xA84B50A1, 0xAC6FC0BB, 0x5CA6184C, 0x34E46183, 0x42B379A9, 0x79883AB6, 517 | 0x08750921, 0x35AF2B19, 0xF7AA886A, 0x49F281D3, 0xA1768059, 0x14568CFD, 0x8B3625F6, 0x3E1B2D9D, 518 | 0xF60E14CE, 0x1157270A, 0xDB5C7EB3, 0x738A0AFA, 0x19C248E5, 0x590CBD62, 0x7B37C312, 0xFC00B148, 519 | 0xD808CF07, 0xD6BD1C82, 0xBD50F1D8, 0x91DEA3B8, 0xFA86B340, 0xF5DF2A80, 0x9A7BEA6E, 0x1720B8F1, 520 | 0xED94A56B, 0xBF02BE28, 0x0D419FA8, 0x073B4DBC, 0x829E3144, 0x029F43E1, 0x71E6D51F, 0xA9381F09, 521 | 0x583075E0, 0xE398D789, 0xF0E31106, 0x75073EB5, 0x5704863E, 0x6EF1043B, 0xBC407F33, 0x8DBCFB25, 522 | 0x886C8F22, 0x5AF4DD7A, 0x2CEACA35, 0x8FC969DC, 0x9DB8D6B4, 0xC65EDC2F, 0xE60F9316, 0x0A84519A, 523 | 0x3A294011, 0xDCF3063F, 0x41621623, 0x228CB75B, 0x28E9D166, 0xAE631B7F, 0x06D8C267, 0xDA693C94, 524 | 0x54A5E860, 0x7C2170F4, 0xF2E294CB, 0x5B77A0F9, 0xB91522A6, 0xEC549500, 0x10DD78A7, 0x3823E458, 525 | 0x77D3635A, 0x018E3069, 0xE039D055, 0xD5C341BF, 0x9C2400EA, 0x85C0A1D1, 0x66059C86, 0x0416FF1A, 526 | 0xE27E05C8, 0xB19C4C2D, 0xFE4DF58F, 0xD2F0CE2A, 0x32E013C0, 0xEED637D7, 0xE9FEC1E8, 0xA4890DCA, 527 | 0xF4180313, 0x7291738C, 0xE1B053A2, 0x9801267E, 0x2DA15BDB, 0xADC4DA4F, 0xCF95D474, 0xC0265781, 528 | 0x1F226CED, 0xA7472952, 0x3C5F0273, 0xC152BA68, 0xDD66F09B, 0x93C7EDCF, 0x4F147404, 0x3193425D, 529 | 0x26B5768A, 0x0E683B2E, 0x952FDF30, 0x2A6BAE46, 0xA3559270, 0xB781D897, 0xEB4ECB51, 0xDE49394D, 530 | 0x483F629C, 0x2153845E, 0xB40D64E2, 0x47DB0ED0, 0x302D8E4B, 0x4BF8125F, 0x2BD2B0AC, 0x3DC836EC, 531 | 0xC7871965, 0xB64C5CDE, 0x9EA8BC27, 0xD1853490, 0x3B42EC6F, 0x63A4FD91, 0xAA289D18, 0x4D2B1E49, 532 | 0xB8A060AD, 0xB5F6C799, 0x6D1F7D1C, 0xBA8DAAE6, 0xE51A0FC3, 0xD94890E7, 0x167DF6D2, 0x879BCD41, 533 | 0x5096AC1B, 0x05ACB5DA, 0x375D24EE, 0x7F2EB6AA, 0xA535F738, 0xCAD0AD10, 0xF8456E3A, 0x23FD5492, 534 | 0xB3745532, 0x53C1A272, 0x469DFCDF, 0xE897BF7D, 0xA6BBE2AE, 0x68CE38AF, 0x5D783D0B, 0x524F21E4, 535 | 0x4A257B31, 0xCE7A07B2, 0x562CE045, 0x33B708A4, 0x8CEE8AEF, 0xC8FB71FF, 0x74E52FAB, 0xCDB18796, 536 | }; 537 | } // namespace sai -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | - [Cracking PaintTool Sai documents](#cracking-painttool-sai-documents) 4 | - [Decryption](#decryption) 5 | - [Caching](#caching) 6 | - [File System](#file-system) 7 | - [Folder structure](#folder-structure) 8 | - [Serialization Streams](#serialization-streams) 9 | - [Files](#files) 10 | - [".XXXXXXXXXXXXXXXX"](#xxxxxxxxxxxxxxxx) 11 | - ["canvas"](#canvas) 12 | - ["laytbl" "subtbl"](#laytbl-subtbl) 13 | - ["/layers" "/sublayers"](#layers-sublayers) 14 | - [Raster Layers](#raster-layers) 15 | - [Linework Layers](#linework-layers) 16 | - [Decryption Keys](#decryption-keys) 17 | - [UserKey](#userkey) 18 | - [NotRemoveMe](#notremoveme) 19 | - [LocalState](#localstate) 20 | - [sai.ssd](#saissd) 21 | 22 | 23 | 24 | # Cracking PaintTool Sai documents 25 | 26 | 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. 27 | 28 | > PaintTool SAI Ver.1 29 | > 30 | > ![](https://www.systemax.jp/image/sai_logo.jpg) 31 | > 32 | > `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.` 33 | > 34 | > SYSTEMAX Software Development 35 | > 36 | > Details: 37 | > - Fully digitizer support with pressure. 38 | > - Amazing anti-aliased drawings. 39 | > - Highly accurate composition with 16bit ARGB channels. 40 | > - Simple but powerful user interface, easy to learn. 41 | > - Fully support Intel MMX Technology. 42 | > - Data protection function to avoid abnormal termination such as bugs. 43 | > 44 | > Copyright 1996-2016 SYSTEMAX Software Development 45 | 46 | # Decryption 47 | 48 | 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. 49 | 50 | 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`: 51 | 52 | ```cpp 53 | // Gets the Table-Block index appropriate for the current block index 54 | std::size_t NearestTable(std::size_t BlockIndex) 55 | { 56 | return BlockIndex & ~(0x1FF); 57 | } 58 | // Demonstrating how to quickly determine if a block Index is a data-block or a table-block 59 | bool IsTableBlock(std::size_t BlockIndex) 60 | { 61 | return (BlockIndex & 0x1FF) ? false:true; 62 | } 63 | bool IsDataBlock(std::size_t BlockIndex) 64 | { 65 | return (BlockIndex & 0x1FF) ? true:false; 66 | } 67 | ``` 68 | 69 | 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`. 70 | 71 | `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`. 72 | 73 | ```cpp 74 | // Ensure BlockIndex is a valid Table-Block index 75 | void DecryptTable(std::uint32_t BlockIndex, std::uint32_t* Data) 76 | { 77 | // see "IsTableBlock" above on making sure BlockIndex 78 | // is a table or use: 79 | // BlockNumber &= (~0x1FF); 80 | for( std::size_t i = 0; i < 1024; i++ ) 81 | { 82 | std::uint32_t CurCipher = Data[i]; 83 | std::uint32_t X = BlockIndex ^ CurCipher ^ ( 84 | UserKey[(BlockIndex >> 24) & 0xFF] 85 | + UserKey[(BlockIndex >> 16) & 0xFF] 86 | + UserKey[(BlockIndex >> 8) & 0xFF] 87 | + UserKey[BlockIndex & 0xFF]); 88 | 89 | Data[i] = static_cast((X << 16) | (X >> 16)); 90 | 91 | BlockIndex = CurCipher; 92 | }; 93 | } 94 | 95 | void DecryptData(std::uint32_t Vector, std::uint32_t* Data) 96 | { 97 | for( std::size_t i = 0; i < 1024; i++ ) 98 | { 99 | std::uint32_t CurCipher = Data[i]; 100 | Data[i] = 101 | CurCipher 102 | - (Vector ^ ( 103 | UserKey[Vector & 0xFF] 104 | + UserKey[(Vector >> 8) & 0xFF] 105 | + UserKey[(Vector >> 16) & 0xFF] 106 | + UserKey[(Vector >> 24) & 0xFF])); 107 | Vector = CurCipher; 108 | } 109 | } 110 | ``` 111 | 112 | `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. 113 | 114 | ```cpp 115 | struct TableEntry 116 | { 117 | std::uint32_t Checksum; 118 | std::uint32_t NextBlock; 119 | } TableEntries[512]; 120 | ``` 121 | 122 | ``` 123 | ~ ~ 124 | Table-Block | | 125 | +----------+----------+<----+---------+ 126 | 0 |0xChecksum|0xPrelimin| |XXXX|XXXX| Block 512 127 | Checksum used to+--> 1 |0xChecksum|0xPrelimin| |XXXX|XXXX| 0x200200 128 | decrypt block 513 2 |0xChecksum|0xPrelimin| |XXXX|XXXX| 129 | 3 |0xChecksum|0xPrelimin| +---------+ 130 | 4 |0xChecksum|0xPrelimin| /| | Block 513 131 | 512 entries 5 |0xChecksum|0xPrelimin| / | | 0x200400 132 | 6 |0xChecksum|0xPrelimi.| / | | 133 | 7 |0xChecksum|0xPrelim..| / +---------+ 134 | 8 |0xChecksum|0xPreli...|< | | Block 514 135 | 9 |0xChecksum|0xPrel....| | | 0x200600 136 | 10 |0xChecksu.| | | | 137 | ~ ~ ~ +---------+ 138 | | | 139 | ~ ~ 140 | 141 | ``` 142 | 143 | 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. 144 | 145 | 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. 146 | 147 | ```cpp 148 | // If your block number is a multiple of 512, set `Table` to true. 149 | std::uint32_t Checksum(bool Table, std::uint32_t* Data) 150 | { 151 | std::uint32_t Sum = 0; 152 | for( std::size_t i = (Table ? 1 : 0); i < 1024; i++ ) 153 | { 154 | Sum = ( ( Sum << 1 ) | (Sum >> 31)) ^ Data[i]; 155 | } 156 | return Sum | 1; 157 | } 158 | 159 | // Generic version for both Table-Blocks and Data-Blocks 160 | // Works on tables if you set the first 32-bit integer to 0 before running. 161 | std::uint32_t Checksum(std::uint32_t* Data) 162 | { 163 | std::uint32_t Sum = 0; 164 | for( std::size_t i = 0; i < 1024; i++ ) 165 | { 166 | Sum = ( ( Sum << 1 ) | (Sum >> 31)) ^ Data[i]; 167 | } 168 | return Sum | 1; 169 | } 170 | ``` 171 | 172 | 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. 173 | 174 | ## Caching 175 | 176 | 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`. 177 | 178 | # File System 179 | 180 | 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. 181 | 182 | ```cpp 183 | enum class EntryType : std::uint8_t 184 | { 185 | Folder = 0x10, 186 | File = 0x80 187 | }; 188 | 189 | struct FATEntry 190 | { 191 | std::uint32_t Flags; 192 | char Name[32]; 193 | std::uint8_t Pad1; 194 | std::uint8_t Pad2; 195 | std::uint8_t Type; // EntryType enum 196 | std::uint8_t Pad4; 197 | std::uint32_t Block; 198 | std::uint32_t Size; 199 | // Windows FILETIME structure 200 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724284(v=vs.85).aspx 201 | std::uint64_t TimeStamp; 202 | std::uint64_t UnknownB; 203 | }; 204 | 205 | struct FATBlock 206 | { 207 | FATEntry Entries[64]; 208 | } 209 | ``` 210 | >**Note:** When reading file-data of an FATEntry, files are **not** stored continuously. 211 | > 212 | >`TableBlocks` may intercept the file stream and must be skipped. So when reading filedata you must abstract away table blocks. 213 | >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 214 | > 215 | >So offsets such as: 216 | > 217 | >[0,4096],[2097152,2101248],[4194304,4198400],...,**[TableIndex * 4096,TableIndex * 4096 + 4096]** 218 | > 219 | >must be skipped over 220 | 221 | 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. 222 | 223 | ```cpp 224 | time_t filetime_to_time_t(std::uint64_t Time) 225 | { 226 | return Time / 10000000ULL - 11644473600ULL; 227 | } 228 | ``` 229 | 230 | 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`. 231 | 232 | 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. 233 | 234 | # Folder structure 235 | 236 | 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. 237 | 238 | ``` 239 | /.a1541b366925e034 | 32 bytes | 2016/10/12 03:53:53 240 | /canvas | 56 bytes | 2016/10/12 03:53:53 241 | /laytbl | 60 bytes | 2016/10/12 03:53:53 242 | /layers/ | --- | 2016/10/12 03:53:53 243 | /0000000a | 464007 bytes | 2016/10/12 03:53:53 244 | /00000010 | 452 bytes | 2016/10/12 03:53:53 245 | /0000000e | 361 bytes | 2016/10/12 03:53:53 246 | /00000011 | 373 bytes | 2016/10/12 03:53:53 247 | /00000012 | 373 bytes | 2016/10/12 03:53:53 248 | /0000000f | 538 bytes | 2016/10/12 03:53:53 249 | /0000000b | 82454 bytes | 2016/10/12 03:53:53 250 | /subtbl | 12 bytes | 2016/10/12 03:53:53 251 | /sublayers/ | --- | 2016/10/12 03:53:53 252 | /0000000d | 87213 bytes | 2016/10/12 03:53:53 253 | /thumbnail | 90012 bytes | 2016/10/12 03:53:53 254 | ``` 255 | 256 | the first entry `.a1541b366925e034` will vary in name but will always be the first entry. See [.xxxxxxxxxxxxxxxx](#xxxxxxxxxxxxxxxx) for more info on this file. 257 | 258 | ## Serialization Streams 259 | 260 | Before going into the file formats a specific format of serialization needs to be explained that is found across the internal files. 261 | 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. 262 | 263 | Format of the Serial-Table found within Sai.exe for the `reso` identifier. 264 | ``` 265 | Serialization Table for `reso` identifier 266 | 267 | 0-0x00000004 Serial Entry+-----+------------------------+ 268 | 0x0000014C <-----------+ | | 269 | 1-0x00000002 Serial Entry \ | Size in Bytes | 270 | 0x00000150 <-----------+ \ +------------------------+ 271 | 2-0x00000002 Serial Entry \ | | 272 | 0x00000152 <-----------+ \ | Runtime Offset | 273 | 0x00000000 End +------------------------+ 274 | ``` 275 | `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. 276 | 277 | SYSTEMAX Source code, probably 278 | ```cpp 279 | struct ResData 280 | { 281 | ... 282 | std::uint32_t DPI;//0x14C bytes within some class/struct/etc 283 | std::uint16_t Unknown150; 284 | std::uint16_t Unknown152; 285 | ... 286 | }; 287 | 288 | std::uint32_t ResDataStream[] = 289 | { 290 | sizeof(ResData.DPI), 291 | offsetof(ResData, DPI), 292 | sizeof(ResData.Unknown150), 293 | offsetof(ResData, Unknown152) 294 | }; 295 | ``` 296 | 297 | Output written by the Serial-Table for some arbitrary runtime ResData object 298 | ``` 299 | 6F 73 65 72 08 00 00 00 00 00 48 00 00 00 00 00 300 | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ 301 | +---------+ +---------+ +---------+ +---+ +---+ 302 | `oser` Size Serial Ser. Ser. 303 | Data Data Data 304 | 0 1 2 305 | ``` 306 | 307 | `oser` is the little endian storage of `reso`. In code the identifier `oser` is actually defined as something along the lines of: 308 | ```cpp 309 | const std::uint32_t ResDataMagic = `reso`; 310 | ``` 311 | `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`. 312 | 313 | Sample code for reading a serial stream. 314 | ```cpp 315 | std::uint32_t CurTag; 316 | std::uint32_t CurTagSize; 317 | while( File.Read(CurTag) && CurTag ) 318 | { 319 | File.Read(CurTagSize); 320 | switch( CurTag ) 321 | { 322 | case 'reso': 323 | { 324 | //Handle 'reso' data 325 | File.Read(...); 326 | File.Read(...); 327 | File.Read(...); 328 | break; 329 | } 330 | case 'lyid': 331 | { 332 | //... 333 | break; 334 | } 335 | case 'layr': 336 | { 337 | //... 338 | break; 339 | } 340 | default: 341 | { 342 | // for any streams that we do not handle, 343 | // we just skip forward in the stream 344 | File.Seek(File.Tell() + CurTagSize); 345 | break; 346 | } 347 | } 348 | } 349 | ``` 350 | 351 | Serial streams from here on out will be depicted as an enumeration of the four-byte identifier and the formatted data that it contains. 352 | 353 | # Files 354 | 355 | ## ".XXXXXXXXXXXXXXXX" 356 | 357 | 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. 358 | 359 | Three strings are queried from Windows Management Instrumentation(WMI) first with the query 360 | 361 | ``` 362 | SELECT * FROM Win32_BaseBoard 363 | ``` 364 | 365 | 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: 366 | 367 | ``` 368 | ASUSTeK COMPUTER INC./Z87-DELUXE/130410781704124 369 | ``` 370 | 371 | The machine-identifying hash is then calculated with this from this string. 372 | Within the hash function this null-terminated string is repeated continuously until it fits a 256 byte span. 373 | 374 | ``` 375 | ASUSTeK COMPUTER INC./Z87-DELUXE 376 | /130410781704124\ASUSTeK COMPUTE 377 | R INC./Z87-DELUXE/13041078170412 378 | 4\0ASUSTeK COMPUTER INC./Z87-DELU 379 | XE/130410781704124\0ASUSTeK COMPU 380 | TER INC./Z87-DELUXE/130410781704 381 | 124\0ASUSTeK COMPUTER INC./Z87-DE 382 | LUXE/130410781704124\0ASUSTeK COM 383 | ``` 384 | 385 | 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. 386 | 387 | ```cpp 388 | std::uint64_t MachineHash(const char* MachineIdentifier) 389 | { 390 | std::uint32_t StringBlock[64]; 391 | const char* ReadPoint = MachineIdentifier; 392 | for(std::size_t i = 0; i < 256; i++) 393 | { 394 | reinterpret_cast(StringBlock)[i] = *ReadPoint; 395 | ReadPoint = *ReadPoint ? ++ReadPoint : MachineIdentifier; 396 | } 397 | std::uint32_t UpperHash = 0; 398 | std::uint32_t LowerHash = 0; 399 | std::uint32_t Temp1 = 0; 400 | for(std::size_t i = 0; i < 64; i++) 401 | { 402 | std::uint32_t CurUpper = UpperHash + StringBlock[i % 64]; 403 | std::uint32_t CurLower = LowerHash + StringBlock[(i + 1) % 64]; 404 | for( std::size_t j = 0; j < 4; j++ ) 405 | { 406 | CurUpper = CurLower + ((CurUpper << CurLower) | (CurUpper >> (32 - CurLower))); 407 | CurLower = CurUpper + ((CurLower << CurUpper) | (CurLower >> (32 - CurUpper))); 408 | } 409 | LowerHash = CurLower ^ Temp1; 410 | UpperHash ^= CurUpper; 411 | Temp1 ^= CurLower; 412 | } 413 | return (static_cast(UpperHash) << 32) | LowerHash; 414 | } 415 | ``` 416 | 417 | 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. 418 | 419 | The file itself is only 32 bytes long. 420 | 421 | ```cpp 422 | struct AuthorSystemInfo 423 | { 424 | std::uint32_t BitFlag; // always 0x08000000 425 | std::uint32_t Unknown4; 426 | std::uint64_t DateCreated; // Date Created 427 | std::uint64_t DateModified; // Date Modified 428 | std::uint64_t MachineHash; // Calculated using the above routine 429 | } 430 | ``` 431 | 432 | 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. 433 | 434 | ## "canvas" 435 | 436 | This file contains metadata involving the dimensions of the canvas. The first three integers are a static structure: 437 | 438 | ```cpp 439 | struct CanvasInfo 440 | { 441 | std::uint32_t Unknown0; // Always 0x10(16), possibly bpc or alignment 442 | std::uint32_t Width; 443 | std::uint32_t Height 444 | }; 445 | ``` 446 | 447 | After this, a [Serial Stream](#serialization-streams): 448 | 449 | - `reso` 450 | ```cpp 451 | // 16.16 fixed point integer 452 | std::uint32_t DotsPerInch; 453 | // 0 = pixels, 1 = inch, 2 = cm, 3 = mm 454 | std::uint16_t SizeUnits; 455 | // 0 = pixel/inch, 1 = pixel/cm 456 | std::uint16_t ResolutionUnits; 457 | ``` 458 | 459 | - `wsrc` 460 | Layer marked as the selection source 461 | ```cpp 462 | std::uint32_t SelectionSourceID; 463 | ``` 464 | 465 | - `layr` 466 | ```cpp 467 | std::uint32_t SelectedLayerID; 468 | ``` 469 | 470 | - `lyid` 471 | Seems to be a duplication of `layr` 472 | ```cpp 473 | std::uint32_t SelectedLayerID; 474 | ``` 475 | 476 | ## "laytbl" "subtbl" 477 | 478 | 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. 479 | 480 | 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`. 481 | 482 | ```cpp 483 | enum class LayerType : std::uint16_t 484 | { 485 | Null = 0x00, 486 | Layer = 0x03, // Regular Layer 487 | Unknown4 = 0x4, // Unknown 488 | Linework = 0x05, // Vector Linework Layer 489 | Mask = 0x06, // Masks applied to any layer object 490 | Unknown7 = 0x07, //Unknown 491 | Set = 0x08//Layer Folder 492 | }; 493 | 494 | struct LayerTableEntry 495 | { 496 | std::uint32_t Identifier; 497 | std::uint16_t Type; // LayerType enum 498 | std::uint16_t Unknown6; // Gets sent as windows message 0x80CA for some reason 499 | }; 500 | ``` 501 | 502 | Sample routine: 503 | ```cpp 504 | // First integer is number of layer entires 505 | std::uint32_t LayerCount = File.Read(); 506 | while( LayerCount-- ) // Read each layer entry 507 | { 508 | // Read current layer entry into above structure 509 | LayerTableEntry CurrentLayer = File.Read(); 510 | // Do something with this layer 511 | //... 512 | } 513 | 514 | ``` 515 | --- 516 | ## "/layers" "/sublayers" 517 | 518 | 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. 519 | 520 | ```cpp 521 | enum BlendingModes : std::uint32_t 522 | { 523 | PassThrough = 'pass', 524 | Normal = 'norm', 525 | Multiply = 'mul\0', 526 | Screen = 'scrn', 527 | Overlay = 'over', 528 | Luminosity = 'add\0', 529 | Shade = 'sub\0', 530 | LumiShade = 'adsb', 531 | Binary = 'cbin' 532 | }; 533 | 534 | // Rectangular bounds 535 | // Can be off-canvas or larger than canvas if the user moves 536 | // The layer outside of the "canvas window" without cropping 537 | // similar to photoshop 538 | // 0,0 is top-left corner of image 539 | struct LayerBounds 540 | { 541 | // Can be negative, rounded to nearest multiple of 32 542 | std::int32_t X; 543 | std::int32_t Y; 544 | std::uint32_t Width; 545 | std::uint32_t Height; 546 | }; 547 | 548 | struct LayerHeader 549 | { 550 | std::uint32_t Type; // LayerType enum 551 | std::uint32_t Identifier; 552 | LayerBounds Bounds; 553 | std::uint32_t Unknown18; 554 | std::uint8_t Opacity; 555 | std::uint8_t Visible; 556 | std::uint8_t PreserveOpacity; 557 | std::uint8_t Clipping; 558 | std::uint8_t Unknown1C; 559 | std::uint32_t Blending; // BlendingModes enum 560 | }; 561 | ``` 562 | 563 | Immediately after the `LayerHeader` is a [Serial Stream](#serialization-streams). 564 | 565 | >**Note:** 566 | >Not all streams might be present depending on the type of layer the file is referencing. 567 | >Streams such as `texp` and `peff` may not exist if the layer is a lineart layer or folder 568 | 569 | - `lorg` 570 | 571 | ```cpp 572 | std::uint32_t Unknown0; 573 | std::uint32_t Unknown4; 574 | ``` 575 | 576 | - `name` 577 | 578 | Zero terminated string of the layer's name. 579 | ```cpp 580 | std::uint8_t LayerName[256]; 581 | ``` 582 | - `pfid` 583 | 584 | Parent Set ID. If this layer is a child of a folder this will be a layer identifier of the parent container layer. 585 | ```cpp 586 | std::uint32_t ParentSetID; 587 | ``` 588 | 589 | - `plid` 590 | 591 | 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. 592 | ```cpp 593 | std::uint32_t ParentLayerID; 594 | ``` 595 | 596 | - `lmfl` 597 | 598 | Only appears in mask layers 599 | ```cpp 600 | // 0b01 = Nonzero blending mode? 601 | // 0b10 = Opacity is greater than 0 602 | std::uint32_t Unknown0; // Bitmask, only the bottom two bits are used 603 | ``` 604 | 605 | - `fopn` 606 | 607 | Present only in a layer that is a Set/Folder. 608 | A single `bool` variable for if the folder is expanded within the layers panel or not 609 | ```cpp 610 | std::uint8_t Open; 611 | ``` 612 | 613 | - `texn` 614 | 615 | Name of the overlay-texture assigned to a layer. Ex: `Watercolor A` 616 | Only appears in layers that have an overlay enabled 617 | ```cpp 618 | std::uint8_t TextureName[64]; // UTF16 string 619 | ``` 620 | 621 | - `texp` 622 | 623 | 624 | Options related to the overlay-texture 625 | ```cpp 626 | std::uint16_t TextureScale; 627 | std::uint8_t TextureOpacity; 628 | ``` 629 | 630 | - `peff` 631 | 632 | Options related to the watercolor fringe assigned to a layer 633 | ```cpp 634 | std::uint8_t Enabled; // bool 635 | std::uint8_t Opacity; // 100 636 | std::uint8_t Width; // 1 - 15 637 | ``` 638 | 639 | - `vmrk` 640 | ```cpp 641 | std::uint8_t Unknown0; 642 | ``` 643 | 644 | 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. 645 | 646 | Sample layer file reading procedure 647 | ```cpp 648 | // Read header 649 | LayerHeader CurHeader = LayerFile.Read(LayerHead); 650 | 651 | // Read Serial Stream 652 | std::uint32_t CurTag, CurTagSize; 653 | CurTag = CurTagSize = 0; 654 | 655 | char Name[256]; 656 | 657 | while( LayerFile.Read(CurTag) && CurTag ) 658 | { 659 | LayerFile.Read(CurTagSize); 660 | 661 | switch( CurTag ) 662 | { 663 | case 'name': 664 | { 665 | LayerFile.Read(Name); 666 | break; 667 | } 668 | // any other cases you care for 669 | case 'pfid': // Parent folder ID 670 | { 671 | // ... 672 | break; 673 | } 674 | default: 675 | { 676 | LayerFile.Seek(LayerFile.Tell() + CurTagSize); 677 | break; 678 | } 679 | } 680 | } 681 | 682 | if( CurHeader.Type == LayerType::Layer ) 683 | { 684 | // Read Raster data 685 | } 686 | else if( CurHeader.Type == LayerType::Linework ) 687 | { 688 | // Read Linework data 689 | } 690 | 691 | ``` 692 | 693 | 694 | ## Raster Layers 695 | 696 | 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. 697 | 698 | 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 699 | 700 | 1. First, load in the array of `(LayerWidth / 32) * (LayerHeight / 32)` bytes immediately following the layer's Serial Stream as `BlockMap` 701 | 2. Iterate both Y and X dimensions by `LayerHeight / 32` and `LayerWidth / 32` times respectively 702 | - **Be sure to iterate the Y dimension first, then the X to ensure a row-by-row iteration.** 703 | - Access the the boolean at index `(LayerWidth/32) * Y + X` from `BlockMap` 704 | - If the boolean is true(1) 705 | - Read a 16 bit integer 706 | - 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. 707 | - 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. 708 | - 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 709 | - If zero, no more streams to read. Move on to the next tile by iterating at step 2. 710 | 711 | 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 712 | 713 | Routine for decompressing an RLE stream and placing resulting data into the appropriate interleaved 32bpp 8bpc channel index. 714 | ```cpp 715 | void RLEDecompress32(void* Destination, const std::uint8_t *Source, std::size_t SourceSize, std::size_t IntCount, std::size_t Channel) 716 | { 717 | std::uint8_t *Write = reinterpret_cast(Destination) + Channel; 718 | std::size_t WriteCount = 0; 719 | 720 | while( WriteCount < IntCount ) 721 | { 722 | std::uint8_t Length = *Source++; 723 | if( Length == 128 ) // No-op 724 | { 725 | } 726 | else if( Length < 128 ) // Copy 727 | { 728 | // Copy the next Length+1 bytes 729 | Length++; 730 | WriteCount += Length; 731 | while( Length ) 732 | { 733 | *Write = *Source++; 734 | Write += 4; 735 | Length--; 736 | } 737 | } 738 | else if( Length > 128 ) // Repeating byte 739 | { 740 | // Repeat next byte exactly "-Length + 1" times 741 | Length ^= 0xFF; 742 | Length += 2; 743 | WriteCount += Length; 744 | std::uint8_t Value = *Source++; 745 | while( Length ) 746 | { 747 | *Write = Value; 748 | Write += 4; 749 | Length--; 750 | } 751 | } 752 | } 753 | } 754 | ``` 755 | 756 | ```cpp 757 | 758 | // Read BlockMap 759 | // 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 760 | std::vector BlockMap; 761 | TileData.resize((LayerHead.Bounds.Width / 32) * (LayerHead.Bounds.Height / 32)); 762 | 763 | // Read Block Map 764 | LayerFile.Read(BlockMap.data(), (LayerHead.Bounds.Width / 32) * (LayerHead.Bounds.Height / 32)); 765 | 766 | // the resulting raster image data for this layer, RGBA 32bpp interleaved 767 | // Use a vector to ensure that tiles with no data are still initialized 768 | // to #00000000 769 | // Also note that the claim that SystemMax has made involving 16bit color depth 770 | // may actually only be true at run-time. All raster data found in files are stored at 771 | // 8bpc while only some run-time color arithmetic converts to 16-bit 772 | std::vector LayerImage; 773 | LayerImage.resize(LayerHead.Bounds.Width * LayerHead.Bounds.Height * 4); 774 | 775 | 776 | // iterate 32x32 tile chunks row by row 777 | for( std::size_t y = 0; y < (LayerHead.Bounds.Height / 32); y++ ) 778 | { 779 | for( std::size_t x = 0; x < (LayerHead.Bounds.Width / 32); x++ ) 780 | { 781 | if( BlockMap[(LayerHead.Bounds.Width / 32) * y + x] ) // if tile is active 782 | { 783 | // Decompress Tile 784 | std::array CompressedTile; 785 | 786 | // Aligned memory for simd 787 | alignas(sizeof(__m128i)) std::array DecompressedTile; 788 | 789 | std::uint8_t Channel = 0; 790 | std::uint16_t Size = 0; 791 | while( LayerFile.Read(Size) ) // Get Current RLE stream size 792 | { 793 | LayerFile.Read(CompressedTile.data(), Size); 794 | // decompress and place into the appropriate interleaved channel 795 | RLEDecompress32( 796 | DecompressedTile.data(), 797 | CompressedTile.data(), 798 | Size, 799 | 1024, 800 | Channel 801 | ); 802 | Channel++; // Move on to next channel 803 | if( Channel >= 4 ) // skip all other channels besides the RGBA ones we care about 804 | { 805 | for( std::size_t i = 0; i < 4; i++ ) 806 | { 807 | std::uint16_t Size = LayerFile.Read(); 808 | LayerFile.Seek(LayerFile.Tell() + Size); 809 | } 810 | break; 811 | } 812 | } 813 | 814 | // Current 32x32 tile within final image 815 | std::uint32_t *ImageBlock = reinterpret_cast(LayerImage.data()) + (x * 32) + ((y * LayerHead.Bounds.Width) * 32); 816 | 817 | for( std::size_t i = 0; i < (32 * 32) / 4; i++ ) // Process 4 pixels at a time 818 | { 819 | __m128i QuadPixel = _mm_load_si128( 820 | reinterpret_cast<__m128i*>(DecompressedTile.data()) + i 821 | ); 822 | 823 | // ABGR to ARGB, if you want. 824 | // Do your swizzling here 825 | QuadPixel = _mm_shuffle_epi8( 826 | QuadPixel, 827 | _mm_set_epi8( 828 | 15, 12, 13, 14, 829 | 11, 8, 9, 10, 830 | 7, 4, 5, 6, 831 | 3, 0, 1, 2) 832 | ); 833 | 834 | /// Alpha is pre-multiplied, convert to straight 835 | // Get Alpha into [0.0,1.0] range 836 | __m128 Scale = _mm_div_ps( 837 | _mm_cvtepi32_ps( 838 | _mm_shuffle_epi8( 839 | QuadPixel, 840 | _mm_set_epi8( 841 | -1, -1, -1, 15, 842 | -1, -1, -1, 11, 843 | -1, -1, -1, 7, 844 | -1, -1, -1, 3 845 | ) 846 | ) 847 | ), _mm_set1_ps(255.0f)); 848 | 849 | // Normalize each channel into straight color 850 | for( std::uint8_t i = 0; i < 3; i++ ) 851 | { 852 | __m128i CurChannel = _mm_srli_epi32(QuadPixel, i * 8); 853 | CurChannel = _mm_and_si128(CurChannel, _mm_set1_epi32(0xFF)); 854 | __m128 ChannelFloat = _mm_cvtepi32_ps(CurChannel); 855 | 856 | ChannelFloat = _mm_div_ps(ChannelFloat, _mm_set1_ps(255.0));// [0,255] to [0,1] 857 | ChannelFloat = _mm_div_ps(ChannelFloat, Scale); 858 | ChannelFloat = _mm_mul_ps(ChannelFloat, _mm_set1_ps(255.0));// [0,1] to [0,255] 859 | 860 | CurChannel = _mm_cvtps_epi32(ChannelFloat); 861 | CurChannel = _mm_and_si128(CurChannel, _mm_set1_epi32(0xff)); 862 | CurChannel = _mm_slli_epi32(CurChannel, i * 8); 863 | 864 | QuadPixel = _mm_andnot_si128(_mm_set1_epi32(0xFF << (i * 8)), QuadPixel); 865 | QuadPixel = _mm_or_si128(QuadPixel, CurChannel); 866 | } 867 | 868 | // Write directly to final image 869 | _mm_store_si128( 870 | reinterpret_cast<__m128i*>(ImageBlock) + (i % 8) + ((i / 8) * (LayerHead.Bounds.Width / 4)), 871 | QuadPixel 872 | ); 873 | } 874 | } 875 | } 876 | } 877 | ``` 878 | 879 | --- 880 | 881 | ## Mask Layers 882 | 883 | Mask layers consist of 16bpc grayscale pixels, stored in big endian. They can be read with the same procedure that `raster` data uses. 884 | 885 | 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: 886 | 887 | ```cpp 888 | std::unique_ptr ReadMaskLayer( 889 | const sai::LayerHeader& LayerHeader, sai::VirtualFileEntry& LayerFile 890 | ) 891 | { 892 | const std::size_t TileSize = 32u; 893 | const std::size_t LayerTilesX = LayerHeader.Bounds.Width / TileSize; 894 | const std::size_t LayerTilesY = LayerHeader.Bounds.Height / TileSize; 895 | const auto Index2D = [](std::size_t X, std::size_t Y, std::size_t Stride 896 | ) -> std::size_t { return X + (Y * Stride); }; 897 | // Do not use a std::vector as this is implemented as a specialized 898 | // type that does not implement individual bool values as bytes, but rather 899 | // as packed bits within a word. 900 | 901 | std::unique_ptr TileMap 902 | = std::make_unique(LayerTilesX * LayerTilesY); 903 | LayerFile.Read(TileMap.get(), LayerTilesX * LayerTilesY); 904 | 905 | std::unique_ptr LayerImage 906 | = std::make_unique( 907 | LayerHeader.Bounds.Width * LayerHeader.Bounds.Height 908 | ); 909 | 910 | // 32 x 32 Tile of G8A8 pixels 911 | std::array CompressedTile = {}; 912 | std::array DecompressedTile = {}; 913 | 914 | // Iterate 32x32 tile chunks row by row 915 | for( std::size_t y = 0; y < LayerTilesY; ++y ) 916 | { 917 | for( std::size_t x = 0; x < LayerTilesX; ++x ) 918 | { 919 | // Process active Tiles 920 | if( !TileMap[Index2D(x, y, LayerTilesX)] ) 921 | continue; 922 | 923 | std::uint8_t CurChannel = 0; 924 | std::uint16_t RLESize = 0; 925 | // Iterate RLE streams for each channel 926 | while( LayerFile.Read(RLESize) 927 | == sizeof(std::uint16_t) ) 928 | { 929 | assert(RLESize <= CompressedTile.size()); 930 | if( LayerFile.Read(CompressedTile.data(), RLESize) != RLESize ) 931 | { 932 | // Error reading RLE stream 933 | break; 934 | } 935 | // Decompress and place into the appropriate interleaved channel 936 | RLEDecompressStride( 937 | DecompressedTile.data(), CompressedTile.data(), 938 | sizeof(std::int16_t), 0x1000 / sizeof(std::uint32_t), 939 | CurChannel 940 | ); 941 | ++CurChannel; 942 | if( CurChannel == 2 ) 943 | { 944 | break; 945 | } 946 | } 947 | 948 | // Write 32x32 tile into final image 949 | const std::int16_t* ImageSource 950 | = reinterpret_cast(DecompressedTile.data() 951 | ); 952 | // Current 32x32 tile within final image 953 | std::int16_t* ImageDest 954 | = LayerImage.get() 955 | + Index2D(x * TileSize, y * LayerHeader.Bounds.Width, TileSize); 956 | for( std::size_t i = 0; i < (TileSize * TileSize); i++ ) 957 | { 958 | std::int16_t CurPixel = ImageSource[i]; 959 | /// 960 | // Do any Per-Pixel processing you need to do here 961 | /// 962 | ImageDest[Index2D( 963 | i % TileSize, i / TileSize, LayerHeader.Bounds.Width 964 | )] = CurPixel; 965 | } 966 | } 967 | } 968 | return LayerImage; 969 | } 970 | ``` 971 | 972 | ### LayerType::Unknown4 and LayerType::Unknown7 973 | 974 | 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 ). 975 | 976 | --- 977 | 978 | ## Linework Layers 979 | 980 | Todo 981 | 982 | --- 983 | # Decryption Keys 984 | 985 | ## UserKey 986 | This is the key that we care for. Used to encrypt/decrypt all user-created files. 987 | Decrypts `.sai` files. 988 | 989 | ```cpp 990 | const std::uint32_t UserKey[256] = 991 | { 992 | 0x9913D29E,0x83F58D3D,0xD0BE1526,0x86442EB7,0x7EC69BFB,0x89D75F64,0xFB51B239,0xFF097C56, 993 | 0xA206EF1E,0x973D668D,0xC383770D,0x1CB4CCEB,0x36F7108B,0x40336BCD,0x84D123BD,0xAFEF5DF3, 994 | 0x90326747,0xCBFFA8DD,0x25B94703,0xD7C5A4BA,0xE40A17A0,0xEADAE6F2,0x6B738250,0x76ECF24A, 995 | 0x6F2746CC,0x9BF95E24,0x1ECA68C5,0xE71C5929,0x7817E56C,0x2F99C471,0x395A32B9,0x61438343, 996 | 0x5E3E4F88,0x80A9332C,0x1879C69F,0x7A03D354,0x12E89720,0xF980448E,0x03643576,0x963C1D7B, 997 | 0xBBED01D6,0xC512A6B1,0x51CB492B,0x44BADEC9,0xB2D54BC1,0x4E7C2893,0x1531C9A3,0x43A32CA5, 998 | 0x55B25A87,0x70D9FA79,0xEF5B4AE3,0x8AE7F495,0x923A8505,0x1D92650C,0xC94A9A5C,0x27D4BB14, 999 | 0x1372A9F7,0x0C19A7FE,0x64FA1A53,0xF1A2EB6D,0x9FEB910F,0x4CE10C4E,0x20825601,0x7DFC98C4, 1000 | 0xA046C808,0x8E90E7BE,0x601DE357,0xF360F37C,0x00CD6F77,0xCC6AB9D4,0x24CC4E78,0xAB1E0BFC, 1001 | 0x6A8BC585,0xFD70ABF0,0xD4A75261,0x1ABF5834,0x45DCFE17,0x5F67E136,0x948FD915,0x65AD9EF5, 1002 | 0x81AB20E9,0xD36EAF42,0x0F7F45C7,0x1BAE72D9,0xBE116AC6,0xDF58B4D5,0x3F0B960E,0xC2613F98, 1003 | 0xB065F8B0,0x6259F975,0xC49AEE84,0x29718963,0x0B6D991D,0x09CF7A37,0x692A6DF8,0x67B68B02, 1004 | 0x2E10DBC2,0x6C34E93C,0xA84B50A1,0xAC6FC0BB,0x5CA6184C,0x34E46183,0x42B379A9,0x79883AB6, 1005 | 0x08750921,0x35AF2B19,0xF7AA886A,0x49F281D3,0xA1768059,0x14568CFD,0x8B3625F6,0x3E1B2D9D, 1006 | 0xF60E14CE,0x1157270A,0xDB5C7EB3,0x738A0AFA,0x19C248E5,0x590CBD62,0x7B37C312,0xFC00B148, 1007 | 0xD808CF07,0xD6BD1C82,0xBD50F1D8,0x91DEA3B8,0xFA86B340,0xF5DF2A80,0x9A7BEA6E,0x1720B8F1, 1008 | 0xED94A56B,0xBF02BE28,0x0D419FA8,0x073B4DBC,0x829E3144,0x029F43E1,0x71E6D51F,0xA9381F09, 1009 | 0x583075E0,0xE398D789,0xF0E31106,0x75073EB5,0x5704863E,0x6EF1043B,0xBC407F33,0x8DBCFB25, 1010 | 0x886C8F22,0x5AF4DD7A,0x2CEACA35,0x8FC969DC,0x9DB8D6B4,0xC65EDC2F,0xE60F9316,0x0A84519A, 1011 | 0x3A294011,0xDCF3063F,0x41621623,0x228CB75B,0x28E9D166,0xAE631B7F,0x06D8C267,0xDA693C94, 1012 | 0x54A5E860,0x7C2170F4,0xF2E294CB,0x5B77A0F9,0xB91522A6,0xEC549500,0x10DD78A7,0x3823E458, 1013 | 0x77D3635A,0x018E3069,0xE039D055,0xD5C341BF,0x9C2400EA,0x85C0A1D1,0x66059C86,0x0416FF1A, 1014 | 0xE27E05C8,0xB19C4C2D,0xFE4DF58F,0xD2F0CE2A,0x32E013C0,0xEED637D7,0xE9FEC1E8,0xA4890DCA, 1015 | 0xF4180313,0x7291738C,0xE1B053A2,0x9801267E,0x2DA15BDB,0xADC4DA4F,0xCF95D474,0xC0265781, 1016 | 0x1F226CED,0xA7472952,0x3C5F0273,0xC152BA68,0xDD66F09B,0x93C7EDCF,0x4F147404,0x3193425D, 1017 | 0x26B5768A,0x0E683B2E,0x952FDF30,0x2A6BAE46,0xA3559270,0xB781D897,0xEB4ECB51,0xDE49394D, 1018 | 0x483F629C,0x2153845E,0xB40D64E2,0x47DB0ED0,0x302D8E4B,0x4BF8125F,0x2BD2B0AC,0x3DC836EC, 1019 | 0xC7871965,0xB64C5CDE,0x9EA8BC27,0xD1853490,0x3B42EC6F,0x63A4FD91,0xAA289D18,0x4D2B1E49, 1020 | 0xB8A060AD,0xB5F6C799,0x6D1F7D1C,0xBA8DAAE6,0xE51A0FC3,0xD94890E7,0x167DF6D2,0x879BCD41, 1021 | 0x5096AC1B,0x05ACB5DA,0x375D24EE,0x7F2EB6AA,0xA535F738,0xCAD0AD10,0xF8456E3A,0x23FD5492, 1022 | 0xB3745532,0x53C1A272,0x469DFCDF,0xE897BF7D,0xA6BBE2AE,0x68CE38AF,0x5D783D0B,0x524F21E4, 1023 | 0x4A257B31,0xCE7A07B2,0x562CE045,0x33B708A4,0x8CEE8AEF,0xC8FB71FF,0x74E52FAB,0xCDB18796 1024 | }; 1025 | ``` 1026 | 1027 | ## NotRemoveMe 1028 | Seems to only be used for the `Notremoveme.ssd` file located in `"C:\ProgramData\SYSTEMAX Software Development\SAI"` 1029 | 1030 | Appears to contain log data similar to `sai.ssd` 1031 | 1032 | ```cpp 1033 | const std::uint32_t NotRemoveMeKey[256] = 1034 | { 1035 | 0xA0C62B54,0x0374CB94,0xB3A53F76,0x5B772C6B,0xF2B92931,0x80F923A9,0x7A22EF7A,0x216C7582, 1036 | 0xEDFF8B71,0x8B0C6642,0xAF81AD2F,0x8E095A62,0x02926C0C,0xDD2F56B9,0xA3614155,0xF9AED6E4, 1037 | 0x079C3E5E,0xE6D9E1FD,0x256F165C,0x77280767,0x5D2037A1,0x3019B3CE,0xFC13CC15,0xF457C85F, 1038 | 0x728DF4E9,0x4405AA18,0x2AE0B950,0xE847316F,0xD69FA172,0x62F658E2,0xB0F21F89,0x8AFB852E, 1039 | 0x1A3E924A,0xDBAD0B48,0x88ECBD5A,0xC53FC908,0x81251757,0x57D53685,0x73F463A3,0x048F4B58, 1040 | 0xC36A46AC,0x9A8B6FBD,0x35DC9DC1,0xF76EABF5,0x9280D935,0xBFCC93FB,0x4B2BCA7D,0x60861DFC, 1041 | 0x7C548877,0x2EA46821,0x7136998F,0x5AD45EDF,0x019BA6EF,0x6FC598C7,0x1DF383EC,0x39BAC06D, 1042 | 0x5C3A5B1F,0x7827FB39,0x27FCA953,0x8601E843,0x6C429623,0xBA5DC127,0xCE659075,0x48291378, 1043 | 0x5EDA6B5B,0xE355AC99,0xCF8C704D,0x965E6A29,0xF5035103,0x20582702,0x1B7909DB,0xCA974452, 1044 | 0x7DB20E30,0x2807326C,0x2DF56D0E,0x084E9C41,0xA42DE39C,0x9170A5C3,0x9DB4F95D,0x53CA2068, 1045 | 0x3488FC6E,0xD1BB7AE8,0xC61F81C5,0x310857E5,0xEF1694EE,0xF63067B1,0x3E621B8B,0x22523BFF, 1046 | 0x0D37A4BA,0xCB83BECA,0x9BE78691,0xB7D84E2C,0x45A676DD,0x1F31F636,0x7FAB97C6,0x3CA15F33, 1047 | 0xFA6DB6FE,0x67DD72DC,0x6B8948FA,0x9849FF4B,0xBE452E79,0x38AF6E7F,0x8FE211A7,0x941728B4, 1048 | 0x63217749,0x70EF1280,0x13A9F201,0xACDB14A2,0x1184E73A,0x337E87B5,0xB6008EB7,0xC868C43C, 1049 | 0x85F7DC83,0xD35AD519,0xF87310ED,0xA7C0D29B,0x361D2DCF,0xC1D27C3F,0x9C78DFE0,0x2C4FD8C4, 1050 | 0x05357D9D,0x2B398964,0x182AC610,0xFD4A3873,0xE71E6416,0x842C4A05,0x5946F70F,0xB95FA366, 1051 | 0x1C0B71CB,0x50CEFA06,0xAB9DC211,0x659ABCAE,0xD2E17FE7,0x581A0365,0xA61BE0B0,0xD460B084, 1052 | 0xE21C5CF9,0x87B1D460,0x4DF8CF04,0x4C1573EA,0xCD967432,0xD58EBA12,0x5F2E9A3B,0x6A9955EB, 1053 | 0x55A391AF,0xEBC1EED5,0xB59E8C7C,0x1E825946,0xAA18A04E,0x6891EDF3,0x663C542D,0xC459D37E, 1054 | 0xC06453BC,0x460D223E,0x1690F8DE,0xC97580F7,0xA1F08D4F,0x56DE4381,0xEE06B5E3,0xC2FA05D1, 1055 | 0x3794B488,0xEACD428E,0x7B2362C2,0xE97FDE9F,0xBB4C60D2,0xE4B3E2AB,0x74C93909,0x76AA2FDA, 1056 | 0x9F049B7B,0x93BCDA8A,0x51BEC790,0x0FD6E4CC,0x8972E6AD,0xBCA70F40,0x405C2469,0x10673486, 1057 | 0xBD104C97,0x49381E0D,0x063B456A,0x23D02634,0x43ACEC9E,0xE50E49F8,0x197DBF1B,0x8DF1BB9A, 1058 | 0xB46B1CA6,0xD7E895A5,0xCC51A217,0xE1C2F196,0xDEB533C9,0x24FDC58D,0x32850822,0x12DF4DA8, 1059 | 0x90BD3500,0x97C7F320,0xDA3450F4,0x2F534059,0xDC7B3D63,0x95B6CD98,0x09BF19D6,0xA5D15DBF, 1060 | 0x42E47851,0xF07A021E,0x9ECB2A3D,0xE0C39F38,0x99714F95,0x3A5BEA4C,0xB2C4DD25,0xB13D47C0, 1061 | 0xAD418A0B,0x6DEAB81C,0x83EE25F2,0x3B26AE47,0xA8B018D3,0xFF76E5F1,0xA2ED0461,0x26119ED8, 1062 | 0x61EB0A74,0x15A2B187,0x4A93CE2A,0x7943A707,0x29E5B744,0x4E14F02B,0x0A698424,0xD9A03AE6, 1063 | 0xEC87D7C8,0xA94021B8,0x3D95D1CD,0x6E2415BE,0x52E3F592,0x64A83CD9,0x8263C31D,0x41B87EB6, 1064 | 0x8C50FD1A,0x47C80CD7,0xD844008C,0xB812E9AA,0x0B983013,0xFB7C520A,0x4F66FEBB,0x17E982D0, 1065 | 0x00FE6914,0xFE0FD028,0x0C328F93,0x75021AF6,0x3FE6AFB2,0x7E330DE1,0xDF8ADB45,0x14D37B37, 1066 | 0xD04D06A4,0x694B0156,0x0ECF6170,0xC756EBF0,0xF1B76526,0xF348A8B3,0xAE0A79A0,0x54D7B2D4 1067 | }; 1068 | ``` 1069 | 1070 | ## LocalState 1071 | Used for thumbnail files located in `"C:\ProgramData\SYSTEMAX Software Development\SAI\thumbnail"` 1072 | 1073 | Thumbnail filenames use [printf](http://en.cppreference.com/w/cpp/io/c/fprintf) pattern `"%08x.ssd"`. 1074 | Named `LocalState` as it describes an active user context. 1075 | 1076 | ```cpp 1077 | const std::uint32_t LocalStateKey[256] = 1078 | { 1079 | 0x021CF107,0xE9253648,0x8AFBA619,0x8CF31842,0xBF40F860,0xA672F03E,0xFA2756AC,0x927B2E7E, 1080 | 0x1E37D3C4,0x7C3A0524,0x4F284D1B,0xD8A31E9D,0xBA73B6E6,0xF399710D,0xBD8B1937,0x70FFE130, 1081 | 0x056DAA4A,0xDC509CA1,0x07358DFF,0xDF30A2DC,0x67E7349F,0x49532C31,0x2393EBAA,0xE54DF202, 1082 | 0x3A2C7EC9,0x98AB13EF,0x7FA52975,0x83E4792E,0x7485DA08,0x4A1823A8,0x77812011,0x8710BB89, 1083 | 0x9B4E0C68,0x64125D8E,0x5F174A0E,0x33EA50E7,0xA5E168B0,0x1BD9B944,0x6D7D8FE0,0xEE66B84C, 1084 | 0xF0DB530C,0xF8B06B72,0x97ED7DF8,0x126E0122,0x364BED23,0xA103B75C,0x3BC844FA,0xD0946501, 1085 | 0x4E2F70F1,0x79A6F413,0x60B9E977,0xC1582F10,0x759B286A,0xE723EEF5,0x8BAC4B39,0xB074B188, 1086 | 0xCC528E64,0x698700EE,0x44F9E5BB,0x7E336153,0xE2413AFD,0x91DCE2BE,0xFDCE9EC1,0xCAB2DE4F, 1087 | 0x46C5A486,0xA0D630DB,0x1FCD5FCA,0xEA110891,0x3F20C6F9,0xE8F1B25D,0x6EFD10C8,0x889027AF, 1088 | 0xF284AF3F,0x89EE9A61,0x58AF1421,0xE41B9269,0x260C6D71,0x5079D96E,0xD959E465,0x519CD72C, 1089 | 0x73B64F5A,0x40BE5535,0x78386CBC,0x0A1A02CF,0xDBC126B6,0xAD02BC8D,0x22A85BC5,0xA28ABEC3, 1090 | 0x5C643952,0xE35BC9AD,0xCBDACA63,0x4CA076A4,0x4B6121CB,0x9500BF7D,0x6F8E32BF,0xC06587E5, 1091 | 0x21FAEF46,0x9C2AD2F6,0x7691D4A2,0xB13E4687,0xC7460AD6,0xDDFE54D5,0x81F516F3,0xC60D7438, 1092 | 0xB9CB3BC7,0xC4770D94,0xF4571240,0x06862A50,0x30D343D3,0x5ACF52B2,0xACF4E68A,0x0FC2A59B, 1093 | 0xB70AEACD,0x53AA5E80,0xCF624E8F,0xF1214CEB,0x936072DF,0x62193F18,0xF5491CDA,0x5D476958, 1094 | 0xDA7A852D,0x5B053E12,0xC5A9F6D0,0xABD4A7D1,0xD25E6E82,0xA4D17314,0x2E148C4E,0x6B9F6399, 1095 | 0xBC26DB47,0x8296DDCE,0x3E71D616,0x350E4083,0x2063F503,0x167833F2,0x115CDC5E,0x4208E715, 1096 | 0x03A49B66,0x43A724BA,0xA3B71B8C,0x107584AE,0xC24AE0C6,0xB3FC6273,0x280F3795,0x1392C5D4, 1097 | 0xD5BAC762,0xB46B5A3B,0xC9480B8B,0xC39783FC,0x17F2935B,0x9DB482F4,0xA7E9CC09,0x553F4734, 1098 | 0x8DB5C3A3,0x7195EC7A,0xA8518A9A,0x0CE6CB2A,0x14D50976,0x99C077A5,0x012E1733,0x94EC3D7C, 1099 | 0x3D825805,0x0E80A920,0x1D39D1AB,0xFCD85126,0x3C7F3C79,0x7A43780B,0xB26815D9,0xAF1F7F1C, 1100 | 0xBB8D7C81,0xAAE5250F,0x34BC670A,0x1929C8D2,0xD6AE9FC0,0x1AE07506,0x416F3155,0x9EB38698, 1101 | 0x8F22CF29,0x04E8065F,0xE07CFBDE,0x2AEF90E8,0x6CAD049C,0x4DC3A8CC,0x597E3596,0x08562B92, 1102 | 0x52A21D6F,0xB6C9881D,0xFBD75784,0xF613FC32,0x54C6F757,0x66E2D57B,0xCD69FE9E,0x478CA13D, 1103 | 0x2F5F6428,0x8E55913C,0xF9091185,0x0089E8B3,0x1C6A48BD,0x3844946D,0x24CC8B6B,0x6524AC2B, 1104 | 0xD1F6A0F0,0x32980E51,0x8634CE17,0xED67417F,0x250BAEB9,0x84D2FD1A,0xEC6C4593,0x29D0C0B1, 1105 | 0xEBDF42A9,0x0D3DCD45,0x72BF963A,0x27F0B590,0x159D5978,0x3104ABD7,0x903B1F27,0x9F886A56, 1106 | 0x80540FA6,0x18F8AD1F,0xEF5A9870,0x85016FC2,0xC8362D41,0x6376C497,0xE1A15C67,0x6ABD806C, 1107 | 0x569AC1E2,0xFE5D1AF7,0x61CADF59,0xCE063874,0xD4F722DD,0x37DEC2EC,0xAE70BDEA,0x0B2D99B4, 1108 | 0x39B895FE,0x091E9DFB,0xA9150754,0x7D1D7A36,0x9A07B41E,0x5E8FE3B5,0xD34503A0,0xBE2BFAB7, 1109 | 0x5742D0A7,0x48DDBA25,0x7BE3604D,0x2D4C66E9,0xB831FFB8,0xF7BBA343,0x451697E4,0x2C4FD84B, 1110 | 0x96B17B00,0xB5C789E3,0xFFEBF9ED,0xD7C4B349,0xDE3281D8,0x689E4904,0xE683F32F,0x2B3CB0E1 1111 | }; 1112 | ``` 1113 | 1114 | ## sai.ssd 1115 | 1116 | Used only for `sai.ssd` 1117 | Handled the same as user-files but with a different block size of `1024` and `Table-blocks` indexes at every multiple of `128`. 1118 | 1119 | `sai.ssd` seems to have multiple log files stored with symbolic headers: 1120 | - "++FSIF logfile++" 1121 | - Seems to be related to file-security and encryption 1122 | - "++VFS logfile++" 1123 | - Everything related to the virtual file system 1124 | - "++SCDF logfile++" 1125 | - Unknown 1126 | 1127 | ```cpp 1128 | const std::uint32_t SystemKey[256] = 1129 | { 1130 | 0x724FB987,0x4A3E70BE,0xCA549C50,0x34E263E1,0x2D5ED2FF,0x127F0E11,0x58A42B78,0x5F6D14AE, 1131 | 0x7E2F745D,0xC3450384,0xCFBB15DE,0xDF0A6D8A,0xEF2545F3,0x6D8919DB,0xBC413C94,0xCCB0A198, 1132 | 0xE42DBBD2,0x361C0B8C,0x8359731F,0x13D61E9F,0x7505F7CE,0x271D7957,0x429C0699,0xD84EC85F, 1133 | 0x953391DD,0xB25DE567,0xC1BA2F97,0x2309B605,0x69A134D1,0x14A092F2,0x681500EF,0xB90148A7, 1134 | 0x01AF398B,0x16FD5168,0x9E572161,0x0F7405E3,0x56AC576D,0xF275A349,0x1E8120C0,0x4BF64E3A, 1135 | 0x5A90E85E,0xD27BC4F1,0x3BD2FFB1,0xD6B40FDC,0x26EC61CF,0xF744AD3F,0xCDE7C548,0x8AFFE60A, 1136 | 0xE382CA47,0x87DA3E1B,0x8FA3DB36,0x5737C7E0,0xACD8CC17,0xD0CC3B66,0xD93D776B,0x37E5BE2B, 1137 | 0xD38A1129,0x037E81D0,0x15B15072,0xA6493052,0x35BCD4B9,0xC4538D32,0xEC66C1D5,0xA20DF513, 1138 | 0x5524EB75,0x92C10488,0xDA03D9FD,0x65168F4B,0x1902BA24,0x7439FA7D,0x1D8CB46F,0xFBC39389, 1139 | 0xC5DF6A58,0x89E8FB00,0x50DBE0A1,0xAAE98AF8,0x6A7C6C9C,0x7712D6EC,0x4030D0CD,0x6052B585, 1140 | 0x6132AA77,0xEB4A38C3,0x673AB1E6,0x1C3C07C6,0x91EA2C76,0x7A4C7EA0,0x10B3DCFC,0xBE7DF402, 1141 | 0x2817D87A,0x25632264,0xBD8D02B0,0xF6D0F8A8,0xB1ED3AF0,0xE6C4F1CA,0x99E028B5,0xE5D48674, 1142 | 0x09CF47B8,0x9D6EAF0E,0x0A721AFE,0xB6109E54,0x8D642344,0x9FEFC27C,0xF0CA520F,0x2C6BDA7E, 1143 | 0x2E9DB06A,0x97DEFC2E,0x53C5F0EE,0xAD4B8C60,0xE9F36696,0xA8C68907,0x70B70A20,0x3D9F82AA, 1144 | 0x7604A595,0x441A563B,0x39193D4A,0x33BF1DC7,0x31B283FB,0xA399F25B,0x642CE39E,0xF9E3B204, 1145 | 0x79A87534,0x5DBE2943,0x9813E93E,0x47864AD6,0xD420D1BF,0x24A6C986,0xFE386EF7,0xD1B65AB7, 1146 | 0x3A96BF2F,0x006FE1AB,0x22938E90,0x78FE7A40,0x5CE1319B,0x46F5EEF5,0xBB064BE4,0xB7271C22, 1147 | 0xC0225D21,0xFA145B10,0x7C58BC33,0xF84654C2,0xEEF4691E,0x021BEC16,0xE16C1737,0x1BCB2603, 1148 | 0x48A2954D,0xDD56A8FA,0xB8C8A48D,0x5277590B,0x1194E7A9,0x590F42B4,0x7B97C0D8,0x7142B714, 1149 | 0xAEDD6BC8,0xBA116212,0x6B0E642C,0xF42ABDC5,0x6E76AC81,0xBF348819,0xCB790C59,0xDC6718AD, 1150 | 0x80471230,0x84DC985C,0x2AEE32C1,0x4D35964F,0x0C6894AC,0x3EF2CDE5,0xB59B37A5,0x9BC9729D, 1151 | 0x186A41AF,0xEA98A970,0x21F8A291,0x5487E2C9,0xE05F3F42,0xA523B86E,0x8C1E4062,0xA962F6CB, 1152 | 0x0D4816E8,0x9A4DF92D,0x20439DCC,0xA0713645,0x43506FE9,0xC2EB4651,0xB4780D6C,0xAFC29B28, 1153 | 0x1FCE5FD4,0x9C7385D3,0xCE00E463,0x38CD997F,0x452933DA,0xC9F7DEBA,0x0840A093,0xDB287B41, 1154 | 0x90E48479,0x66FC6709,0x6C884C65,0x3FB56082,0xF5B87123,0xED367D1D,0x6F0C44F9,0x8270DD38, 1155 | 0x0E314F83,0x1AE69F35,0xD5A51FB3,0xA761A671,0x850B4DED,0x06AE0892,0x5EAA2A06,0xC7FA80F6, 1156 | 0xB0692D4E,0x81657F8F,0x948B0980,0xB3D97C01,0xFC80C3EA,0xFF9E53A4,0x30BD784C,0xF3AD970C, 1157 | 0xA12E9A31,0x04D37646,0x072655A3,0xE8D5F353,0x4CA98BDF,0x7391FE56,0x7D5BEDA6,0x2BD7650D, 1158 | 0x862B5C73,0x8B60A726,0x7F8ECB3C,0x517A49B6,0xD7B9CF5A,0x6308D5BC,0x0B3F68D7,0x62A7EA15, 1159 | 0xC65AFD3D,0xAB8525B2,0xA451B308,0xE7C7AB18,0x88F91369,0x1783279A,0x4F95DF2A,0x41F158BD, 1160 | 0xC8D1CEBB,0x325CD3E2,0xF1928739,0x9355AE8E,0x2FC05EC4,0x4E0735E7,0xDE3B10D9,0x8E18C61A, 1161 | 0xE29AEF25,0x4984D7A2,0x051F247B,0x29AB9055,0xFD2101F4,0x96FB2E1C,0x5BF04327,0x3C8F1BEB 1162 | }; 1163 | ``` 1164 | --------------------------------------------------------------------------------