├── README.md ├── cmake └── .gitignore ├── thirdparty ├── .gitignore └── lib │ └── .gitignore ├── src ├── util.cpp ├── debug.hpp ├── common.hpp ├── debug.cpp ├── crc.hpp ├── compression.hpp ├── crypto.cpp ├── io_util.cpp ├── util.hpp ├── io_util.hpp ├── compression.cpp ├── btree.cpp ├── crc.cpp ├── main.cpp ├── volume.hpp ├── crypto.hpp ├── volume.cpp └── btree.hpp └── CMakeLists.txt /README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cmake/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /thirdparty/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /thirdparty/lib/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/util.cpp: -------------------------------------------------------------------------------- 1 | #include "util.hpp" 2 | -------------------------------------------------------------------------------- /src/debug.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "common.hpp" 4 | 5 | #include 6 | 7 | void hexDump(const void* data, size_t dataSize); 8 | -------------------------------------------------------------------------------- /src/common.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #define UNREACHABLE_CODE(x) do { __builtin_unreachable(); } while (0) 11 | #define UNUSED(x) (void)(x) 12 | -------------------------------------------------------------------------------- /src/debug.cpp: -------------------------------------------------------------------------------- 1 | #include "debug.hpp" 2 | 3 | void hexDump(const void* data, size_t dataSize) 4 | { 5 | const uint8_t* p = static_cast(data); 6 | 7 | // TODO: refactor 8 | for (size_t i = 0; i < dataSize; ++i) { 9 | printf("%02X ", p[i]); 10 | if (i != 0 && (i & 0xF) == 0xF) { 11 | printf("\n"); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/crc.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "common.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | extern const std::array g_crc32_0x04C11DB7; 11 | 12 | template 13 | inline uint32_t crc32_0x04C11DB7(InputIt first, InputIt last, uint32_t initial) 14 | { 15 | typedef typename std::iterator_traits::value_type ValueType; 16 | 17 | return std::accumulate( 18 | first, last, 19 | initial, 20 | [](uint32_t& crc, const ValueType& data) -> uint32_t { 21 | const uint8_t byte = data & 0xFF; 22 | crc = (crc << 8) ^ g_crc32_0x04C11DB7[(crc >> 24) ^ byte]; 23 | return crc; 24 | } 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/compression.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "common.hpp" 4 | 5 | #include 6 | 7 | class FileExpand 8 | { 9 | public: 10 | static bool inflate(std::vector& out, const uint8_t* data, size_t dataSize); 11 | 12 | static bool checkIfExpanded(const std::vector& data); 13 | static bool unexpand(const std::vector& in, std::vector& out); 14 | 15 | private: 16 | static const auto MAGIC = UINT32_C(0xFFF7F32F); 17 | 18 | static const auto ALIGNMENT = 0x400; 19 | 20 | struct SuperHeader 21 | { 22 | uint32_t magic; 23 | uint32_t decompressedFileSize; 24 | uint32_t fileSize; 25 | uint32_t segmentSize; 26 | uint32_t flags; 27 | uint32_t pad[3]; 28 | }; 29 | 30 | struct SegmentHeader 31 | { 32 | uint32_t magic; 33 | uint32_t size; 34 | uint32_t zSize; 35 | uint32_t checkSum; 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/crypto.cpp: -------------------------------------------------------------------------------- 1 | #include "crypto.hpp" 2 | 3 | constexpr std::array Salsa20Cipher::MATRIX = {{ 4 | { 4, 0, 12, 7 }, 5 | { 8, 4, 0, 9 }, 6 | { 12, 8, 4, 13 }, 7 | { 0, 12, 8, 18 }, 8 | { 9, 5, 1, 7 }, 9 | { 13, 9, 5, 9 }, 10 | { 1, 13, 9, 13 }, 11 | { 5, 1, 13, 18 }, 12 | { 14, 10, 6, 7 }, 13 | { 2, 14, 10, 9 }, 14 | { 6, 2, 14, 13 }, 15 | { 10, 6, 2, 18 }, 16 | { 3, 15, 11, 7 }, 17 | { 7, 3, 15, 9 }, 18 | { 11, 7, 3, 13 }, 19 | { 15, 11, 7, 18 }, 20 | { 1, 0, 3, 7 }, 21 | { 2, 1, 0, 9 }, 22 | { 3, 2, 1, 13 }, 23 | { 0, 3, 2, 18 }, 24 | { 6, 5, 4, 7 }, 25 | { 7, 6, 5, 9 }, 26 | { 4, 7, 6, 13 }, 27 | { 5, 4, 7, 18 }, 28 | { 11, 10, 9, 7 }, 29 | { 8, 11, 10, 9 }, 30 | { 9, 8, 11, 13 }, 31 | { 10, 9, 8, 18 }, 32 | { 12, 15, 14, 7 }, 33 | { 13, 12, 15, 9 }, 34 | { 14, 13, 12, 13 }, 35 | { 15, 14, 13, 18 }, 36 | }}; 37 | -------------------------------------------------------------------------------- /src/io_util.cpp: -------------------------------------------------------------------------------- 1 | #include "io_util.hpp" 2 | 3 | #include 4 | #include 5 | 6 | bool loadFromFile(const std::string& filePath, std::vector& data) 7 | { 8 | try { 9 | std::ifstream file; 10 | file.exceptions(std::ifstream::failbit | std::ifstream::badbit); 11 | file.open(filePath, std::ifstream::in | std::ifstream::binary); 12 | data.assign( 13 | (std::istreambuf_iterator(file)), std::istreambuf_iterator() 14 | ); 15 | file.close(); 16 | return true; 17 | } 18 | catch (const std::ios_base::failure& e) { 19 | std::cerr << e.what() << std::endl; 20 | return false; 21 | } 22 | } 23 | 24 | bool saveToFile(const std::string& filePath, const void* data, size_t dataSize) 25 | { 26 | try { 27 | std::ofstream file; 28 | file.exceptions(std::ofstream::failbit | std::ofstream::badbit); 29 | file.open(filePath, std::ofstream::out | std::ofstream::binary); 30 | file.write(reinterpret_cast(data), dataSize); 31 | file.close(); 32 | return true; 33 | } 34 | catch (const std::ios_base::failure& e) { 35 | std::cerr << e.what() << std::endl; 36 | return false; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | string(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") 4 | 5 | set(THIRDPARTY_PATH "${CMAKE_SOURCE_DIR}/thirdparty") 6 | 7 | project(gttool) 8 | 9 | # 10 | # Compiler settings. 11 | # 12 | 13 | string(APPEND CMAKE_CXX_FLAGS "-std=c++14 -march=native -Wall -Wextra -Werror-implicit-function-declaration -Wno-unused-function -Wno-unused-parameter -Wno-unused-label -fno-limit-debug-info -fno-strict-aliasing") 14 | 15 | if(WIN32) 16 | add_definitions(-DWIN32_LEAN_AND_MEAN) 17 | add_definitions(-DNOMINMAX) 18 | endif() 19 | 20 | if(CMAKE_BUILD_TYPE STREQUAL "Debug") 21 | add_definitions(-D_DEBUG) 22 | else() 23 | add_definitions(-DNDEBUG) 24 | endif() 25 | 26 | # 27 | # Third-party dependendices. 28 | # 29 | 30 | find_package(ZLIB REQUIRED) 31 | if(ZLIB_FOUND) 32 | include_directories(${ZLIB_INCLUDE_DIRS}) 33 | endif() 34 | 35 | set(Boost_USE_STATIC_LIBS ON) 36 | set(Boost_USE_MULTITHREADED ON) 37 | set(Boost_USE_STATIC_RUNTIME OFF) 38 | find_package(Boost REQUIRED COMPONENTS system filesystem iostreams program_options) 39 | if(Boost_FOUND) 40 | include_directories(${Boost_INCLUDE_DIRS}) 41 | message("-- Boost ${Boost_MAJOR_VERSION}.${Boost_MINOR_VERSION}.${Boost_SUBMINOR_VERSION} found!") 42 | endif() 43 | 44 | link_directories(thirdparty/lib) 45 | 46 | # 47 | # Project settings. 48 | # 49 | 50 | set(SOURCE_FILES 51 | src/btree.cpp 52 | src/btree.hpp 53 | src/common.hpp 54 | src/compression.cpp 55 | src/compression.hpp 56 | src/crc.cpp 57 | src/crc.hpp 58 | src/debug.cpp 59 | src/debug.hpp 60 | src/io_util.hpp 61 | src/crypto.cpp 62 | src/crypto.hpp 63 | src/main.cpp 64 | src/util.cpp 65 | src/util.hpp 66 | src/volume.cpp 67 | src/volume.hpp 68 | src/io_util.cpp) 69 | 70 | set(THIRDPARTY_LIBRARIES ${Boost_LIBRARIES} ${ZLIB_LIBRARIES} ) 71 | 72 | add_executable(gttool ${SOURCE_FILES}) 73 | target_link_libraries(gttool ${THIRDPARTY_LIBRARIES}) 74 | -------------------------------------------------------------------------------- /src/util.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "common.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | template 10 | inline typename std::enable_if::value, T>::type alignUp(T x, size_t alignment) 11 | { 12 | const T mask = ~(static_cast(alignment) - 1); 13 | return (x + (alignment - 1)) & mask; 14 | } 15 | 16 | template 17 | inline typename std::enable_if::value, T>::type alignDown(T x, size_t alignment) 18 | { 19 | const T mask = ~(static_cast(alignment) - 1); 20 | return x & mask; 21 | } 22 | 23 | template 24 | inline const T* advancePointer(const T* data, intptr_t count) 25 | { 26 | return reinterpret_cast( 27 | reinterpret_cast(data) + count 28 | ); 29 | } 30 | 31 | template 32 | inline void advancePointerInplace(const T*& buffer, size_t count) 33 | { 34 | buffer = advancePointer(buffer, count); 35 | } 36 | 37 | template::value>> 38 | inline constexpr T rotateLeft(const T x, unsigned n) 39 | { 40 | const unsigned bitCount = sizeof(T) * CHAR_BIT; 41 | return (x << n) | (x >> (bitCount - n)); 42 | } 43 | 44 | inline int charToInt(int x) 45 | { 46 | if (x >= '0' && x <= '9') 47 | return (x - '0'); 48 | if (x >= 'A' && x <= 'F') 49 | return (x - 'A' + 10); 50 | if (x >= 'a' && x <= 'f') 51 | return (x - 'a' + 10); 52 | throw std::invalid_argument("Unexpected character"); 53 | } 54 | 55 | inline size_t parseHexString(const char* str, uint8_t* data, size_t maxSize) 56 | { 57 | size_t n; 58 | 59 | for (n = 0; str[0] && str[1] && n < maxSize; ) { 60 | if (std::isspace(*str)) { 61 | ++str; 62 | continue; 63 | } 64 | 65 | *data++ = static_cast((charToInt(str[0]) << 4) + charToInt(str[1])); 66 | str += 2; 67 | ++n; 68 | } 69 | 70 | return n; 71 | } 72 | 73 | inline size_t parseHexString(const std::string& str, uint8_t* data, size_t maxSize) 74 | { 75 | return parseHexString(str.c_str(), data, maxSize); 76 | } 77 | -------------------------------------------------------------------------------- /src/io_util.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "util.hpp" 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | template 11 | inline T read(const CharT* buffer) 12 | { 13 | T value; 14 | std::memcpy(&value, buffer, sizeof(value)); 15 | return value; 16 | } 17 | 18 | template 19 | inline T* readN(const CharT* buffer, T* data, size_t count) 20 | { 21 | std::memcpy(data, buffer, count); 22 | return data; 23 | } 24 | 25 | template 26 | inline T readNext(const CharT*& buffer) 27 | { 28 | const T value = read(buffer); 29 | advancePointerInplace(buffer, sizeof(value)); 30 | return value; 31 | } 32 | 33 | template 34 | inline T* readNextN(const CharT*& buffer, T* data, size_t count) 35 | { 36 | data = readN(buffer, data, count); 37 | advancePointerInplace(buffer, count); 38 | return data; 39 | } 40 | 41 | template 42 | inline T readAt(const CharT* buffer, size_t offset) 43 | { 44 | T value; 45 | std::memcpy(&value, advancePointer(buffer, offset), sizeof(value)); 46 | return value; 47 | } 48 | 49 | template 50 | inline T* readAtN(const CharT* buffer, size_t offset, T* data, size_t count) 51 | { 52 | std::memcpy(data, advancePointer(buffer, offset), count); 53 | return data; 54 | } 55 | 56 | template 57 | inline T readWithByteSwap(const CharT* buffer) 58 | { 59 | const T value = read(buffer); 60 | return boost::endian::endian_reverse(value); 61 | } 62 | 63 | template 64 | inline T readNextWithByteSwap(const CharT*& buffer) 65 | { 66 | const T value = readNext(buffer); 67 | return boost::endian::endian_reverse(value); 68 | } 69 | 70 | template 71 | inline T readAtWithByteSwap(const CharT* buffer, size_t offset) 72 | { 73 | const T value = readAt(buffer, offset); 74 | return boost::endian::endian_reverse(value); 75 | } 76 | 77 | bool loadFromFile(const std::string& filePath, std::vector& data); 78 | bool saveToFile(const std::string& filePath, const void* data, size_t dataSize); 79 | -------------------------------------------------------------------------------- /src/compression.cpp: -------------------------------------------------------------------------------- 1 | #include "compression.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | bool FileExpand::inflate(std::vector& out, const uint8_t* data, size_t dataSize) { 10 | if (data && dataSize > 0) { 11 | try { 12 | boost::iostreams::zlib_params params; 13 | params.window_bits = -boost::iostreams::zlib::default_window_bits; 14 | 15 | boost::iostreams::filtering_ostream os; 16 | os.push(boost::iostreams::zlib_decompressor(params)); 17 | os.push(std::back_inserter(out)); 18 | 19 | boost::iostreams::write(os, reinterpret_cast(data), dataSize); 20 | } 21 | catch (const std::exception& e) { 22 | return false; 23 | } 24 | } 25 | 26 | return true; 27 | } 28 | 29 | bool FileExpand::checkIfExpanded(const std::vector& data) 30 | { 31 | if (data.size() < sizeof(SuperHeader)) { 32 | return false; 33 | } 34 | const auto superHdr = reinterpret_cast(data.data()); 35 | if (superHdr->magic != MAGIC) { 36 | return false; 37 | } 38 | if (superHdr->segmentSize == 0 || (superHdr->segmentSize % ALIGNMENT != 0)) { 39 | return false; 40 | } 41 | if (data.size() < superHdr->fileSize) { 42 | return false; 43 | } 44 | 45 | return true; 46 | } 47 | 48 | bool FileExpand::unexpand(const std::vector& in, std::vector& out) 49 | { 50 | if (!checkIfExpanded(in)) { 51 | return false; 52 | } 53 | 54 | const auto superHdr = reinterpret_cast(in.data()); 55 | const auto segmentCount = (superHdr->fileSize + superHdr->segmentSize - 1) / superHdr->segmentSize; 56 | 57 | out.clear(); 58 | out.reserve(superHdr->decompressedFileSize); 59 | 60 | boost::iostreams::zlib_params params; 61 | params.window_bits = -boost::iostreams::zlib::default_window_bits; 62 | 63 | auto status = true; 64 | for (auto i = 0u; i < segmentCount; ++i) { 65 | const auto segmentHdr = (i == 0) 66 | ? reinterpret_cast(superHdr + 1) 67 | : reinterpret_cast(reinterpret_cast(superHdr) + static_cast(superHdr->segmentSize) * i) 68 | ; 69 | 70 | const auto segmentData = reinterpret_cast(segmentHdr + 1); 71 | if (!inflate(out, segmentData, segmentHdr->zSize)) { 72 | status = false; 73 | break; 74 | } 75 | } 76 | 77 | if (out.size() != superHdr->decompressedFileSize) { 78 | status = false; 79 | } 80 | 81 | return status; 82 | } 83 | -------------------------------------------------------------------------------- /src/btree.cpp: -------------------------------------------------------------------------------- 1 | #include "btree.hpp" 2 | #include "volume.hpp" 3 | 4 | void StringKey::dump() const 5 | { 6 | std::string str(value(), length()); 7 | std::cout 8 | << "Length: " << str.length() << std::endl 9 | << "Value: " << str << std::endl 10 | << std::endl; 11 | } 12 | 13 | StringBTree::CallbackResult StringBTree::traverseCallback(const uint8_t* data) const 14 | { 15 | CallbackResult result = CallbackResult::CONTINUE; 16 | 17 | KeyType obj; 18 | if (parseData(obj, data)) { 19 | std::string str(obj.value(), obj.length()); 20 | std::cout << str << std::endl; 21 | } else { 22 | std::cerr << "Cannot parse String object." << std::endl; 23 | } 24 | 25 | return result; 26 | } 27 | 28 | void EntryKey::dump() const 29 | { 30 | std::cout 31 | << std::hex 32 | << "Flags: 0x" << flags() << std::endl 33 | << "Name index: 0x" << nameIndex() << std::endl 34 | << "Ext index: 0x" << extIndex() << std::endl 35 | << "Link index: 0x" << linkIndex() << std::endl 36 | << "Is directory: " << (isDirectory() ? "true" : "false") << std::endl 37 | << "Is file: " << (isFile() ? "true" : "false") << std::endl 38 | << std::dec << std::endl; 39 | } 40 | 41 | EntryBTree::CallbackResult EntryBTree::traverseCallback(const uint8_t* data) const 42 | { 43 | CallbackResult result = CallbackResult::CONTINUE; 44 | 45 | KeyType obj; 46 | if (parseData(obj, data)) { 47 | std::cout << obj.nameIndex() << ":" << obj.extIndex() << ":" << obj.linkIndex() << std::endl; 48 | } else { 49 | std::cerr << "Cannot parse Entry object." << std::endl; 50 | } 51 | 52 | return result; 53 | } 54 | 55 | void NodeKey::dump() const 56 | { 57 | std::cout 58 | << std::hex 59 | << "Flags: 0x" << flags() << std::endl 60 | << "Size1: 0x" << size1() << std::endl 61 | << "Size2: 0x" << size2() << std::endl 62 | << "Node index: 0x" << nodeIndex() << std::endl 63 | << "Volume index: 0x" << volumeIndex() << std::endl 64 | << "Sector index: 0x" << sectorIndex() << std::endl 65 | << "Has compression: " << (hasCompression() ? "true" : "false") << std::endl 66 | << "Has bit 4: " << (hasBit4() ? "true" : "false") << std::endl 67 | << "Has bit 5: " << (hasBit5() ? "true" : "false") << std::endl 68 | << std::dec << std::endl; 69 | } 70 | 71 | NodeBTree::CallbackResult NodeBTree::traverseCallback(const uint8_t* data) const 72 | { 73 | CallbackResult result = CallbackResult::CONTINUE; 74 | 75 | KeyType obj; 76 | if (parseData(obj, data)) { 77 | std::cout << obj.nodeIndex() << ":" << obj.volumeIndex() << ":" << obj.size1() << ":" << obj.size2() << std::endl; 78 | } else { 79 | std::cerr << "Cannot parse Node object." << std::endl; 80 | } 81 | 82 | return result; 83 | } 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/crc.cpp: -------------------------------------------------------------------------------- 1 | #include "crc.hpp" 2 | 3 | const std::array g_crc32_0x04C11DB7 = {{ 4 | 0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9, 0x130476DC, 0x17C56B6B, 5 | 0x1A864DB2, 0x1E475005, 0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61, 6 | 0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD, 0x4C11DB70, 0x48D0C6C7, 7 | 0x4593E01E, 0x4152FDA9, 0x5F15ADAC, 0x5BD4B01B, 0x569796C2, 0x52568B75, 8 | 0x6A1936C8, 0x6ED82B7F, 0x639B0DA6, 0x675A1011, 0x791D4014, 0x7DDC5DA3, 9 | 0x709F7B7A, 0x745E66CD, 0x9823B6E0, 0x9CE2AB57, 0x91A18D8E, 0x95609039, 10 | 0x8B27C03C, 0x8FE6DD8B, 0x82A5FB52, 0x8664E6E5, 0xBE2B5B58, 0xBAEA46EF, 11 | 0xB7A96036, 0xB3687D81, 0xAD2F2D84, 0xA9EE3033, 0xA4AD16EA, 0xA06C0B5D, 12 | 0xD4326D90, 0xD0F37027, 0xDDB056FE, 0xD9714B49, 0xC7361B4C, 0xC3F706FB, 13 | 0xCEB42022, 0xCA753D95, 0xF23A8028, 0xF6FB9D9F, 0xFBB8BB46, 0xFF79A6F1, 14 | 0xE13EF6F4, 0xE5FFEB43, 0xE8BCCD9A, 0xEC7DD02D, 0x34867077, 0x30476DC0, 15 | 0x3D044B19, 0x39C556AE, 0x278206AB, 0x23431B1C, 0x2E003DC5, 0x2AC12072, 16 | 0x128E9DCF, 0x164F8078, 0x1B0CA6A1, 0x1FCDBB16, 0x018AEB13, 0x054BF6A4, 17 | 0x0808D07D, 0x0CC9CDCA, 0x7897AB07, 0x7C56B6B0, 0x71159069, 0x75D48DDE, 18 | 0x6B93DDDB, 0x6F52C06C, 0x6211E6B5, 0x66D0FB02, 0x5E9F46BF, 0x5A5E5B08, 19 | 0x571D7DD1, 0x53DC6066, 0x4D9B3063, 0x495A2DD4, 0x44190B0D, 0x40D816BA, 20 | 0xACA5C697, 0xA864DB20, 0xA527FDF9, 0xA1E6E04E, 0xBFA1B04B, 0xBB60ADFC, 21 | 0xB6238B25, 0xB2E29692, 0x8AAD2B2F, 0x8E6C3698, 0x832F1041, 0x87EE0DF6, 22 | 0x99A95DF3, 0x9D684044, 0x902B669D, 0x94EA7B2A, 0xE0B41DE7, 0xE4750050, 23 | 0xE9362689, 0xEDF73B3E, 0xF3B06B3B, 0xF771768C, 0xFA325055, 0xFEF34DE2, 24 | 0xC6BCF05F, 0xC27DEDE8, 0xCF3ECB31, 0xCBFFD686, 0xD5B88683, 0xD1799B34, 25 | 0xDC3ABDED, 0xD8FBA05A, 0x690CE0EE, 0x6DCDFD59, 0x608EDB80, 0x644FC637, 26 | 0x7A089632, 0x7EC98B85, 0x738AAD5C, 0x774BB0EB, 0x4F040D56, 0x4BC510E1, 27 | 0x46863638, 0x42472B8F, 0x5C007B8A, 0x58C1663D, 0x558240E4, 0x51435D53, 28 | 0x251D3B9E, 0x21DC2629, 0x2C9F00F0, 0x285E1D47, 0x36194D42, 0x32D850F5, 29 | 0x3F9B762C, 0x3B5A6B9B, 0x0315D626, 0x07D4CB91, 0x0A97ED48, 0x0E56F0FF, 30 | 0x1011A0FA, 0x14D0BD4D, 0x19939B94, 0x1D528623, 0xF12F560E, 0xF5EE4BB9, 31 | 0xF8AD6D60, 0xFC6C70D7, 0xE22B20D2, 0xE6EA3D65, 0xEBA91BBC, 0xEF68060B, 32 | 0xD727BBB6, 0xD3E6A601, 0xDEA580D8, 0xDA649D6F, 0xC423CD6A, 0xC0E2D0DD, 33 | 0xCDA1F604, 0xC960EBB3, 0xBD3E8D7E, 0xB9FF90C9, 0xB4BCB610, 0xB07DABA7, 34 | 0xAE3AFBA2, 0xAAFBE615, 0xA7B8C0CC, 0xA379DD7B, 0x9B3660C6, 0x9FF77D71, 35 | 0x92B45BA8, 0x9675461F, 0x8832161A, 0x8CF30BAD, 0x81B02D74, 0x857130C3, 36 | 0x5D8A9099, 0x594B8D2E, 0x5408ABF7, 0x50C9B640, 0x4E8EE645, 0x4A4FFBF2, 37 | 0x470CDD2B, 0x43CDC09C, 0x7B827D21, 0x7F436096, 0x7200464F, 0x76C15BF8, 38 | 0x68860BFD, 0x6C47164A, 0x61043093, 0x65C52D24, 0x119B4BE9, 0x155A565E, 39 | 0x18197087, 0x1CD86D30, 0x029F3D35, 0x065E2082, 0x0B1D065B, 0x0FDC1BEC, 40 | 0x3793A651, 0x3352BBE6, 0x3E119D3F, 0x3AD08088, 0x2497D08D, 0x2056CD3A, 41 | 0x2D15EBE3, 0x29D4F654, 0xC5A92679, 0xC1683BCE, 0xCC2B1D17, 0xC8EA00A0, 42 | 0xD6AD50A5, 0xD26C4D12, 0xDF2F6BCB, 0xDBEE767C, 0xE3A1CBC1, 0xE760D676, 43 | 0xEA23F0AF, 0xEEE2ED18, 0xF0A5BD1D, 0xF464A0AA, 0xF9278673, 0xFDE69BC4, 44 | 0x89B8FD09, 0x8D79E0BE, 0x803AC667, 0x84FBDBD0, 0x9ABC8BD5, 0x9E7D9662, 45 | 0x933EB0BB, 0x97FFAD0C, 0xAFB010B1, 0xAB710D06, 0xA6322BDF, 0xA2F33668, 46 | 0xBCB4666D, 0xB8757BDA, 0xB5365D03, 0xB1F740B4, 47 | }}; 48 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "volume.hpp" 2 | 3 | #include 4 | 5 | #include 6 | 7 | int main(int argc, const char* argv[]) 8 | { 9 | try { 10 | boost::program_options::options_description generalOpts("General options"); 11 | generalOpts.add_options() 12 | ("help,h", "Display help message") 13 | ("unpack,u", "Unpack volume files") 14 | ("decrypt,d", "Decrypt file") 15 | ; 16 | 17 | boost::program_options::options_description unpackOpts("Unpack options"); 18 | unpackOpts.add_options() 19 | ("input,i", boost::program_options::value(), "Volume/Index file") 20 | ("output,o", boost::program_options::value(), "Output directory") 21 | ; 22 | 23 | boost::program_options::options_description decryptOpts("Decrypt options"); 24 | decryptOpts.add_options() 25 | ("input,i", boost::program_options::value(), "Input file") 26 | ("output,o", boost::program_options::value(), "Output file") 27 | ("key,k", boost::program_options::value(), "Encryption key") 28 | ; 29 | 30 | boost::program_options::options_description allOpts; 31 | allOpts.add(generalOpts).add(unpackOpts).add(decryptOpts); 32 | 33 | auto parsedOpts = boost::program_options::command_line_parser(argc, argv) 34 | .style(boost::program_options::command_line_style::unix_style) 35 | .allow_unregistered() 36 | .options(generalOpts) 37 | .run(); 38 | 39 | boost::program_options::variables_map varMap; 40 | boost::program_options::store(parsedOpts, varMap); 41 | const auto restParams = boost::program_options::collect_unrecognized(parsedOpts.options, boost::program_options::include_positional); 42 | boost::program_options::notify(varMap); 43 | 44 | if (varMap.count("help")) { 45 | show_help: 46 | std::cout << "GT Tool (c) flatz, 2018" << std::endl; 47 | std::cout << allOpts; 48 | } else if (varMap.count("decrypt")) { 49 | boost::program_options::variables_map restVarMap; 50 | boost::program_options::store( 51 | boost::program_options::command_line_parser(restParams) 52 | .style(boost::program_options::command_line_style::unix_style) 53 | .allow_unregistered() 54 | .options(decryptOpts) 55 | .run(), 56 | restVarMap 57 | ); 58 | boost::program_options::notify(restVarMap); 59 | 60 | if (!restVarMap.count("input") || !restVarMap.count("output") || !restVarMap.count("key")) { 61 | goto show_help; 62 | } 63 | 64 | const auto& inFile = restVarMap["input"].as(); 65 | const auto& outFile = restVarMap["output"].as(); 66 | const auto& keyStr = restVarMap["key"].as(); 67 | 68 | uint8_t key[0x20] = {}; 69 | if (parseHexString(keyStr, key, sizeof(key)) != sizeof(key)) { 70 | std::cerr << "Invalid key specified." << std::endl; 71 | return EXIT_FAILURE; 72 | } 73 | 74 | if (!boost::filesystem::exists(inFile) || !boost::filesystem::is_regular_file(inFile)) { 75 | std::cerr << "Invalid input file specified." << std::endl; 76 | return EXIT_FAILURE; 77 | } 78 | if (boost::filesystem::exists(outFile) && !boost::filesystem::is_regular_file(outFile)) { 79 | std::cerr << "Invalid output file specified." << std::endl; 80 | return EXIT_FAILURE; 81 | } 82 | 83 | std::vector data; 84 | if (!loadFromFile(inFile, data)) { 85 | std::cerr << "Unable to load input file." << std::endl; 86 | return EXIT_FAILURE; 87 | } 88 | 89 | std::cout << "Decrypting file..." << std::endl; 90 | 91 | Salsa20Cipher cipher(key, sizeof(key)); 92 | cipher.processBytes(data.data(), data.data(), data.size()); 93 | 94 | if (!saveToFile(outFile, data.data(), data.size())) { 95 | std::cerr << "Unable to load input file." << std::endl; 96 | return EXIT_FAILURE; 97 | } 98 | 99 | std::cout << "Done!" << std::endl; 100 | return EXIT_SUCCESS; 101 | } else if (varMap.count("unpack")) { 102 | boost::program_options::variables_map restVarMap; 103 | boost::program_options::store( 104 | boost::program_options::command_line_parser(restParams) 105 | .style(boost::program_options::command_line_style::unix_style) 106 | .allow_unregistered() 107 | .options(decryptOpts) 108 | .run(), 109 | restVarMap 110 | ); 111 | boost::program_options::notify(restVarMap); 112 | 113 | if (!restVarMap.count("input") || !restVarMap.count("output")) { 114 | goto show_help; 115 | } 116 | 117 | const auto& inFile = restVarMap["input"].as(); 118 | const auto& outDir = restVarMap["output"].as(); 119 | 120 | if (!boost::filesystem::exists(inFile) || !boost::filesystem::is_regular_file(inFile)) { 121 | std::cerr << "Invalid volume file specified." << std::endl; 122 | return EXIT_FAILURE; 123 | } 124 | if (boost::filesystem::exists(outDir) && !boost::filesystem::is_directory(outDir)) { 125 | std::cerr << "Invalid output directory specified." << std::endl; 126 | return EXIT_FAILURE; 127 | } 128 | 129 | GT5VolumeFile vol5; 130 | GT6VolumeFile vol6; 131 | GT7VolumeFile vol7; 132 | std::array volumes = {{ &vol5, &vol6, &vol7 }}; 133 | VolumeFile* volume = nullptr; 134 | for (auto vol: volumes) { 135 | if (vol->load(inFile)) { 136 | volume = vol; 137 | break; 138 | } 139 | } 140 | if (!volume) { 141 | std::cerr << "Unable to load volume file." << std::endl; 142 | return EXIT_FAILURE; 143 | } 144 | 145 | std::cout << "Unpacking files..." << std::endl; 146 | if (!volume->unpackAll(outDir)) { 147 | std::cerr << "Unable to unpack volume file." << std::endl; 148 | return EXIT_FAILURE; 149 | } 150 | 151 | std::cout << "Done!" << std::endl; 152 | return EXIT_SUCCESS; 153 | } else { 154 | goto show_help; 155 | } 156 | } 157 | catch (const std::exception& e) { 158 | std::cerr << "Unhandled error occurred:" << std::endl << e.what() << std::endl; 159 | return EXIT_FAILURE; 160 | } 161 | 162 | return 0; 163 | } -------------------------------------------------------------------------------- /src/volume.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "btree.hpp" 4 | #include "crypto.hpp" 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | // TODO: refactor this shit 12 | #define VOLUME_READ(volumePtr, dataPtr, type) ((volumePtr)->needSwapEndian() ? readWithByteSwap(dataPtr) : read(dataPtr)) 13 | #define VOLUME_READ_SELF(dataPtr, type) VOLUME_READ(this, dataPtr, type) 14 | 15 | #define VOLUME_READ_NEXT(volumePtr, dataPtr, type) ((volumePtr)->needSwapEndian() ? readNextWithByteSwap(dataPtr) : readNext(dataPtr)) 16 | #define VOLUME_READ_NEXT_SELF(dataPtr, type) VOLUME_READ_NEXT(this, dataPtr, type) 17 | 18 | #define VOLUME_READ_AT(volumePtr, dataPtr, type, offset) ((volumePtr)->needSwapEndian() ? readAtWithByteSwap(dataPtr, (offset)) : readAt(dataPtr, (offset))) 19 | #define VOLUME_READ_AT_SELF(dataPtr, type, offset) VOLUME_READ_AT(this, dataPtr, type, offset) 20 | 21 | #define VOLUME_READN(volumePtr, dataPtr, type, ptr, count) readN((dataPtr), (ptr), (count)) 22 | #define VOLUME_READN_SELF(dataPtr, type, ptr, count) VOLUME_READN(this, dataPtr, type, ptr, count) 23 | 24 | #define VOLUME_READN_NEXT(volumePtr, dataPtr, type, ptr, count) readNextN((dataPtr), (ptr), (count)) 25 | #define VOLUME_READN_NEXT_SELF(dataPtr, type, ptr, count) VOLUME_READN_NEXT(this, dataPtr, type, ptr, count) 26 | 27 | #define VOLUME_READN_AT(volumePtr, dataPtr, type, offset, ptr, count) readAtN((dataPtr), (offset), (ptr), (count)) 28 | #define VOLUME_READN_AT_SELF(dataPtr, type, offset, ptr, count) VOLUME_READN_AT(this, dataPtr, type, offset, ptr, count) 29 | 30 | class VolumeFile 31 | : private boost::noncopyable 32 | { 33 | public: 34 | static const auto SEGMENT_SIZE = UINT64_C(0x800); 35 | 36 | VolumeFile(bool swapEndian) 37 | : m_swapEndian(swapEndian) 38 | { 39 | reset(); 40 | } 41 | 42 | virtual ~VolumeFile() {} 43 | 44 | bool load(const std::string& filePath); 45 | 46 | bool unpackNode(const NodeKey& nodeKey, const std::string& filePath); 47 | bool unpackAll(const std::string& outDirectory); 48 | 49 | std::string getEntryPath(const EntryKey& entryKey, const std::string& prefix) const; 50 | 51 | const auto& data() const { return m_data; } 52 | 53 | bool needSwapEndian() const { return m_swapEndian; } 54 | bool hasMultipleVolumes() const { return m_dataStreams.size() > 1; } 55 | 56 | auto nameTreeOffset() const { return m_nameTreeOffset; } 57 | auto extTreeOffset() const { return m_extTreeOffset; } 58 | auto entryTreeCount() const { return m_entryTreeCount; } 59 | auto entryTreeOffset(uint32_t index) const { return m_entryTreeOffsets[index]; } 60 | auto nodeTreeOffset() const { return m_nodeTreeOffset; } 61 | auto dataOffset() const { return m_dataOffset; } 62 | 63 | protected: 64 | static const auto HEADER_MAGIC = UINT32_C(0x5B745162); 65 | static const auto SEGMENT_MAGIC = UINT32_C(0x5B74516E); 66 | static const auto Z_MAGIC = UINT32_C(0xFFF7EEC5); 67 | 68 | static const auto DEFAULT_SECTOR_SIZE = UINT32_C(0x800); 69 | static const auto DEFAULT_SEGMENT_SIZE = UINT32_C(0x10000); 70 | 71 | struct StreamDesc 72 | { 73 | StreamDesc() 74 | : fileSize(0) 75 | , sectorSize(DEFAULT_SECTOR_SIZE) 76 | , segmentSize(DEFAULT_SEGMENT_SIZE) 77 | { 78 | } 79 | 80 | std::ifstream stream; 81 | std::vector extHeader; 82 | std::string filePath; 83 | 84 | uint64_t fileSize; 85 | uint32_t sectorSize; 86 | uint32_t segmentSize; 87 | }; 88 | 89 | virtual const Keyset& getKeyset() const = 0; 90 | 91 | virtual size_t getHeaderSize() const = 0; 92 | 93 | virtual void reset() 94 | { 95 | m_origPath.clear(); 96 | m_basePath.clear(); 97 | m_baseName.clear(); 98 | 99 | if (m_mainStream.is_open()) { 100 | m_mainStream.close(); 101 | } 102 | 103 | for (auto& dataStream: m_dataStreams) { 104 | if (!dataStream.stream.is_open()) { 105 | continue; 106 | } 107 | dataStream.stream.close(); 108 | } 109 | m_dataStreams.clear(); 110 | 111 | m_mainFileSize = 0; 112 | 113 | m_data.clear(); 114 | m_entryTreeOffsets.clear(); 115 | 116 | m_nameTreeOffset = m_extTreeOffset = 0; 117 | m_nodeTreeOffset = m_entryTreeCount = 0; 118 | 119 | m_dataOffset = 0; 120 | } 121 | 122 | bool readDataAt(std::vector& data, uint64_t offset, uint64_t size) 123 | { 124 | return readDataAt(m_mainStream, data, offset, size); 125 | } 126 | 127 | unsigned int getNodeByPath(const std::string& filePath, NodeKey& nodeKey) const; 128 | 129 | bool decryptData(uint8_t* data, uint64_t dataSize, uint32_t seed) const; 130 | 131 | bool inflateDataIfNeeded(std::vector& in, uint64_t outSize) const; 132 | 133 | virtual std::string normalizeFilePath(const std::string& path) const 134 | { 135 | return path; 136 | } 137 | 138 | bool readDataAt(std::ifstream& stream, std::vector& data, uint64_t offset, uint64_t size); 139 | 140 | virtual bool parseHeader(const uint8_t* header, uint64_t headerSize) 141 | { 142 | return false; 143 | } 144 | 145 | bool parseSegment(); 146 | 147 | virtual bool decryptHeader(uint8_t* header, uint64_t headerSize) const; 148 | 149 | boost::filesystem::path m_origPath; 150 | boost::filesystem::path m_basePath, m_baseName; 151 | 152 | std::ifstream m_mainStream; 153 | std::vector m_dataStreams; 154 | 155 | uint64_t m_mainFileSize; 156 | 157 | std::vector m_data; 158 | std::vector m_entryTreeOffsets; 159 | 160 | uint32_t m_nameTreeOffset; 161 | uint32_t m_extTreeOffset; 162 | uint32_t m_nodeTreeOffset; 163 | uint32_t m_entryTreeCount; 164 | uint64_t m_dataOffset; 165 | 166 | bool m_swapEndian; 167 | }; 168 | 169 | class GT5VolumeFile 170 | : public VolumeFile 171 | { 172 | public: 173 | GT5VolumeFile() 174 | : VolumeFile(true) 175 | { 176 | } 177 | 178 | const auto& titleId() const { return m_titleId; } 179 | 180 | protected: 181 | const Keyset& getKeyset() const override; 182 | 183 | size_t getHeaderSize() const override { return 0xA0; } 184 | 185 | void reset() override 186 | { 187 | VolumeFile::reset(); 188 | 189 | m_titleId.clear(); 190 | } 191 | 192 | bool parseHeader(const uint8_t* header, uint64_t headerSize) override; 193 | 194 | std::string m_titleId; 195 | }; 196 | 197 | class GT6VolumeFile 198 | : public GT5VolumeFile 199 | { 200 | protected: 201 | const Keyset& getKeyset() const override; 202 | }; 203 | 204 | class GT7VolumeFile 205 | : public VolumeFile 206 | { 207 | public: 208 | GT7VolumeFile() 209 | : VolumeFile(false) 210 | { 211 | } 212 | 213 | protected: 214 | struct ExtHeader 215 | { 216 | uint64_t magic; 217 | uint32_t sectorSize; 218 | uint32_t segmentSize; 219 | uint64_t fileSize; 220 | uint32_t flags; 221 | uint32_t unk; // TODO: seed? 222 | }; 223 | 224 | static const auto MAX_FILE_NAME_LENGTH = 16; 225 | 226 | static const auto EXT_HEADER_MAGIC = UINT64_C(0x2B26958523AD); 227 | static const auto EXT_ALIGNMENT = 0x400; 228 | 229 | static const auto UNK_SEED = UINT32_C(0xD265FF5C); 230 | 231 | struct VolumeInfo 232 | { 233 | char fileName[MAX_FILE_NAME_LENGTH]; 234 | uint64_t fileSize; 235 | }; 236 | 237 | const Keyset& getKeyset() const override; 238 | 239 | size_t getHeaderSize() const override { return 0xA60; } 240 | 241 | bool decryptHeader(uint8_t* header, uint64_t headerSize) const override; 242 | 243 | void reset() override 244 | { 245 | VolumeFile::reset(); 246 | 247 | m_volumes.clear(); 248 | } 249 | 250 | bool parseHeader(const uint8_t* header, uint64_t headerSize) override; 251 | bool parseExtendedHeader(StreamDesc& streamDesc); 252 | 253 | std::string normalizeFilePath(const std::string& path) const override; 254 | 255 | std::vector m_volumes; 256 | }; 257 | -------------------------------------------------------------------------------- /src/crypto.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "crc.hpp" 4 | #include "util.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | class Keyset 14 | { 15 | public: 16 | typedef std::array Key; 17 | 18 | Keyset(const std::string& magic, const Key& key) 19 | : m_magic(magic) 20 | , m_key(key) 21 | { 22 | } 23 | 24 | const Key computeKey(uint32_t seed) const 25 | { 26 | const auto c0 = (~crc32_0x04C11DB7(m_magic.begin(), m_magic.end(), 0)) ^ seed; 27 | 28 | const auto c1 = invXorShift(c0, m_key[0]); 29 | const auto c2 = invXorShift(c1, m_key[1]); 30 | const auto c3 = invXorShift(c2, m_key[2]); 31 | const auto c4 = invXorShift(c3, m_key[3]); 32 | 33 | return Key({{ 34 | c1 & ((1 << 17) - 1), 35 | c2 & ((1 << 19) - 1), 36 | c3 & ((1 << 23) - 1), 37 | c4 & ((1 << 29) - 1), 38 | }}); 39 | } 40 | 41 | template< 42 | typename InputIt, typename OutputIt, 43 | typename = typename std::enable_if_t< 44 | std::is_same::value_type, uint8_t>::value && 45 | std::is_same::value_type, uint8_t>::value 46 | > 47 | > 48 | void cryptBytes(InputIt srcFirst, InputIt srcLast, OutputIt dstFirst, uint32_t seed) const 49 | { 50 | typedef typename std::iterator_traits::value_type ValueType; 51 | 52 | auto c = computeKey(seed); 53 | 54 | std::transform( 55 | srcFirst, srcLast, dstFirst, 56 | [&c](const ValueType in) { 57 | const ValueType out = (((c[0] ^ c[1]) ^ in) ^ (c[2] ^ c[3])) & UINT8_C(0xFF); 58 | 59 | c[0] = ((rotateLeft(c[0], 9) & UINT32_C(0x1FE00)) | (c[0] >> 8)); 60 | c[1] = ((rotateLeft(c[1], 11) & UINT32_C(0x7F800)) | (c[1] >> 8)); 61 | c[2] = ((rotateLeft(c[2], 15) & UINT32_C(0x7F8000)) | (c[2] >> 8)); 62 | c[3] = ((rotateLeft(c[3], 21) & UINT32_C(0x1FE00000)) | (c[3] >> 8)); 63 | 64 | return out; 65 | } 66 | ); 67 | } 68 | 69 | template 70 | static void cryptBlocks(InputIt srcFirst, InputIt srcLast, OutputIt dstFirst) 71 | { 72 | cryptBlocksInternal(srcFirst, srcLast, dstFirst); 73 | } 74 | 75 | template 76 | static void cryptBlocksWithSwapEndian(InputIt srcFirst, InputIt srcLast, OutputIt dstFirst) 77 | { 78 | cryptBlocksInternal(srcFirst, srcLast, dstFirst); 79 | } 80 | 81 | const auto& magic() const { return m_magic; } 82 | 83 | const auto& key() const { return m_key; } 84 | auto key(size_t i) const { return m_key[i]; } 85 | 86 | private: 87 | static uint32_t xorShift(uint32_t x, uint32_t y) 88 | { 89 | auto result = x; 90 | const auto count = sizeof(x) * CHAR_BIT; 91 | for (auto i = 0u; i < count; ++i) { 92 | const auto hasUpperBit = (result & UINT32_C(0x80000000)) != 0; 93 | result <<= 1; 94 | if (hasUpperBit) { 95 | result ^= y; 96 | } 97 | } 98 | return result; 99 | } 100 | 101 | static uint32_t invXorShift(uint32_t x, uint32_t y) 102 | { 103 | return ~xorShift(x, y); 104 | } 105 | 106 | static uint32_t shuffleBits(uint32_t x) 107 | { 108 | auto crc = UINT32_C(0); 109 | for (auto i = 0; i < 4; ++i) { 110 | crc = (crc << 8) ^ g_crc32_0x04C11DB7[(rotateLeft(x ^ crc, 10) & 0x3FC) >> 2]; 111 | x <<= 8; 112 | } 113 | return ~crc; 114 | } 115 | 116 | static uint32_t cryptBlock(uint32_t x, uint32_t y) 117 | { 118 | return x ^ shuffleBits(y); 119 | } 120 | 121 | template< 122 | typename InputIt, typename OutputIt, bool NeedSwapEndian, 123 | typename = typename std::enable_if_t< 124 | std::is_same::value_type, uint32_t>::value && 125 | std::is_same::value_type, uint32_t>::value 126 | > 127 | > 128 | static void cryptBlocksInternal(InputIt srcFirst, InputIt srcLast, OutputIt dstFirst) 129 | { 130 | if (srcFirst == srcLast) { 131 | return; 132 | } 133 | 134 | typedef typename std::iterator_traits::value_type ValueType; 135 | using boost::endian::endian_reverse; 136 | 137 | ValueType prevBlock = endian_reverse(*srcFirst++); 138 | *dstFirst++ = (NeedSwapEndian ? prevBlock : endian_reverse(prevBlock)); 139 | 140 | if (srcFirst == srcLast) { 141 | return; 142 | } 143 | 144 | std::transform( 145 | srcFirst, srcLast, dstFirst, 146 | [&prevBlock](const ValueType in) { 147 | const ValueType curBlock = endian_reverse(in); 148 | ValueType out = cryptBlock(curBlock, prevBlock); 149 | if (!NeedSwapEndian) { 150 | out = endian_reverse(out); 151 | } 152 | prevBlock = curBlock; 153 | return out; 154 | } 155 | ); 156 | } 157 | 158 | const std::string m_magic; 159 | const Key m_key; 160 | }; 161 | 162 | class Salsa20Cipher 163 | { 164 | public: 165 | static const auto STATE_SIZE = 16u; 166 | static const auto BLOCK_SIZE = 64u; 167 | static const auto KEY_MAX_SIZE = 32u; 168 | 169 | Salsa20Cipher(const uint8_t key[KEY_MAX_SIZE], size_t keySize, const uint8_t* iv = nullptr) 170 | : m_state() 171 | { 172 | setKey(key, keySize); 173 | setIv(iv); 174 | } 175 | 176 | explicit Salsa20Cipher(const std::string& key, const uint8_t* iv = nullptr) 177 | : Salsa20Cipher(reinterpret_cast(key.c_str()), key.size(), iv) 178 | { 179 | } 180 | 181 | void setKey(const uint8_t key[KEY_MAX_SIZE], size_t keySize) 182 | { 183 | const auto sz = sizeof(uint32_t); 184 | 185 | uint8_t paddedKey[KEY_MAX_SIZE] = { 0 }; 186 | std::copy(key, key + keySize, paddedKey); 187 | 188 | if (keySize > KEY_MAX_SIZE / 2) { 189 | static const auto magic = reinterpret_cast("expand 32-byte k"); 190 | 191 | bytesToUint(magic + 0 * sz, m_state[0]); 192 | bytesToUint(paddedKey + 0 * sz, m_state[1]); 193 | bytesToUint(paddedKey + 1 * sz, m_state[2]); 194 | bytesToUint(paddedKey + 2 * sz, m_state[3]); 195 | bytesToUint(paddedKey + 3 * sz, m_state[4]); 196 | bytesToUint(magic + 1 * sz, m_state[5]); 197 | bytesToUint(magic + 2 * sz, m_state[10]); 198 | bytesToUint(paddedKey + 4 * sz, m_state[11]); 199 | bytesToUint(paddedKey + 5 * sz, m_state[12]); 200 | bytesToUint(paddedKey + 6 * sz, m_state[13]); 201 | bytesToUint(paddedKey + 7 * sz, m_state[14]); 202 | bytesToUint(magic + 3 * sz, m_state[15]); 203 | } else { 204 | static const auto magic = reinterpret_cast("expand 16-byte k"); 205 | 206 | bytesToUint(magic + 0 * sz, m_state[0]); 207 | bytesToUint(paddedKey + 0 * sz, m_state[1]); 208 | bytesToUint(paddedKey + 1 * sz, m_state[2]); 209 | bytesToUint(paddedKey + 2 * sz, m_state[3]); 210 | bytesToUint(paddedKey + 3 * sz, m_state[4]); 211 | bytesToUint(magic + 1 * sz, m_state[5]); 212 | bytesToUint(magic + 2 * sz, m_state[10]); 213 | bytesToUint(paddedKey + 0 * sz, m_state[11]); 214 | bytesToUint(paddedKey + 1 * sz, m_state[12]); 215 | bytesToUint(paddedKey + 2 * sz, m_state[13]); 216 | bytesToUint(paddedKey + 3 * sz, m_state[14]); 217 | bytesToUint(magic + 3 * sz, m_state[15]); 218 | } 219 | } 220 | 221 | void setIv(const uint8_t* iv) 222 | { 223 | if (iv) { 224 | const auto sz = sizeof(uint32_t); 225 | 226 | bytesToUint(iv + 0 * sz, m_state[6]); 227 | bytesToUint(iv + 1 * sz, m_state[7]); 228 | 229 | m_state[8] = m_state[9] = 0; 230 | } else { 231 | m_state[6] = m_state[7] = m_state[8] = m_state[9] = 0; 232 | } 233 | } 234 | 235 | void processBlocks(const uint8_t* in, uint8_t* out, size_t blockCount) 236 | { 237 | uint8_t keyStream[BLOCK_SIZE]; 238 | 239 | for (auto i = 0u; i < blockCount; ++i) { 240 | generateKeyStream(keyStream); 241 | 242 | for (auto j = 0u; j < BLOCK_SIZE; ++j) { 243 | *out++ = keyStream[j] ^ *in++; 244 | } 245 | } 246 | } 247 | 248 | void processBytes(const uint8_t* in, uint8_t* out, size_t byteCount) 249 | { 250 | uint8_t keyStream[BLOCK_SIZE]; 251 | 252 | while (byteCount != 0) { 253 | generateKeyStream(keyStream); 254 | 255 | const auto count = byteCount < BLOCK_SIZE ? byteCount : BLOCK_SIZE; 256 | for (auto i = 0u; i < count; ++i, --byteCount) 257 | *out++ = keyStream[i] ^ *in++; 258 | } 259 | } 260 | 261 | private: 262 | struct MatrixElement 263 | { 264 | const uint8_t outIndex; 265 | const uint8_t term1Index; 266 | const uint8_t term2Index; 267 | const uint8_t shift; 268 | }; 269 | 270 | static const std::array MATRIX; 271 | 272 | void generateKeyStream(uint8_t keyStream[BLOCK_SIZE]) 273 | { 274 | std::array newState = m_state; 275 | 276 | for (auto i = 20; i > 0; i -= 2) { 277 | for (const auto& el: MATRIX) { 278 | newState[el.outIndex] ^= rotateLeft(newState[el.term1Index] + newState[el.term2Index], el.shift); 279 | } 280 | } 281 | 282 | const auto sz = sizeof(uint32_t); 283 | for (auto i = 0u; i < STATE_SIZE; ++i) { 284 | newState[i] += m_state[i]; 285 | uintToBytes(keyStream + i * sz, newState[i]); 286 | } 287 | 288 | m_state[8]++; 289 | m_state[9] += (m_state[8] == 0) ? 1 : 0; 290 | } 291 | 292 | static uint8_t* uintToBytes(uint8_t* bytes, uint32_t value) 293 | { 294 | *bytes++ = static_cast(value >> 0); 295 | *bytes++ = static_cast(value >> 8); 296 | *bytes++ = static_cast(value >> 16); 297 | *bytes++ = static_cast(value >> 24); 298 | 299 | return bytes; 300 | } 301 | 302 | static const uint8_t* bytesToUint(const uint8_t* bytes, uint32_t& value) 303 | { 304 | value = static_cast(*bytes++) << 0; 305 | value |= static_cast(*bytes++) << 8; 306 | value |= static_cast(*bytes++) << 16; 307 | value |= static_cast(*bytes++) << 24; 308 | 309 | return bytes; 310 | } 311 | 312 | std::array m_state; 313 | }; -------------------------------------------------------------------------------- /src/volume.cpp: -------------------------------------------------------------------------------- 1 | #include "volume.hpp" 2 | #include "compression.hpp" 3 | #include "debug.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | static inline bool prepareStream(std::ifstream& stream, const std::string& filePath, uint64_t* fileSize = nullptr) 13 | { 14 | stream.open(filePath, std::ifstream::in | std::ifstream::binary); 15 | if (!stream.is_open()) { 16 | return false; 17 | } 18 | 19 | if (fileSize) { 20 | stream.seekg(0, std::ios_base::end); 21 | *fileSize = static_cast(stream.tellg()); 22 | stream.seekg(0, std::ios_base::beg); 23 | } 24 | 25 | return true; 26 | } 27 | 28 | bool VolumeFile::load(const std::string& filePath) 29 | { 30 | m_origPath = boost::filesystem::path(filePath); 31 | m_basePath = m_origPath.parent_path(); 32 | m_baseName = m_origPath.filename(); 33 | 34 | if (!prepareStream(m_mainStream, filePath, &m_mainFileSize)) { 35 | return false; 36 | } 37 | 38 | std::vector headerData; 39 | if (!readDataAt(headerData, 0, getHeaderSize())) { 40 | return false; 41 | } 42 | if (!decryptHeader(headerData.data(), headerData.size())) { 43 | return false; 44 | } 45 | if (!parseHeader(headerData.data(), headerData.size())) { 46 | return false; 47 | } 48 | if (!parseSegment()) { 49 | return false; 50 | } 51 | 52 | return true; 53 | } 54 | 55 | bool VolumeFile::readDataAt(std::ifstream& stream, std::vector& data, uint64_t offset, uint64_t size) 56 | { 57 | data.resize(size); 58 | 59 | stream.seekg(offset); 60 | stream.read(reinterpret_cast(data.data()), data.size()); 61 | 62 | return true; 63 | } 64 | 65 | bool VolumeFile::parseSegment() 66 | { 67 | const auto* p = m_data.data(); 68 | 69 | const auto segmentMagic = VOLUME_READ_NEXT_SELF(p, uint32_t); 70 | if (segmentMagic != SEGMENT_MAGIC) { 71 | return false; 72 | } 73 | 74 | m_nameTreeOffset = VOLUME_READ_NEXT_SELF(p, uint32_t); 75 | m_extTreeOffset = VOLUME_READ_NEXT_SELF(p, uint32_t); 76 | m_nodeTreeOffset = VOLUME_READ_NEXT_SELF(p, uint32_t); 77 | m_entryTreeCount = VOLUME_READ_NEXT_SELF(p, uint32_t); 78 | 79 | m_entryTreeOffsets.resize(m_entryTreeCount); 80 | for (auto& offset: m_entryTreeOffsets) { 81 | offset = VOLUME_READ_NEXT_SELF(p, uint32_t); 82 | } 83 | 84 | return true; 85 | } 86 | 87 | unsigned int VolumeFile::getNodeByPath(const std::string& filePath, NodeKey& nodeKey) const 88 | { 89 | if (m_entryTreeCount == 0) { 90 | return NodeBTree::INVALID_INDEX; 91 | } 92 | 93 | const auto normalizedFilePath = normalizeFilePath(filePath); 94 | std::vector parts; 95 | boost::algorithm::split( 96 | parts, 97 | normalizedFilePath, 98 | boost::algorithm::is_any_of("/"), 99 | boost::algorithm::token_compress_on 100 | ); 101 | 102 | if (parts.empty()) { 103 | return NodeBTree::INVALID_INDEX; 104 | } 105 | 106 | const StringBTree nameBtree( 107 | advancePointer(m_data.data(), nameTreeOffset()) 108 | ); 109 | const StringBTree extBtree( 110 | advancePointer(m_data.data(), extTreeOffset()) 111 | ); 112 | 113 | auto nodeIndex = NodeBTree::INVALID_INDEX; 114 | for (auto i = 0u, entryTreeIndex = 0u; i < parts.size(); ++i) { 115 | const auto& part = parts[i]; 116 | const auto dotPos = part.find_last_of('.'); 117 | 118 | const auto nameLength = (dotPos != std::string::npos) ? dotPos : part.size(); 119 | StringKey nameKey(part.c_str(), static_cast(nameLength)); 120 | const auto nameIndex = nameBtree.searchByKey(nameKey); 121 | if (nameIndex == StringBTree::INVALID_INDEX) { 122 | break; 123 | } 124 | 125 | auto extIndex = 0u; 126 | if (dotPos != std::string::npos) { 127 | const auto extPart = part.substr(dotPos); 128 | const auto extLength = extPart.size(); 129 | StringKey extKey(extPart.c_str(), static_cast(extLength)); 130 | extIndex = extBtree.searchByKey(extKey); 131 | if (extIndex == StringBTree::INVALID_INDEX) { 132 | break; 133 | } 134 | } 135 | 136 | const EntryBTree entryBtree( 137 | advancePointer(m_data.data(), entryTreeOffset(entryTreeIndex)) 138 | ); 139 | EntryKey entryKey(nameIndex, extIndex); 140 | const auto entryIndex = entryBtree.searchByKey(entryKey); 141 | if (entryIndex == EntryBTree::INVALID_INDEX) { 142 | break; 143 | } 144 | 145 | if (entryKey.isDirectory()) { 146 | entryTreeIndex = entryKey.linkIndex(); 147 | } else { 148 | nodeIndex = entryKey.linkIndex(); 149 | break; 150 | } 151 | } 152 | if (nodeIndex == NodeBTree::INVALID_INDEX) { 153 | return NodeBTree::INVALID_INDEX; 154 | } 155 | 156 | const NodeBTree nodeBtree( 157 | advancePointer(m_data.data(), nodeTreeOffset()), 158 | hasMultipleVolumes() 159 | ); 160 | nodeKey = NodeKey(nodeIndex); 161 | nodeIndex = nodeBtree.searchByKey(nodeKey); 162 | if (nodeIndex == NodeBTree::INVALID_INDEX) { 163 | return NodeBTree::INVALID_INDEX; 164 | } 165 | 166 | return nodeIndex; 167 | } 168 | 169 | class EntryUnpacker 170 | { 171 | public: 172 | explicit EntryUnpacker(VolumeFile& volume, const std::string& outDirectory, const std::string& parentDirectory = std::string()) 173 | : m_volume(volume) 174 | , m_outDirectory(outDirectory) 175 | , m_parentDirectory(parentDirectory) 176 | { 177 | } 178 | 179 | bool operator ()(const EntryKey& entryKey) const 180 | { 181 | const auto entryPath = m_volume.getEntryPath(entryKey, m_parentDirectory); 182 | if (entryPath.empty()) { 183 | std::cerr << "Cannot determine entry path." << std::endl; 184 | return false; 185 | } 186 | 187 | const auto fullEntryPath = boost::filesystem::path(m_outDirectory) / entryPath; 188 | 189 | if (entryKey.isDirectory()) { 190 | std::cout << "DIR:" << entryPath << std::endl; 191 | //entryKey.dump(); 192 | 193 | boost::filesystem::create_directories(fullEntryPath); 194 | 195 | const EntryBTree childEntryBtree( 196 | advancePointer(m_volume.data().data(), m_volume.entryTreeOffset(entryKey.linkIndex())) 197 | ); 198 | const EntryUnpacker childUnpacker(m_volume, m_outDirectory, entryPath); 199 | childEntryBtree.traverse(childUnpacker); 200 | } else { 201 | std::cout << "FILE:" << entryPath << std::endl; 202 | //entryKey.dump(); 203 | 204 | const NodeBTree nodeBtree( 205 | advancePointer(m_volume.data().data(), m_volume.nodeTreeOffset()), 206 | m_volume.hasMultipleVolumes() 207 | ); 208 | NodeKey nodeKey(entryKey.linkIndex()); 209 | const auto nodeIndex = nodeBtree.searchByKey(nodeKey); 210 | bool unpacked = false; 211 | if (nodeIndex != NodeBTree::INVALID_INDEX) { 212 | if (m_volume.unpackNode(nodeKey, fullEntryPath.string())) { 213 | unpacked = true; 214 | } 215 | } 216 | if (!unpacked) { 217 | std::cerr << boost::format("Cannot unpack node: %s") % fullEntryPath.string() << std::endl; 218 | return false; 219 | } 220 | } 221 | 222 | return true; 223 | } 224 | 225 | private: 226 | VolumeFile& m_volume; 227 | const std::string& m_outDirectory; 228 | std::string m_parentDirectory; 229 | }; 230 | 231 | bool VolumeFile::unpackNode(const NodeKey& nodeKey, const std::string& filePath) 232 | { 233 | const auto volumeIndex = nodeKey.volumeIndex(); 234 | if (volumeIndex >= m_dataStreams.size()) { 235 | return false; 236 | } 237 | auto& streamDesc = m_dataStreams[volumeIndex]; 238 | 239 | const auto offset = dataOffset() + static_cast(nodeKey.sectorIndex()) * streamDesc.sectorSize; 240 | const auto uncompressedSize = nodeKey.size2(); 241 | 242 | std::vector data; 243 | if (!readDataAt(streamDesc.stream, data, offset, nodeKey.size1())) { 244 | return false; 245 | } 246 | 247 | decryptData(data.data(), data.size(), nodeKey.nodeIndex()); 248 | inflateDataIfNeeded(data, uncompressedSize); 249 | 250 | if (FileExpand::checkIfExpanded(data)) { 251 | std::vector unexpandedData; 252 | if (FileExpand::unexpand(data, unexpandedData)) { 253 | saveToFile(filePath, unexpandedData.data(), unexpandedData.size()); 254 | } else { 255 | std::cerr << "Error whilst unexpanding file: " << filePath << std::endl; 256 | } 257 | } else { 258 | saveToFile(filePath, data.data(), data.size()); 259 | } 260 | 261 | return true; 262 | } 263 | 264 | bool VolumeFile::unpackAll(const std::string& outDirectory) 265 | { 266 | if (m_entryTreeCount == 0) { 267 | return false; 268 | } 269 | 270 | const EntryBTree rootEntryBtree( 271 | advancePointer(m_data.data(), entryTreeOffset(0)) 272 | ); 273 | const EntryUnpacker unpacker(*this, outDirectory); 274 | rootEntryBtree.traverse(unpacker); 275 | 276 | return true; 277 | } 278 | 279 | bool VolumeFile::decryptHeader(uint8_t* header, uint64_t headerSize) const 280 | { 281 | if (!decryptData(header, headerSize, 1)) { 282 | return false; 283 | } 284 | 285 | const auto& keyset = getKeyset(); 286 | 287 | auto* beg = reinterpret_cast(header); 288 | auto* end = beg + headerSize / sizeof(*beg); 289 | 290 | if (!needSwapEndian()) { // TODO: rewrite 291 | keyset.cryptBlocksWithSwapEndian(beg, end, beg); 292 | } else { 293 | keyset.cryptBlocks(beg, end, beg); 294 | } 295 | 296 | return true; 297 | } 298 | 299 | bool VolumeFile::decryptData(uint8_t* data, uint64_t dataSize, uint32_t seed) const 300 | { 301 | if (!data) { 302 | return false; 303 | } 304 | 305 | if (dataSize > 0) { 306 | const auto& keyset = getKeyset(); 307 | keyset.cryptBytes(data, data + dataSize, data, seed); 308 | } 309 | 310 | return true; 311 | } 312 | 313 | bool VolumeFile::inflateDataIfNeeded(std::vector& in, uint64_t outSize) const { 314 | if (outSize > UINT32_MAX) 315 | return false; 316 | 317 | const auto* p = in.data(); 318 | 319 | // XXX: inflated data use little-endian always. 320 | const auto magic = readNext(p); 321 | const auto sizeComplement = readNext(p); 322 | 323 | if (magic != Z_MAGIC || (static_cast(outSize) + sizeComplement) != 0) { // not compressed? 324 | return false; 325 | } 326 | 327 | const auto headerSize = sizeof(magic) + sizeof(sizeComplement); 328 | if (in.size() < headerSize) { 329 | return false; 330 | } 331 | 332 | std::vector out; 333 | FileExpand::inflate(out, p, in.size() - headerSize); 334 | in.swap(out); 335 | 336 | return true; 337 | } 338 | 339 | std::string VolumeFile::getEntryPath(const EntryKey& entryKey, const std::string& prefix) const 340 | { 341 | std::string path(prefix); 342 | 343 | StringBTree nameBtree( 344 | advancePointer(m_data.data(), nameTreeOffset()) 345 | ); 346 | StringKey nameKey; 347 | if (nameBtree.searchByIndex(entryKey.nameIndex(), nameKey)) { 348 | path.append(nameKey.value(), nameKey.value() + nameKey.length()); 349 | } 350 | 351 | if (entryKey.isFile()) { 352 | StringBTree extBtree( 353 | advancePointer(m_data.data(), extTreeOffset()) 354 | ); 355 | StringKey extKey; 356 | if (extBtree.searchByIndex(entryKey.extIndex(), extKey)) { 357 | if (extKey.length() > 0) { 358 | path.append(extKey.value(), extKey.value() + extKey.length()); 359 | } 360 | } 361 | } else if (entryKey.isDirectory()) { 362 | path += '/'; 363 | } 364 | 365 | return path; 366 | } 367 | 368 | const Keyset& GT5VolumeFile::getKeyset() const 369 | { 370 | static const Keyset keyset({ 371 | "KALAHARI-37863889", {{ 0x2DEE26A7, 0x412D99F5, 0x883C94E9, 0x0F1A7069 }} 372 | }); 373 | return keyset; 374 | } 375 | 376 | bool GT5VolumeFile::parseHeader(const uint8_t* header, uint64_t headerSize) 377 | { 378 | const auto* p = header; 379 | 380 | const auto headerSizeAligned = SEGMENT_SIZE; 381 | 382 | const auto magic = VOLUME_READ_NEXT_SELF(p, uint32_t); 383 | if (magic != HEADER_MAGIC) { 384 | return false; 385 | } 386 | 387 | const auto seed = VOLUME_READ_NEXT_SELF(p, uint32_t); // TODO: segment index actually? 388 | const auto zDataSize = VOLUME_READ_NEXT_SELF(p, uint32_t); // with header 389 | const auto dataSize = VOLUME_READ_NEXT_SELF(p, uint32_t); 390 | const auto unk = VOLUME_READ_NEXT_SELF(p, uint64_t); 391 | const auto fileSize = VOLUME_READ_NEXT_SELF(p, uint64_t); 392 | UNUSED(fileSize); 393 | 394 | char titleId[128]; 395 | VOLUME_READN_NEXT_SELF(p, char, titleId, sizeof(titleId)); 396 | m_titleId = titleId; 397 | 398 | std::vector data; 399 | if (!readDataAt(data, headerSizeAligned, zDataSize)) { 400 | return false; 401 | } 402 | decryptData(data.data(), data.size(), seed); 403 | if (!inflateDataIfNeeded(data, dataSize)) { 404 | return false; 405 | } 406 | 407 | m_dataOffset = alignUp(headerSizeAligned + zDataSize, SEGMENT_SIZE); 408 | m_data.swap(data); 409 | 410 | m_dataStreams.emplace_back(); 411 | auto& streamDesc = m_dataStreams.back(); 412 | { 413 | streamDesc.filePath = m_origPath.string(); 414 | if (!prepareStream(streamDesc.stream, streamDesc.filePath, &streamDesc.fileSize)) { 415 | return false; 416 | } 417 | std::cout << boost::format("Data file size: %1%") % streamDesc.fileSize << std::endl; 418 | } 419 | 420 | return true; 421 | } 422 | 423 | const Keyset& GT6VolumeFile::getKeyset() const 424 | { 425 | static const Keyset keyset({ 426 | "PISCINAS-323419048", {{ 0xAA1B6A59, 0xE70B6FB3, 0x62DC6095, 0x6A594A25 }} 427 | }); 428 | return keyset; 429 | } 430 | 431 | const Keyset& GT7VolumeFile::getKeyset() const 432 | { 433 | static const Keyset keyset({ 434 | "KYZYLKUM-873068469", {{ 0xC9DA80A5, 0x050DA9A1, 0x9EB1FE65, 0xB651F2FB }} 435 | }); 436 | return keyset; 437 | } 438 | 439 | bool GT7VolumeFile::decryptHeader(uint8_t* header, uint64_t headerSize) const 440 | { 441 | const bool result = VolumeFile::decryptHeader(header, headerSize); 442 | if (result) { 443 | const auto blocks = reinterpret_cast(header); 444 | blocks[0] ^= UINT32_C(0x9AEFDE67); 445 | } 446 | return result; 447 | } 448 | 449 | bool GT7VolumeFile::parseHeader(const uint8_t* header, uint64_t headerSize) 450 | { 451 | const auto* p = header; 452 | 453 | const auto headerSizeAligned = SEGMENT_SIZE; 454 | 455 | const auto magic = VOLUME_READ_NEXT_SELF(p, uint32_t); 456 | if (magic != HEADER_MAGIC) { 457 | return false; 458 | } 459 | 460 | // TODO: figure out what are these fields 461 | const auto hdr_0x1CC = VOLUME_READ_NEXT_SELF(p, uint32_t); // 0x04 462 | const auto hdr_0x1D0 = VOLUME_READ_NEXT_SELF(p, uint32_t); // 0x08 463 | const auto hdr_0x1D4 = VOLUME_READ_NEXT_SELF(p, uint32_t); // 0x0C 464 | const auto hdr_0x1D8 = VOLUME_READ_NEXT_SELF(p, uint32_t); // 0x10 465 | 466 | advancePointerInplace(p, 0xDC); 467 | 468 | const auto seed = VOLUME_READ_NEXT_SELF(p, uint32_t); 469 | const auto zDataSize = VOLUME_READ_NEXT_SELF(p, uint32_t); // with header 470 | const auto dataSize = VOLUME_READ_NEXT_SELF(p, uint32_t); 471 | const auto volumeCount = VOLUME_READ_NEXT_SELF(p, uint32_t); 472 | 473 | m_volumes.resize(volumeCount); 474 | std::copy_n(reinterpret_cast(p), m_volumes.size(), m_volumes.begin()); 475 | for (auto& volumeInfo: m_volumes) { 476 | volumeInfo.fileSize = (volumeInfo.fileSize >> 32) | ((volumeInfo.fileSize & 0xFFFFFFFF) << 32); 477 | } 478 | 479 | std::vector data; 480 | if (!readDataAt(data, headerSizeAligned, zDataSize)) { 481 | return false; 482 | } 483 | decryptData(data.data(), data.size(), seed); 484 | if (!inflateDataIfNeeded(data, dataSize)) { 485 | return false; 486 | } 487 | 488 | m_dataOffset = 0; 489 | m_data.swap(data); 490 | 491 | for (auto& volumeInfo: m_volumes) { 492 | m_dataStreams.emplace_back(); 493 | auto& streamDesc = m_dataStreams.back(); 494 | { 495 | streamDesc.filePath = (m_basePath / volumeInfo.fileName).string(); 496 | if (!prepareStream(streamDesc.stream, streamDesc.filePath, &streamDesc.fileSize)) { 497 | return false; 498 | } 499 | if (!parseExtendedHeader(streamDesc)) { 500 | return false; 501 | } 502 | } 503 | } 504 | 505 | return true; 506 | } 507 | 508 | bool GT7VolumeFile::parseExtendedHeader(StreamDesc& streamDesc) 509 | { 510 | streamDesc.extHeader.clear(); 511 | 512 | if (!readDataAt(streamDesc.stream, streamDesc.extHeader, 0, sizeof(ExtHeader))) { 513 | return false; 514 | } 515 | 516 | const auto& header = *reinterpret_cast(streamDesc.extHeader.data()); 517 | if (header.magic != EXT_HEADER_MAGIC) { 518 | return false; 519 | } 520 | if (header.sectorSize == 0 || (header.sectorSize % EXT_ALIGNMENT != 0)) { 521 | return false; 522 | } 523 | if (header.segmentSize == 0 || (header.segmentSize % EXT_ALIGNMENT != 0)) { 524 | return false; 525 | } 526 | 527 | streamDesc.sectorSize = header.sectorSize; 528 | streamDesc.segmentSize = header.segmentSize; 529 | 530 | return true; 531 | } 532 | 533 | std::string GT7VolumeFile::normalizeFilePath(const std::string& path) const 534 | { 535 | return boost::to_upper_copy( 536 | boost::algorithm::trim_left_copy(path) 537 | ); 538 | } 539 | -------------------------------------------------------------------------------- /src/btree.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "io_util.hpp" 4 | #include "debug.hpp" // TODO: temporarily 5 | 6 | #include 7 | #include // TODO: temporarily 8 | #include // TODO: temporarily 9 | 10 | #include 11 | 12 | static inline uint16_t getBitsAt(const uint8_t* data, uint32_t offset) 13 | { 14 | const auto offsetAligned = (offset * 12) / 8; 15 | auto result = readAtWithByteSwap(data, offsetAligned); 16 | if ((offset & 0x1) == 0) 17 | result >>= 4; 18 | return result & UINT16_C(0xFFF); 19 | } 20 | 21 | static inline uint64_t decodeBitsAndAdvance(const uint8_t*& data) 22 | { 23 | uint64_t value = *data++; 24 | uint64_t mask = 0x80; 25 | while (value & mask) { 26 | value = ((value - mask) << 8) | (*data++); 27 | mask <<= 7; 28 | } 29 | return value; 30 | } 31 | 32 | template 33 | class BTree 34 | { 35 | private: 36 | struct SearchResult 37 | { 38 | SearchResult() 39 | : lowerBound(0) 40 | , upperBound(0) 41 | , index(INVALID_INDEX) 42 | , maxIndex(0) 43 | { 44 | } 45 | 46 | unsigned int lowerBound; 47 | unsigned int upperBound; 48 | 49 | unsigned int index; 50 | unsigned int maxIndex; 51 | }; 52 | 53 | typedef int (Derived::*KeyCompareOp)(const Key& key, const uint8_t* data) const; 54 | 55 | public: 56 | typedef Key KeyType; 57 | 58 | enum class CallbackResult 59 | { 60 | STOP, 61 | CONTINUE, 62 | }; 63 | 64 | static const auto INVALID_INDEX = ~0u; 65 | 66 | const uint8_t* getByIndex(unsigned int index) const 67 | { 68 | const auto& self = static_cast(*this); 69 | 70 | unsigned int childNodeCount = readAtWithByteSwap(m_data, 0); 71 | 72 | unsigned int nodeDataOffset = readAtWithByteSwap(m_data, 0) & UINT32_C(0xFFFFFF); 73 | unsigned int nodeOffset; 74 | 75 | const uint8_t* nodeData = advancePointer(m_data, nodeDataOffset); 76 | const uint8_t* node; 77 | 78 | unsigned int startKeyIndex = 0, nextKeyIndex, keyCount; 79 | bool found = false; 80 | for (auto i = childNodeCount; i != 0 && !found; --i) { 81 | keyCount = getBitsAt(nodeData, 0) & 0x7FFu; 82 | node = nullptr; 83 | for (auto j = 0u; j < keyCount; ++j) { 84 | nodeOffset = getBitsAt(nodeData, j + 1); 85 | node = advancePointer(nodeData, nodeOffset); 86 | nextKeyIndex = static_cast(decodeBitsAndAdvance(node)); 87 | if (index < nextKeyIndex) { 88 | found = true; 89 | break; 90 | } 91 | startKeyIndex = nextKeyIndex; 92 | } 93 | if (!node) 94 | break; 95 | 96 | node = self.advanceData(node); 97 | 98 | nodeDataOffset = static_cast(decodeBitsAndAdvance(node)); 99 | nodeData = advancePointer(m_data, nodeDataOffset); 100 | } 101 | 102 | keyCount = getBitsAt(nodeData, 0) & 0x7FFu; 103 | nodeOffset = getBitsAt(nodeData, (index - startKeyIndex) + 1); 104 | node = advancePointer(nodeData, nodeOffset); 105 | 106 | return node; 107 | } 108 | 109 | bool getByIndex(unsigned int index, Key& key) const 110 | { 111 | const auto& self = static_cast(*this); 112 | const uint8_t* data = getByIndex(index); 113 | if (!data) 114 | return false; 115 | return self.parseData(key, data) != nullptr; 116 | } 117 | 118 | bool searchByIndexOldest(unsigned int index, Key& key) const 119 | { 120 | return getByIndex(index, key); 121 | } 122 | 123 | bool searchByIndex(unsigned int index, Key& key) const 124 | { 125 | const auto& self = static_cast(*this); 126 | 127 | auto p = m_data; 128 | 129 | const auto offsetAndCount = readNextWithByteSwap(p); 130 | const auto nodeCount = readNextWithByteSwap(p); 131 | 132 | UNUSED(offsetAndCount); 133 | 134 | for (auto i = 0u; i < nodeCount; ++i) { 135 | const auto high = getBitsAt(p, 0) & UINT32_C(0x7FF); 136 | const auto nextOffset = getBitsAt(p, high + 1); 137 | 138 | if (index < high) 139 | break; 140 | index -= high; 141 | 142 | p = advancePointer(p, nextOffset); 143 | } 144 | 145 | const auto offset = getBitsAt(p, index + 1); 146 | p = advancePointer(p, offset); 147 | 148 | return self.parseData(key, p) != nullptr; 149 | } 150 | 151 | unsigned int searchByKey(Key& key) const 152 | { 153 | const auto& self = static_cast(*this); 154 | 155 | const auto count = static_cast(readAtWithByteSwap(m_data, 0)); 156 | const auto offset = readAtWithByteSwap(m_data, 0) & UINT32_C(0xFFFFFF); 157 | 158 | auto data = advancePointer(m_data, offset); 159 | SearchResult result; 160 | for (auto i = count; i != 0; i--) { 161 | data = searchWithComparison(result, data, count, key, &Derived::lessThanKeyCompareOp); 162 | if (!data) 163 | goto done; 164 | 165 | result.maxIndex = static_cast(decodeBitsAndAdvance(data)); 166 | 167 | data = self.advanceData(data); 168 | 169 | const auto nextOffset = static_cast(decodeBitsAndAdvance(data)); 170 | data = advancePointer(m_data, nextOffset); 171 | } 172 | 173 | data = searchWithComparison(result, data, 0, key, &Derived::equalKeyCompareOp); 174 | 175 | done: 176 | if (count == 0) 177 | result.upperBound = 0; 178 | 179 | if (data) { 180 | const auto index = (result.maxIndex - result.upperBound + result.lowerBound); 181 | self.parseData(key, data); 182 | return index; 183 | } else { 184 | return INVALID_INDEX; 185 | } 186 | } 187 | 188 | template 189 | unsigned int traverse(TraverseFunctor& traverseFunctor) const 190 | { 191 | const auto& self = static_cast(*this); 192 | 193 | auto p = m_data; 194 | 195 | const auto offsetAndCount = readNextWithByteSwap(p); 196 | const auto nodeCount = readNextWithByteSwap(p); 197 | 198 | UNUSED(offsetAndCount); 199 | 200 | auto visitedKeyCount = 0u; 201 | for (auto i = 0u; i < nodeCount; ++i) { 202 | const auto high = getBitsAt(p, 0) & UINT32_C(0x7FF); 203 | const auto nextOffset = getBitsAt(p, high + 1); 204 | 205 | for (auto j = 0u; j < high; ++j) { 206 | const auto offset = getBitsAt(p, j + 1); 207 | const auto data = advancePointer(p, offset); 208 | 209 | Key key; 210 | if (self.parseData(key, data)) { 211 | ++visitedKeyCount; 212 | if (!traverseFunctor(key)) 213 | goto done; 214 | } 215 | } 216 | 217 | p = advancePointer(p, nextOffset); 218 | } 219 | 220 | done: 221 | return visitedKeyCount; 222 | } 223 | 224 | protected: 225 | explicit BTree(const uint8_t* data) 226 | : m_data(data) 227 | { 228 | } 229 | 230 | const uint8_t* searchWithComparison(SearchResult& result, const uint8_t* data, unsigned int count, const Key& key, KeyCompareOp compOp) const 231 | { 232 | const auto& self = static_cast(*this); 233 | 234 | auto high = getBitsAt(data, 0) & UINT32_C(0x7FF), low = 0u; 235 | auto index = 0u; 236 | const uint8_t* subData; 237 | 238 | result.upperBound = high; 239 | 240 | while (low < high) { 241 | const auto mid = low + (high - low) / 2; 242 | index = mid + 1; 243 | 244 | const auto offset = getBitsAt(data, index); 245 | subData = advancePointer(data, offset); 246 | 247 | const auto ret = (self.*compOp)(key, subData); 248 | if (ret == 0) { 249 | result.lowerBound = mid; 250 | result.index = mid; 251 | goto done; 252 | } else if (ret > 0) { 253 | low = index; 254 | } else if (ret < 0) { 255 | high = mid; 256 | index = mid; 257 | } 258 | } 259 | 260 | subData = nullptr; 261 | 262 | result.lowerBound = index; 263 | result.index = INVALID_INDEX; 264 | 265 | if (count != 0 && index != result.upperBound) { 266 | const auto offset = getBitsAt(data, index + 1); 267 | subData = advancePointer(data, offset); 268 | } 269 | 270 | done: 271 | return subData; 272 | } 273 | 274 | const uint8_t* parseData(KeyType& key, const uint8_t* data) const { return nullptr; } 275 | const uint8_t* advanceData(const uint8_t* data) const { return nullptr; } 276 | 277 | int equalKeyCompareOp(const KeyType& key, const uint8_t* data) const { return -1; } 278 | int lessThanKeyCompareOp(const KeyType& key, const uint8_t* data) const { return -1; } 279 | 280 | CallbackResult traverseCallback(const uint8_t* data) const { return CallbackResult::CONTINUE; } 281 | 282 | const uint8_t* m_data; 283 | }; 284 | 285 | class StringKey 286 | { 287 | public: 288 | StringKey() 289 | : m_value(nullptr) 290 | , m_length(0) 291 | { 292 | } 293 | 294 | explicit StringKey(const char* value, uint32_t length = ~0u) 295 | : StringKey() 296 | { 297 | setValue(value); 298 | 299 | if (length == ~0u) 300 | length = static_cast(strlen(value)); 301 | setLength(length); 302 | } 303 | 304 | StringKey(const StringKey& other) = default; 305 | 306 | void dump() const; 307 | 308 | StringKey& setValue(const char* value) 309 | { 310 | m_value = value; 311 | return *this; 312 | } 313 | 314 | const auto* value() const { return m_value; } 315 | 316 | StringKey& setLength(uint32_t length) 317 | { 318 | m_length = length; 319 | return *this; 320 | } 321 | 322 | auto length() const { return m_length; } 323 | 324 | private: 325 | const char* m_value; 326 | unsigned int m_length; 327 | }; 328 | 329 | class EntryKey 330 | { 331 | public: 332 | static const auto INVALID_INDEX = ~0u; 333 | 334 | static const auto FLAG_DIRECTORY = (1u << 0); 335 | static const auto FLAG_FILE = (1u << 1); 336 | 337 | EntryKey() 338 | : m_flags(0) 339 | , m_nameIndex(INVALID_INDEX) 340 | , m_extIndex(INVALID_INDEX) 341 | , m_linkIndex(INVALID_INDEX) 342 | { 343 | } 344 | 345 | explicit EntryKey(uint32_t nameIndex, uint32_t extIndex = 0) 346 | : EntryKey() 347 | { 348 | setNameIndex(nameIndex); 349 | setExtIndex(extIndex); 350 | } 351 | 352 | EntryKey(const EntryKey& other) = default; 353 | 354 | void dump() const; 355 | 356 | EntryKey& setFlags(uint32_t flags) 357 | { 358 | m_flags = flags; 359 | return *this; 360 | } 361 | 362 | auto flags() const { return m_flags; } 363 | 364 | EntryKey& setNameIndex(uint32_t index) 365 | { 366 | m_nameIndex = index; 367 | return *this; 368 | } 369 | 370 | auto nameIndex() const { return m_nameIndex; } 371 | 372 | // If no extension is needed then index should be 0. 373 | EntryKey& setExtIndex(uint32_t index) 374 | { 375 | m_extIndex = index; 376 | return *this; 377 | } 378 | 379 | auto extIndex() const { return m_extIndex; } 380 | 381 | EntryKey& setLinkIndex(uint32_t index) 382 | { 383 | m_linkIndex = index; 384 | return *this; 385 | } 386 | 387 | // For directory entries it points to next entry tree index, for file entries it points to node index. 388 | auto linkIndex() const { return m_linkIndex; } 389 | 390 | bool isDirectory() const { return (m_flags & FLAG_DIRECTORY) != 0; } 391 | bool isFile() const { return (m_flags & FLAG_FILE) != 0; } 392 | 393 | private: 394 | uint32_t m_flags; 395 | uint32_t m_nameIndex; 396 | uint32_t m_extIndex; 397 | uint32_t m_linkIndex; 398 | }; 399 | 400 | class NodeKey 401 | { 402 | public: 403 | static const auto INVALID_INDEX = ~0u; 404 | 405 | static const auto FLAG_COMPRESSED = (1u << 0); 406 | static const auto FLAG_BIT1 = (1u << 1); // TODO: figure out 407 | static const auto FLAG_BIT4 = (1u << 4); // TODO: figure out 408 | static const auto FLAG_BIT5 = (1u << 5); // TODO: figure out 409 | static const auto FLAG_BIT0123 = 0xFu; // TODO: figure out 410 | static const auto FLAG_0x13 = FLAG_COMPRESSED | FLAG_BIT1 | FLAG_BIT4; 411 | 412 | NodeKey() 413 | : m_flags(0) 414 | , m_nodeIndex(INVALID_INDEX) 415 | , m_size1(0) 416 | , m_size2(0) 417 | , m_volumeIndex(INVALID_INDEX) 418 | , m_sectorIndex(0) 419 | { 420 | } 421 | 422 | explicit NodeKey(uint32_t nodeIndex) 423 | : NodeKey() 424 | { 425 | setNodeIndex(nodeIndex); 426 | } 427 | 428 | NodeKey(const NodeKey& other) = default; 429 | 430 | void dump() const; 431 | 432 | NodeKey& setFlags(uint32_t flags) 433 | { 434 | m_flags = flags; 435 | return *this; 436 | } 437 | 438 | auto flags() const { return m_flags; } 439 | 440 | NodeKey& setNodeIndex(uint32_t index) 441 | { 442 | m_nodeIndex = index; 443 | return *this; 444 | } 445 | 446 | auto nodeIndex() const { return m_nodeIndex; } 447 | 448 | NodeKey& setSize1(uint32_t size) 449 | { 450 | m_size1 = size; 451 | return *this; 452 | } 453 | 454 | auto size1() const { return m_size1; } 455 | 456 | NodeKey& setSize2(uint32_t size) 457 | { 458 | m_size2 = size; 459 | return *this; 460 | } 461 | 462 | auto size2() const { return m_size2; } 463 | 464 | NodeKey& setVolumeIndex(uint32_t index) 465 | { 466 | m_volumeIndex = index; 467 | return *this; 468 | } 469 | 470 | auto volumeIndex() const { return m_volumeIndex; } 471 | 472 | NodeKey& setSectorIndex(uint32_t index) 473 | { 474 | m_sectorIndex = index; 475 | return *this; 476 | } 477 | 478 | auto sectorIndex() const { return m_sectorIndex; } 479 | 480 | // TODO: figure out 481 | bool hasCompression() const { return (m_flags & FLAG_COMPRESSED) != 0; } 482 | bool hasBit4() const { return (m_flags & FLAG_BIT4) != 0; } // TODO: figure out 483 | bool hasBit5() const { return (m_flags & FLAG_BIT5) != 0; } // TODO: figure out 484 | bool hasBits0123() const { return (m_flags & FLAG_BIT0123) != 0; } // TODO: figure out 485 | 486 | private: 487 | uint32_t m_flags; 488 | uint32_t m_nodeIndex; 489 | uint32_t m_size1; 490 | uint32_t m_size2; 491 | uint32_t m_volumeIndex; 492 | uint32_t m_sectorIndex; 493 | }; 494 | 495 | class StringBTree 496 | : public BTree 497 | { 498 | friend class BTree; 499 | 500 | public: 501 | explicit StringBTree(const uint8_t* data) 502 | : BTree(data) 503 | { 504 | } 505 | 506 | protected: 507 | const uint8_t* parseData(KeyType& key, const uint8_t* data) const 508 | { 509 | const auto length = static_cast(decodeBitsAndAdvance(data)); 510 | key.setLength(length); 511 | 512 | const auto value = reinterpret_cast(data); 513 | key.setValue(value); 514 | 515 | data += length; 516 | 517 | return data; 518 | } 519 | 520 | const uint8_t* advanceData(const uint8_t* data) const 521 | { 522 | const auto length = static_cast(decodeBitsAndAdvance(data)); 523 | data += length; 524 | return data; 525 | } 526 | 527 | int equalKeyCompareOp(const KeyType& key, const uint8_t* data) const 528 | { 529 | const auto curLength = key.length(); 530 | const auto curValue = key.value(); 531 | const auto otherLength = static_cast(decodeBitsAndAdvance(data)); 532 | const auto otherValue = reinterpret_cast(data); 533 | const auto minLength = std::min(key.length(), otherLength); 534 | 535 | for (auto i = 0u; i < minLength; ++i) { 536 | if (curValue[i] < otherValue[i]) 537 | return -1; 538 | else if (curValue[i] > otherValue[i]) 539 | return 1; 540 | } 541 | 542 | if (curLength < otherLength) 543 | return -1; 544 | else if (curLength > otherLength) 545 | return 1; 546 | 547 | return 0; 548 | } 549 | 550 | int lessThanKeyCompareOp(const KeyType& key, const uint8_t* data) const 551 | { 552 | // TODO: figure out 553 | const auto unk = static_cast(decodeBitsAndAdvance(data)); 554 | 555 | const auto result = equalKeyCompareOp(key, data); 556 | 557 | return (result != 0) ? result : 1; 558 | } 559 | 560 | CallbackResult traverseCallback(const uint8_t* data) const; 561 | }; 562 | 563 | class EntryBTree 564 | : public BTree 565 | { 566 | friend class BTree; 567 | 568 | public: 569 | explicit EntryBTree(const uint8_t* data) 570 | : BTree(data) 571 | { 572 | } 573 | 574 | protected: 575 | const uint8_t* parseData(KeyType& key, const uint8_t* data) const 576 | { 577 | const auto flags = readNext(data); 578 | key.setFlags(flags); 579 | 580 | const auto nameIndex = static_cast(decodeBitsAndAdvance(data)); 581 | key.setNameIndex(nameIndex); 582 | 583 | const auto extIndex = key.isFile() ? static_cast(decodeBitsAndAdvance(data)) : 0; 584 | key.setExtIndex(extIndex); 585 | 586 | const auto linkIndex = static_cast(decodeBitsAndAdvance(data)); 587 | key.setLinkIndex(linkIndex); 588 | 589 | return data; 590 | } 591 | 592 | const uint8_t* advanceData(const uint8_t* data) const 593 | { 594 | // TODO: figure out 595 | const auto unk = static_cast(decodeBitsAndAdvance(data)); 596 | 597 | return data; 598 | } 599 | 600 | int equalKeyCompareOp(const KeyType& key, const uint8_t* data) const 601 | { 602 | const auto flags = readNext(data); 603 | 604 | const auto nameIndex = static_cast(decodeBitsAndAdvance(data)); 605 | if (key.nameIndex() < nameIndex) 606 | return -1; 607 | else if (key.nameIndex() > nameIndex) 608 | return 1; 609 | 610 | const auto extIndex = (flags & EntryKey::FLAG_FILE) != 0 ? static_cast(decodeBitsAndAdvance(data)) : 0; 611 | if (key.extIndex() < extIndex) 612 | return -1; 613 | else if (key.extIndex() > extIndex) 614 | return 1; 615 | 616 | return 0; 617 | } 618 | 619 | int lessThanKeyCompareOp(const KeyType& key, const uint8_t* data) const 620 | { 621 | const auto nameIndex = static_cast(decodeBitsAndAdvance(data)); 622 | if (key.nameIndex() < nameIndex) 623 | return -1; 624 | else if (key.nameIndex() > nameIndex) 625 | return 1; 626 | 627 | const auto extIndex = static_cast(decodeBitsAndAdvance(data)); 628 | if (key.extIndex() < extIndex) 629 | return -1; 630 | else if (key.extIndex() > extIndex) 631 | return 1; 632 | else 633 | UNREACHABLE_CODE(0xDEAD); 634 | } 635 | 636 | CallbackResult traverseCallback(const uint8_t* data) const; 637 | }; 638 | 639 | class NodeBTree 640 | : public BTree 641 | { 642 | friend class BTree; 643 | 644 | public: 645 | explicit NodeBTree(const uint8_t* data, bool hasMultipleVolumes) 646 | : BTree(data) 647 | , m_hasMultipleVolumes(hasMultipleVolumes) 648 | { 649 | } 650 | 651 | protected: 652 | const uint8_t* parseData(KeyType& key, const uint8_t* data) const 653 | { 654 | const auto flags = readNext(data); 655 | key.setFlags(flags); 656 | 657 | const auto nodeIndex = static_cast(decodeBitsAndAdvance(data)); 658 | key.setNodeIndex(nodeIndex); 659 | 660 | const auto size1 = static_cast(decodeBitsAndAdvance(data)); 661 | key.setSize1(size1); 662 | 663 | // TODO: figure out 664 | const auto size2 = key.hasBits0123() ? static_cast(decodeBitsAndAdvance(data)) : size1; 665 | key.setSize2(size2); 666 | 667 | if (m_hasMultipleVolumes) { 668 | const auto volumeIndex = static_cast(decodeBitsAndAdvance(data)); 669 | key.setVolumeIndex(volumeIndex); 670 | } else { 671 | key.setVolumeIndex(0); 672 | } 673 | 674 | const auto sectorIndex = static_cast(decodeBitsAndAdvance(data)); 675 | key.setSectorIndex(sectorIndex); 676 | 677 | return data; 678 | } 679 | 680 | const uint8_t* advanceData(const uint8_t* data) const 681 | { 682 | return data; 683 | } 684 | 685 | int equalKeyCompareOp(const KeyType& key, const uint8_t* data) const 686 | { 687 | const auto flags = readNext(data); 688 | UNUSED(flags); 689 | 690 | const auto nodeIndex = static_cast(decodeBitsAndAdvance(data)); 691 | if (key.nodeIndex() < nodeIndex) 692 | return -1; 693 | else if (key.nodeIndex() > nodeIndex) 694 | return 1; 695 | 696 | return 0; 697 | } 698 | 699 | int lessThanKeyCompareOp(const KeyType& key, const uint8_t* data) const 700 | { 701 | const auto nodeIndex = static_cast(decodeBitsAndAdvance(data)); 702 | if (key.nodeIndex() < nodeIndex) 703 | return -1; 704 | else if (key.nodeIndex() > nodeIndex) 705 | return 1; 706 | else 707 | UNREACHABLE_CODE(0xDEAD); 708 | } 709 | 710 | CallbackResult traverseCallback(const uint8_t* data) const; 711 | 712 | bool m_hasMultipleVolumes; 713 | }; 714 | --------------------------------------------------------------------------------