Merge pull request #6836 from leoetlino/wii-save-cleanup
WiiSave: (not so) Minor cleanup
This commit is contained in:
commit
e828c243f3
|
@ -150,7 +150,7 @@ add_library(core
|
|||
HW/WiimoteEmu/Encryption.cpp
|
||||
HW/WiimoteEmu/Speaker.cpp
|
||||
HW/WiimoteReal/WiimoteReal.cpp
|
||||
HW/WiiSaveCrypted.cpp
|
||||
HW/WiiSave.cpp
|
||||
IOS/Device.cpp
|
||||
IOS/DeviceStub.cpp
|
||||
IOS/IOS.cpp
|
||||
|
|
|
@ -179,7 +179,7 @@
|
|||
<ClCompile Include="HW\WiimoteReal\IOWin.cpp" />
|
||||
<ClCompile Include="HW\WiimoteReal\WiimoteReal.cpp" />
|
||||
<ClCompile Include="HW\WII_IPC.cpp" />
|
||||
<ClCompile Include="HW\WiiSaveCrypted.cpp" />
|
||||
<ClCompile Include="HW\WiiSave.cpp" />
|
||||
<ClCompile Include="IOS\Device.cpp" />
|
||||
<ClCompile Include="IOS\DeviceStub.cpp" />
|
||||
<ClCompile Include="IOS\IOS.cpp" />
|
||||
|
@ -437,7 +437,7 @@
|
|||
<ClInclude Include="HW\WiimoteEmu\WiimoteHid.h" />
|
||||
<ClInclude Include="HW\WiimoteReal\WiimoteReal.h" />
|
||||
<ClInclude Include="HW\WiimoteReal\WiimoteRealBase.h" />
|
||||
<ClInclude Include="HW\WiiSaveCrypted.h" />
|
||||
<ClInclude Include="HW\WiiSave.h" />
|
||||
<ClInclude Include="HW\WII_IPC.h" />
|
||||
<ClInclude Include="IOS\Device.h" />
|
||||
<ClInclude Include="IOS\DeviceStub.h" />
|
||||
|
|
|
@ -561,7 +561,7 @@
|
|||
<ClCompile Include="HW\SystemTimers.cpp">
|
||||
<Filter>HW %28Flipper/Hollywood%29</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="HW\WiiSaveCrypted.cpp">
|
||||
<ClCompile Include="HW\WiiSave.cpp">
|
||||
<Filter>HW %28Flipper/Hollywood%29</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="DSP\DSPAssembler.cpp">
|
||||
|
@ -1242,7 +1242,7 @@
|
|||
<ClInclude Include="HW\SystemTimers.h">
|
||||
<Filter>HW %28Flipper/Hollywood%29</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="HW\WiiSaveCrypted.h">
|
||||
<ClInclude Include="HW\WiiSave.h">
|
||||
<Filter>HW %28Flipper/Hollywood%29</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="DSP\DSPAssembler.h">
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
// Licensed under the terms of the GNU GPL, version 2
|
||||
// http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
|
||||
|
||||
#include "Core/HW/WiiSaveCrypted.h"
|
||||
#include "Core/HW/WiiSave.h"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstddef>
|
||||
|
@ -29,35 +29,30 @@
|
|||
#include "Common/MsgHandler.h"
|
||||
#include "Common/NandPaths.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "Common/Swap.h"
|
||||
|
||||
const u8 CWiiSaveCrypted::s_sd_key[16] = {0xAB, 0x01, 0xB9, 0xD8, 0xE1, 0x62, 0x2B, 0x08,
|
||||
0xAF, 0xBA, 0xD8, 0x4D, 0xBF, 0xC2, 0xA5, 0x5D};
|
||||
const u8 CWiiSaveCrypted::s_md5_blanker[16] = {0x0E, 0x65, 0x37, 0x81, 0x99, 0xBE, 0x45, 0x17,
|
||||
0xAB, 0x06, 0xEC, 0x22, 0x45, 0x1A, 0x57, 0x93};
|
||||
const u32 CWiiSaveCrypted::s_ng_id = 0x0403AC68;
|
||||
using Md5 = std::array<u8, 0x10>;
|
||||
|
||||
bool CWiiSaveCrypted::ImportWiiSave(const std::string& filename)
|
||||
constexpr std::array<u8, 0x10> s_sd_initial_iv{{0x21, 0x67, 0x12, 0xE6, 0xAA, 0x1F, 0x68, 0x9F,
|
||||
0x95, 0xC5, 0xA2, 0x23, 0x24, 0xDC, 0x6A, 0x98}};
|
||||
constexpr std::array<u8, 0x10> s_sd_key{{0xAB, 0x01, 0xB9, 0xD8, 0xE1, 0x62, 0x2B, 0x08, 0xAF, 0xBA,
|
||||
0xD8, 0x4D, 0xBF, 0xC2, 0xA5, 0x5D}};
|
||||
constexpr Md5 s_md5_blanker{{0x0E, 0x65, 0x37, 0x81, 0x99, 0xBE, 0x45, 0x17, 0xAB, 0x06, 0xEC, 0x22,
|
||||
0x45, 0x1A, 0x57, 0x93}};
|
||||
constexpr u32 s_ng_id = 0x0403AC68;
|
||||
|
||||
bool WiiSave::Import(std::string filename)
|
||||
{
|
||||
CWiiSaveCrypted save_file(filename);
|
||||
return save_file.m_valid;
|
||||
WiiSave save_file{std::move(filename)};
|
||||
return save_file.Import();
|
||||
}
|
||||
|
||||
bool CWiiSaveCrypted::ExportWiiSave(u64 title_id)
|
||||
bool WiiSave::Export(u64 title_id, std::string export_path)
|
||||
{
|
||||
CWiiSaveCrypted export_save("", title_id);
|
||||
if (export_save.m_valid)
|
||||
{
|
||||
SuccessAlertT("Successfully exported file to %s", export_save.m_encrypted_save_path.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
PanicAlertT("Export failed");
|
||||
}
|
||||
return export_save.m_valid;
|
||||
WiiSave export_save{title_id, std::move(export_path)};
|
||||
return export_save.Export();
|
||||
}
|
||||
|
||||
void CWiiSaveCrypted::ExportAllSaves()
|
||||
size_t WiiSave::ExportAll(std::string export_path)
|
||||
{
|
||||
std::string title_folder = File::GetUserPath(D_WIIROOT_IDX) + "/title";
|
||||
std::vector<u64> titles;
|
||||
|
@ -85,56 +80,50 @@ void CWiiSaveCrypted::ExportAllSaves()
|
|||
}
|
||||
}
|
||||
}
|
||||
SuccessAlertT("Found %zu save file(s)", titles.size());
|
||||
u32 success = 0;
|
||||
size_t exported_save_count = 0;
|
||||
for (const u64& title : titles)
|
||||
{
|
||||
CWiiSaveCrypted export_save{"", title};
|
||||
if (export_save.m_valid)
|
||||
success++;
|
||||
WiiSave export_save{title, export_path};
|
||||
if (export_save.Export())
|
||||
++exported_save_count;
|
||||
}
|
||||
SuccessAlertT("Successfully exported %u save(s) to %s", success,
|
||||
(File::GetUserPath(D_USER_IDX) + "private/wii/title/").c_str());
|
||||
return exported_save_count;
|
||||
}
|
||||
|
||||
CWiiSaveCrypted::CWiiSaveCrypted(const std::string& filename, u64 title_id)
|
||||
: m_encrypted_save_path(filename), m_title_id(title_id)
|
||||
WiiSave::WiiSave(std::string filename)
|
||||
: m_sd_iv{s_sd_initial_iv}, m_encrypted_save_path(std::move(filename)), m_valid{true}
|
||||
{
|
||||
memcpy(m_sd_iv, "\x21\x67\x12\xE6\xAA\x1F\x68\x9F\x95\xC5\xA2\x23\x24\xDC\x6A\x98", 0x10);
|
||||
|
||||
if (!title_id) // Import
|
||||
{
|
||||
mbedtls_aes_setkey_dec(&m_aes_ctx, s_sd_key, 128);
|
||||
m_valid = true;
|
||||
ReadHDR();
|
||||
ReadBKHDR();
|
||||
ImportWiiSaveFiles();
|
||||
// TODO: check_sig()
|
||||
if (m_valid)
|
||||
{
|
||||
SuccessAlertT("Successfully imported save file(s)");
|
||||
}
|
||||
else
|
||||
{
|
||||
PanicAlertT("Import failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mbedtls_aes_setkey_enc(&m_aes_ctx, s_sd_key, 128);
|
||||
|
||||
if (getPaths(true))
|
||||
{
|
||||
m_valid = true;
|
||||
WriteHDR();
|
||||
WriteBKHDR();
|
||||
ExportWiiSaveFiles();
|
||||
do_sig();
|
||||
}
|
||||
}
|
||||
mbedtls_aes_setkey_dec(&m_aes_ctx, s_sd_key.data(), 128);
|
||||
}
|
||||
|
||||
void CWiiSaveCrypted::ReadHDR()
|
||||
bool WiiSave::Import()
|
||||
{
|
||||
ReadHDR();
|
||||
ReadBKHDR();
|
||||
ImportWiiSaveFiles();
|
||||
// TODO: check_sig()
|
||||
return m_valid;
|
||||
}
|
||||
|
||||
WiiSave::WiiSave(u64 title_id, std::string export_path)
|
||||
: m_sd_iv{s_sd_initial_iv}, m_encrypted_save_path(std::move(export_path)), m_title_id{title_id}
|
||||
{
|
||||
mbedtls_aes_setkey_enc(&m_aes_ctx, s_sd_key.data(), 128);
|
||||
|
||||
if (getPaths(true))
|
||||
m_valid = true;
|
||||
}
|
||||
|
||||
bool WiiSave::Export()
|
||||
{
|
||||
WriteHDR();
|
||||
WriteBKHDR();
|
||||
ExportWiiSaveFiles();
|
||||
do_sig();
|
||||
return m_valid;
|
||||
}
|
||||
|
||||
void WiiSave::ReadHDR()
|
||||
{
|
||||
File::IOFile data_file(m_encrypted_save_path, "rb");
|
||||
if (!data_file)
|
||||
|
@ -151,9 +140,9 @@ void CWiiSaveCrypted::ReadHDR()
|
|||
}
|
||||
data_file.Close();
|
||||
|
||||
mbedtls_aes_crypt_cbc(&m_aes_ctx, MBEDTLS_AES_DECRYPT, HEADER_SZ, m_sd_iv,
|
||||
mbedtls_aes_crypt_cbc(&m_aes_ctx, MBEDTLS_AES_DECRYPT, HEADER_SZ, m_sd_iv.data(),
|
||||
(const u8*)&m_encrypted_header, (u8*)&m_header);
|
||||
u32 banner_size = Common::swap32(m_header.hdr.BannerSize);
|
||||
u32 banner_size = m_header.hdr.banner_size;
|
||||
if ((banner_size < FULL_BNR_MIN) || (banner_size > FULL_BNR_MAX) ||
|
||||
(((banner_size - BNR_SZ) % ICON_SZ) != 0))
|
||||
{
|
||||
|
@ -161,18 +150,17 @@ void CWiiSaveCrypted::ReadHDR()
|
|||
m_valid = false;
|
||||
return;
|
||||
}
|
||||
m_title_id = Common::swap64(m_header.hdr.SaveGameTitle);
|
||||
m_title_id = m_header.hdr.save_game_title;
|
||||
|
||||
u8 md5_file[16];
|
||||
u8 md5_calc[16];
|
||||
memcpy(md5_file, m_header.hdr.Md5, 0x10);
|
||||
memcpy(m_header.hdr.Md5, s_md5_blanker, 0x10);
|
||||
mbedtls_md5((u8*)&m_header, HEADER_SZ, md5_calc);
|
||||
if (memcmp(md5_file, md5_calc, 0x10))
|
||||
Md5 md5_file = m_header.hdr.md5;
|
||||
m_header.hdr.md5 = s_md5_blanker;
|
||||
Md5 md5_calc;
|
||||
mbedtls_md5((u8*)&m_header, HEADER_SZ, md5_calc.data());
|
||||
if (md5_file == md5_calc)
|
||||
{
|
||||
ERROR_LOG(CONSOLE, "MD5 mismatch\n %016" PRIx64 "%016" PRIx64 " != %016" PRIx64 "%016" PRIx64,
|
||||
Common::swap64(md5_file), Common::swap64(md5_file + 8), Common::swap64(md5_calc),
|
||||
Common::swap64(md5_calc + 8));
|
||||
Common::swap64(md5_file.data()), Common::swap64(md5_file.data() + 8),
|
||||
Common::swap64(md5_calc.data()), Common::swap64(md5_calc.data() + 8));
|
||||
m_valid = false;
|
||||
}
|
||||
|
||||
|
@ -189,7 +177,7 @@ void CWiiSaveCrypted::ReadHDR()
|
|||
{
|
||||
INFO_LOG(CONSOLE, "Creating file %s", banner_file_path.c_str());
|
||||
File::IOFile banner_file(banner_file_path, "wb");
|
||||
banner_file.WriteBytes(m_header.BNR, banner_size);
|
||||
banner_file.WriteBytes(m_header.banner, banner_size);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -197,7 +185,7 @@ void CWiiSaveCrypted::ReadHDR()
|
|||
}
|
||||
}
|
||||
|
||||
void CWiiSaveCrypted::WriteHDR()
|
||||
void WiiSave::WriteHDR()
|
||||
{
|
||||
if (!m_valid)
|
||||
return;
|
||||
|
@ -205,28 +193,28 @@ void CWiiSaveCrypted::WriteHDR()
|
|||
|
||||
std::string banner_file_path = m_wii_title_path + "/banner.bin";
|
||||
u32 banner_size = static_cast<u32>(File::GetSize(banner_file_path));
|
||||
m_header.hdr.BannerSize = Common::swap32(banner_size);
|
||||
m_header.hdr.banner_size = banner_size;
|
||||
|
||||
m_header.hdr.SaveGameTitle = Common::swap64(m_title_id);
|
||||
memcpy(m_header.hdr.Md5, s_md5_blanker, 0x10);
|
||||
m_header.hdr.Permissions = 0x3C;
|
||||
m_header.hdr.save_game_title = m_title_id;
|
||||
m_header.hdr.md5 = s_md5_blanker;
|
||||
m_header.hdr.permissions = 0x3C;
|
||||
|
||||
File::IOFile banner_file(banner_file_path, "rb");
|
||||
if (!banner_file.ReadBytes(m_header.BNR, banner_size))
|
||||
if (!banner_file.ReadBytes(m_header.banner, banner_size))
|
||||
{
|
||||
ERROR_LOG(CONSOLE, "Failed to read banner.bin");
|
||||
m_valid = false;
|
||||
return;
|
||||
}
|
||||
// remove nocopy flag
|
||||
m_header.BNR[7] &= ~1;
|
||||
m_header.banner[7] &= ~1;
|
||||
|
||||
u8 md5_calc[16];
|
||||
mbedtls_md5((u8*)&m_header, HEADER_SZ, md5_calc);
|
||||
memcpy(m_header.hdr.Md5, md5_calc, 0x10);
|
||||
Md5 md5_calc;
|
||||
mbedtls_md5((u8*)&m_header, HEADER_SZ, md5_calc.data());
|
||||
m_header.hdr.md5 = std::move(md5_calc);
|
||||
|
||||
mbedtls_aes_crypt_cbc(&m_aes_ctx, MBEDTLS_AES_ENCRYPT, HEADER_SZ, m_sd_iv, (const u8*)&m_header,
|
||||
(u8*)&m_encrypted_header);
|
||||
mbedtls_aes_crypt_cbc(&m_aes_ctx, MBEDTLS_AES_ENCRYPT, HEADER_SZ, m_sd_iv.data(),
|
||||
(const u8*)&m_header, (u8*)&m_encrypted_header);
|
||||
|
||||
File::IOFile data_file(m_encrypted_save_path, "wb");
|
||||
if (!data_file.WriteBytes(&m_encrypted_header, HEADER_SZ))
|
||||
|
@ -236,7 +224,7 @@ void CWiiSaveCrypted::WriteHDR()
|
|||
}
|
||||
}
|
||||
|
||||
void CWiiSaveCrypted::ReadBKHDR()
|
||||
void WiiSave::ReadBKHDR()
|
||||
{
|
||||
if (!m_valid)
|
||||
return;
|
||||
|
@ -257,47 +245,41 @@ void CWiiSaveCrypted::ReadBKHDR()
|
|||
}
|
||||
fpData_bin.Close();
|
||||
|
||||
if (m_bk_hdr.size != Common::swap32(BK_LISTED_SZ) ||
|
||||
m_bk_hdr.magic != Common::swap32(BK_HDR_MAGIC))
|
||||
if (m_bk_hdr.size != BK_LISTED_SZ || m_bk_hdr.magic != BK_HDR_MAGIC)
|
||||
{
|
||||
ERROR_LOG(CONSOLE, "Invalid Size(%x) or Magic word (%x)", m_bk_hdr.size, m_bk_hdr.magic);
|
||||
ERROR_LOG(CONSOLE, "Invalid Size(%x) or Magic word (%x)", u32(m_bk_hdr.size),
|
||||
u32(m_bk_hdr.magic));
|
||||
m_valid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_files_list_size = Common::swap32(m_bk_hdr.numberOfFiles);
|
||||
m_size_of_files = Common::swap32(m_bk_hdr.sizeOfFiles);
|
||||
m_total_size = Common::swap32(m_bk_hdr.totalSize);
|
||||
|
||||
if (m_size_of_files + FULL_CERT_SZ != m_total_size)
|
||||
if (m_bk_hdr.size_of_files + FULL_CERT_SZ != m_bk_hdr.total_size)
|
||||
{
|
||||
WARN_LOG(CONSOLE, "Size(%x) + cert(%x) does not equal totalsize(%x)", m_size_of_files,
|
||||
FULL_CERT_SZ, m_total_size);
|
||||
WARN_LOG(CONSOLE, "Size(%x) + cert(%x) does not equal totalsize(%x)",
|
||||
u32(m_bk_hdr.size_of_files), FULL_CERT_SZ, u32(m_bk_hdr.total_size));
|
||||
}
|
||||
if (m_title_id != Common::swap64(m_bk_hdr.SaveGameTitle))
|
||||
if (m_title_id != m_bk_hdr.save_game_title)
|
||||
{
|
||||
WARN_LOG(CONSOLE,
|
||||
"Encrypted title (%" PRIx64 ") does not match unencrypted title (%" PRIx64 ")",
|
||||
m_title_id, Common::swap64(m_bk_hdr.SaveGameTitle));
|
||||
m_title_id, u64(m_bk_hdr.save_game_title));
|
||||
}
|
||||
}
|
||||
|
||||
void CWiiSaveCrypted::WriteBKHDR()
|
||||
void WiiSave::WriteBKHDR()
|
||||
{
|
||||
if (!m_valid)
|
||||
return;
|
||||
m_files_list_size = 0;
|
||||
m_size_of_files = 0;
|
||||
|
||||
ScanForFiles(m_wii_title_path, m_files_list, &m_files_list_size, &m_size_of_files);
|
||||
u32 number_of_files = 0, size_of_files = 0;
|
||||
ScanForFiles(m_wii_title_path, m_files_list, &number_of_files, &size_of_files);
|
||||
memset(&m_bk_hdr, 0, BK_SZ);
|
||||
m_bk_hdr.size = Common::swap32(BK_LISTED_SZ);
|
||||
m_bk_hdr.magic = Common::swap32(BK_HDR_MAGIC);
|
||||
m_bk_hdr.NGid = s_ng_id;
|
||||
m_bk_hdr.numberOfFiles = Common::swap32(m_files_list_size);
|
||||
m_bk_hdr.sizeOfFiles = Common::swap32(m_size_of_files);
|
||||
m_bk_hdr.totalSize = Common::swap32(m_size_of_files + FULL_CERT_SZ);
|
||||
m_bk_hdr.SaveGameTitle = Common::swap64(m_title_id);
|
||||
m_bk_hdr.size = BK_LISTED_SZ;
|
||||
m_bk_hdr.magic = BK_HDR_MAGIC;
|
||||
m_bk_hdr.ngid = s_ng_id;
|
||||
m_bk_hdr.number_of_files = number_of_files;
|
||||
m_bk_hdr.size_of_files = size_of_files;
|
||||
m_bk_hdr.total_size = size_of_files + FULL_CERT_SZ;
|
||||
m_bk_hdr.save_game_title = m_title_id;
|
||||
|
||||
File::IOFile data_file(m_encrypted_save_path, "ab");
|
||||
if (!data_file.WriteBytes(&m_bk_hdr, BK_SZ))
|
||||
|
@ -307,7 +289,7 @@ void CWiiSaveCrypted::WriteBKHDR()
|
|||
}
|
||||
}
|
||||
|
||||
void CWiiSaveCrypted::ImportWiiSaveFiles()
|
||||
void WiiSave::ImportWiiSaveFiles()
|
||||
{
|
||||
if (!m_valid)
|
||||
return;
|
||||
|
@ -324,11 +306,10 @@ void CWiiSaveCrypted::ImportWiiSaveFiles()
|
|||
|
||||
FileHDR file_hdr_tmp;
|
||||
|
||||
for (u32 i = 0; i < m_files_list_size; ++i)
|
||||
for (u32 i = 0; i < m_bk_hdr.number_of_files; ++i)
|
||||
{
|
||||
memset(&file_hdr_tmp, 0, FILE_HDR_SZ);
|
||||
memset(m_iv, 0, 0x10);
|
||||
u32 file_size = 0;
|
||||
m_iv.fill(0);
|
||||
|
||||
if (!data_file.ReadBytes(&file_hdr_tmp, FILE_HDR_SZ))
|
||||
{
|
||||
|
@ -336,7 +317,7 @@ void CWiiSaveCrypted::ImportWiiSaveFiles()
|
|||
m_valid = false;
|
||||
}
|
||||
|
||||
if (Common::swap32(file_hdr_tmp.magic) != FILE_HDR_MAGIC)
|
||||
if (file_hdr_tmp.magic != FILE_HDR_MAGIC)
|
||||
{
|
||||
ERROR_LOG(CONSOLE, "Bad File Header");
|
||||
break;
|
||||
|
@ -345,15 +326,14 @@ void CWiiSaveCrypted::ImportWiiSaveFiles()
|
|||
{
|
||||
// Allows files in subfolders to be escaped properly (ex: "nocopy/data00")
|
||||
// Special characters in path components will be escaped such as /../
|
||||
std::string file_path = Common::EscapePath(reinterpret_cast<const char*>(file_hdr_tmp.name));
|
||||
std::string file_path = Common::EscapePath(file_hdr_tmp.name.data());
|
||||
|
||||
std::string file_path_full = m_wii_title_path + '/' + file_path;
|
||||
File::CreateFullPath(file_path_full);
|
||||
const File::FileInfo file_info(file_path_full);
|
||||
if (file_hdr_tmp.type == 1)
|
||||
{
|
||||
file_size = Common::swap32(file_hdr_tmp.size);
|
||||
u32 file_size_rounded = Common::AlignUp(file_size, BLOCK_SZ);
|
||||
u32 file_size_rounded = Common::AlignUp<u32>(file_hdr_tmp.size, BLOCK_SZ);
|
||||
std::vector<u8> file_data(file_size_rounded);
|
||||
std::vector<u8> file_data_enc(file_size_rounded);
|
||||
|
||||
|
@ -364,14 +344,14 @@ void CWiiSaveCrypted::ImportWiiSaveFiles()
|
|||
break;
|
||||
}
|
||||
|
||||
memcpy(m_iv, file_hdr_tmp.IV, 0x10);
|
||||
mbedtls_aes_crypt_cbc(&m_aes_ctx, MBEDTLS_AES_DECRYPT, file_size_rounded, m_iv,
|
||||
m_iv = file_hdr_tmp.iv;
|
||||
mbedtls_aes_crypt_cbc(&m_aes_ctx, MBEDTLS_AES_DECRYPT, file_size_rounded, m_iv.data(),
|
||||
static_cast<const u8*>(file_data_enc.data()), file_data.data());
|
||||
|
||||
INFO_LOG(CONSOLE, "Creating file %s", file_path_full.c_str());
|
||||
|
||||
File::IOFile raw_save_file(file_path_full, "wb");
|
||||
raw_save_file.WriteBytes(file_data.data(), file_size);
|
||||
raw_save_file.WriteBytes(file_data.data(), file_hdr_tmp.size);
|
||||
}
|
||||
else if (file_hdr_tmp.type == 2)
|
||||
{
|
||||
|
@ -391,12 +371,12 @@ void CWiiSaveCrypted::ImportWiiSaveFiles()
|
|||
}
|
||||
}
|
||||
|
||||
void CWiiSaveCrypted::ExportWiiSaveFiles()
|
||||
void WiiSave::ExportWiiSaveFiles()
|
||||
{
|
||||
if (!m_valid)
|
||||
return;
|
||||
|
||||
for (u32 i = 0; i < m_files_list_size; i++)
|
||||
for (u32 i = 0; i < m_bk_hdr.number_of_files; i++)
|
||||
{
|
||||
FileHDR file_hdr_tmp;
|
||||
memset(&file_hdr_tmp, 0, FILE_HDR_SZ);
|
||||
|
@ -414,9 +394,9 @@ void CWiiSaveCrypted::ExportWiiSaveFiles()
|
|||
}
|
||||
|
||||
u32 file_size_rounded = Common::AlignUp(file_size, BLOCK_SZ);
|
||||
file_hdr_tmp.magic = Common::swap32(FILE_HDR_MAGIC);
|
||||
file_hdr_tmp.size = Common::swap32(file_size);
|
||||
file_hdr_tmp.Permissions = 0x3c;
|
||||
file_hdr_tmp.magic = FILE_HDR_MAGIC;
|
||||
file_hdr_tmp.size = file_size;
|
||||
file_hdr_tmp.permissions = 0x3c;
|
||||
|
||||
std::string name =
|
||||
Common::UnescapeFileName(m_files_list[i].substr(m_wii_title_path.length() + 1));
|
||||
|
@ -428,7 +408,7 @@ void CWiiSaveCrypted::ExportWiiSaveFiles()
|
|||
m_valid = false;
|
||||
return;
|
||||
}
|
||||
strncpy((char*)file_hdr_tmp.name, name.c_str(), sizeof(file_hdr_tmp.name));
|
||||
std::strncpy(file_hdr_tmp.name.data(), name.c_str(), file_hdr_tmp.name.size());
|
||||
|
||||
{
|
||||
File::IOFile fpData_bin(m_encrypted_save_path, "ab");
|
||||
|
@ -459,8 +439,9 @@ void CWiiSaveCrypted::ExportWiiSaveFiles()
|
|||
m_valid = false;
|
||||
}
|
||||
|
||||
mbedtls_aes_crypt_cbc(&m_aes_ctx, MBEDTLS_AES_ENCRYPT, file_size_rounded, file_hdr_tmp.IV,
|
||||
static_cast<const u8*>(file_data.data()), file_data_enc.data());
|
||||
mbedtls_aes_crypt_cbc(&m_aes_ctx, MBEDTLS_AES_ENCRYPT, file_size_rounded,
|
||||
file_hdr_tmp.iv.data(), static_cast<const u8*>(file_data.data()),
|
||||
file_data_enc.data());
|
||||
|
||||
File::IOFile fpData_bin(m_encrypted_save_path, "ab");
|
||||
if (!fpData_bin.WriteBytes(file_data_enc.data(), file_size_rounded))
|
||||
|
@ -471,7 +452,7 @@ void CWiiSaveCrypted::ExportWiiSaveFiles()
|
|||
}
|
||||
}
|
||||
|
||||
void CWiiSaveCrypted::do_sig()
|
||||
void WiiSave::do_sig()
|
||||
{
|
||||
if (!m_valid)
|
||||
return;
|
||||
|
@ -514,7 +495,7 @@ void CWiiSaveCrypted::do_sig()
|
|||
generate_ecdsa(ap_sig, ap_sig + 30, ng_priv, hash);
|
||||
make_ec_cert(ap_cert, ap_sig, signer, name, ap_priv, 0);
|
||||
|
||||
data_size = Common::swap32(m_bk_hdr.sizeOfFiles) + 0x80;
|
||||
data_size = m_bk_hdr.size_of_files + 0x80;
|
||||
|
||||
File::IOFile data_file(m_encrypted_save_path, "rb");
|
||||
if (!data_file)
|
||||
|
@ -550,8 +531,8 @@ void CWiiSaveCrypted::do_sig()
|
|||
m_valid = data_file.IsGood();
|
||||
}
|
||||
|
||||
void CWiiSaveCrypted::make_ec_cert(u8* cert, const u8* sig, const char* signer, const char* name,
|
||||
const u8* priv, const u32 key_id)
|
||||
void WiiSave::make_ec_cert(u8* cert, const u8* sig, const char* signer, const char* name,
|
||||
const u8* priv, const u32 key_id)
|
||||
{
|
||||
memset(cert, 0, 0x180);
|
||||
*(u32*)cert = Common::swap32(0x10002);
|
||||
|
@ -564,7 +545,7 @@ void CWiiSaveCrypted::make_ec_cert(u8* cert, const u8* sig, const char* signer,
|
|||
ec_priv_to_pub(priv, cert + 0x108);
|
||||
}
|
||||
|
||||
bool CWiiSaveCrypted::getPaths(bool for_export)
|
||||
bool WiiSave::getPaths(bool for_export)
|
||||
{
|
||||
if (m_title_id)
|
||||
{
|
||||
|
@ -591,12 +572,7 @@ bool CWiiSaveCrypted::getPaths(bool for_export)
|
|||
ERROR_LOG(CONSOLE, "No banner file found for title %s", game_id);
|
||||
return false;
|
||||
}
|
||||
if (m_encrypted_save_path.length() == 0)
|
||||
{
|
||||
// If no path was passed, use User folder
|
||||
m_encrypted_save_path = File::GetUserPath(D_USER_IDX);
|
||||
}
|
||||
m_encrypted_save_path += StringFromFormat("private/wii/title/%s/data.bin", game_id);
|
||||
m_encrypted_save_path += StringFromFormat("/private/wii/title/%s/data.bin", game_id);
|
||||
File::CreateFullPath(m_encrypted_save_path);
|
||||
}
|
||||
else
|
||||
|
@ -606,9 +582,8 @@ bool CWiiSaveCrypted::getPaths(bool for_export)
|
|||
return true;
|
||||
}
|
||||
|
||||
void CWiiSaveCrypted::ScanForFiles(const std::string& save_directory,
|
||||
std::vector<std::string>& file_list, u32* num_files,
|
||||
u32* size_files)
|
||||
void WiiSave::ScanForFiles(const std::string& save_directory, std::vector<std::string>& file_list,
|
||||
u32* num_files, u32* size_files)
|
||||
{
|
||||
std::vector<std::string> directories;
|
||||
directories.push_back(save_directory);
|
||||
|
@ -653,6 +628,6 @@ void CWiiSaveCrypted::ScanForFiles(const std::string& save_directory,
|
|||
*size_files = size;
|
||||
}
|
||||
|
||||
CWiiSaveCrypted::~CWiiSaveCrypted()
|
||||
WiiSave::~WiiSave()
|
||||
{
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
// Copyright 2010 Dolphin Emulator Project
|
||||
// Licensed under GPLv2+
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <mbedtls/aes.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/Swap.h"
|
||||
|
||||
class WiiSave
|
||||
{
|
||||
public:
|
||||
/// Import a save into the NAND from a .bin file.
|
||||
static bool Import(std::string filename);
|
||||
/// Export a save to a .bin file.
|
||||
static bool Export(u64 title_id, std::string export_path);
|
||||
/// Export all saves that are in the NAND. Returns the number of exported saves.
|
||||
static size_t ExportAll(std::string export_path);
|
||||
|
||||
private:
|
||||
explicit WiiSave(std::string filename);
|
||||
explicit WiiSave(u64 title_id, std::string export_path);
|
||||
~WiiSave();
|
||||
|
||||
bool Import();
|
||||
bool Export();
|
||||
|
||||
void ReadHDR();
|
||||
void ReadBKHDR();
|
||||
void WriteHDR();
|
||||
void WriteBKHDR();
|
||||
void ImportWiiSaveFiles();
|
||||
void ExportWiiSaveFiles();
|
||||
void do_sig();
|
||||
void make_ec_cert(u8* cert, const u8* sig, const char* signer, const char* name, const u8* priv,
|
||||
const u32 key_id);
|
||||
bool getPaths(bool for_export = false);
|
||||
void ScanForFiles(const std::string& save_directory, std::vector<std::string>& file_list,
|
||||
u32* num_files, u32* size_files);
|
||||
|
||||
mbedtls_aes_context m_aes_ctx;
|
||||
std::array<u8, 0x10> m_sd_iv;
|
||||
std::vector<std::string> m_files_list;
|
||||
|
||||
std::string m_encrypted_save_path;
|
||||
|
||||
std::string m_wii_title_path;
|
||||
|
||||
std::array<u8, 0x10> m_iv;
|
||||
|
||||
u64 m_title_id;
|
||||
|
||||
bool m_valid;
|
||||
|
||||
enum
|
||||
{
|
||||
BLOCK_SZ = 0x40,
|
||||
HDR_SZ = 0x20,
|
||||
ICON_SZ = 0x1200,
|
||||
BNR_SZ = 0x60a0,
|
||||
FULL_BNR_MIN = 0x72a0, // BNR_SZ + 1*ICON_SZ
|
||||
FULL_BNR_MAX = 0xF0A0, // BNR_SZ + 8*ICON_SZ
|
||||
HEADER_SZ = 0xF0C0, // HDR_SZ + FULL_BNR_MAX
|
||||
BK_LISTED_SZ = 0x70, // Size before rounding to nearest block
|
||||
BK_SZ = 0x80,
|
||||
FILE_HDR_SZ = 0x80,
|
||||
|
||||
SIG_SZ = 0x40,
|
||||
NG_CERT_SZ = 0x180,
|
||||
AP_CERT_SZ = 0x180,
|
||||
FULL_CERT_SZ = 0x3C0, // SIG_SZ + NG_CERT_SZ + AP_CERT_SZ + 0x80?
|
||||
|
||||
BK_HDR_MAGIC = 0x426B0001,
|
||||
FILE_HDR_MAGIC = 0x03adf17e
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
struct DataBinHeader // encrypted
|
||||
{
|
||||
Common::BigEndianValue<u64> save_game_title;
|
||||
Common::BigEndianValue<u32> banner_size; // (0x72A0 or 0xF0A0, also seen 0xBAA0)
|
||||
u8 permissions;
|
||||
u8 unk1; // maybe permissions is a be16
|
||||
std::array<u8, 0x10> md5; // md5 of plaintext header with md5 blanker applied
|
||||
Common::BigEndianValue<u16> unk2;
|
||||
};
|
||||
|
||||
struct Header
|
||||
{
|
||||
DataBinHeader hdr;
|
||||
u8 banner[FULL_BNR_MAX];
|
||||
};
|
||||
|
||||
struct BkHeader // Not encrypted
|
||||
{
|
||||
Common::BigEndianValue<u32> size; // 0x00000070
|
||||
// u16 magic; // 'Bk'
|
||||
// u16 magic2; // or version (0x0001)
|
||||
Common::BigEndianValue<u32> magic; // 0x426B0001
|
||||
Common::BigEndianValue<u32> ngid;
|
||||
Common::BigEndianValue<u32> number_of_files;
|
||||
Common::BigEndianValue<u32> size_of_files;
|
||||
Common::BigEndianValue<u32> unk1;
|
||||
Common::BigEndianValue<u32> unk2;
|
||||
Common::BigEndianValue<u32> total_size;
|
||||
std::array<u8, 64> unk3;
|
||||
Common::BigEndianValue<u64> save_game_title;
|
||||
std::array<u8, 6> mac_address;
|
||||
std::array<u8, 0x12> padding;
|
||||
};
|
||||
|
||||
struct FileHDR // encrypted
|
||||
{
|
||||
Common::BigEndianValue<u32> magic; // 0x03adf17e
|
||||
Common::BigEndianValue<u32> size;
|
||||
u8 permissions;
|
||||
u8 attrib;
|
||||
u8 type; // (1=file, 2=directory)
|
||||
std::array<char, 0x45> name;
|
||||
std::array<u8, 0x10> iv;
|
||||
std::array<u8, 0x20> unk;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
Header m_header;
|
||||
Header m_encrypted_header;
|
||||
BkHeader m_bk_hdr;
|
||||
};
|
|
@ -1,133 +0,0 @@
|
|||
// Copyright 2010 Dolphin Emulator Project
|
||||
// Licensed under GPLv2+
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mbedtls/aes.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
class CWiiSaveCrypted
|
||||
{
|
||||
public:
|
||||
bool static ImportWiiSave(const std::string& filename);
|
||||
bool static ExportWiiSave(u64 title_id);
|
||||
void static ExportAllSaves();
|
||||
|
||||
private:
|
||||
CWiiSaveCrypted(const std::string& filename, u64 title_id = 0);
|
||||
~CWiiSaveCrypted();
|
||||
void ReadHDR();
|
||||
void ReadBKHDR();
|
||||
void WriteHDR();
|
||||
void WriteBKHDR();
|
||||
void Extract() {}
|
||||
void ImportWiiSaveFiles();
|
||||
void ExportWiiSaveFiles();
|
||||
void do_sig();
|
||||
void make_ec_cert(u8* cert, const u8* sig, const char* signer, const char* name, const u8* priv,
|
||||
const u32 key_id);
|
||||
bool getPaths(bool for_export = false);
|
||||
void ScanForFiles(const std::string& save_directory, std::vector<std::string>& file_list,
|
||||
u32* num_files, u32* size_files);
|
||||
|
||||
static const u8 s_sd_key[16];
|
||||
static const u8 s_md5_blanker[16];
|
||||
static const u32 s_ng_id;
|
||||
|
||||
mbedtls_aes_context m_aes_ctx;
|
||||
u8 m_sd_iv[0x10];
|
||||
std::vector<std::string> m_files_list;
|
||||
|
||||
std::string m_encrypted_save_path;
|
||||
|
||||
std::string m_wii_title_path;
|
||||
|
||||
u8 m_iv[0x10];
|
||||
|
||||
u32 m_files_list_size;
|
||||
u32 m_size_of_files;
|
||||
u32 m_total_size;
|
||||
|
||||
u64 m_title_id;
|
||||
|
||||
bool m_valid;
|
||||
|
||||
enum
|
||||
{
|
||||
BLOCK_SZ = 0x40,
|
||||
HDR_SZ = 0x20,
|
||||
ICON_SZ = 0x1200,
|
||||
BNR_SZ = 0x60a0,
|
||||
FULL_BNR_MIN = 0x72a0, // BNR_SZ + 1*ICON_SZ
|
||||
FULL_BNR_MAX = 0xF0A0, // BNR_SZ + 8*ICON_SZ
|
||||
HEADER_SZ = 0xF0C0, // HDR_SZ + FULL_BNR_MAX
|
||||
BK_LISTED_SZ = 0x70, // Size before rounding to nearest block
|
||||
BK_SZ = 0x80,
|
||||
FILE_HDR_SZ = 0x80,
|
||||
|
||||
SIG_SZ = 0x40,
|
||||
NG_CERT_SZ = 0x180,
|
||||
AP_CERT_SZ = 0x180,
|
||||
FULL_CERT_SZ = 0x3C0, // SIG_SZ + NG_CERT_SZ + AP_CERT_SZ + 0x80?
|
||||
|
||||
BK_HDR_MAGIC = 0x426B0001,
|
||||
FILE_HDR_MAGIC = 0x03adf17e
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
struct Data_Bin_HDR // encrypted
|
||||
{
|
||||
u64 SaveGameTitle;
|
||||
u32 BannerSize; // (0x72A0 or 0xF0A0, also seen 0xBAA0)
|
||||
u8 Permissions;
|
||||
u8 unk1; // maybe permissions is a be16
|
||||
u8 Md5[0x10]; // md5 of plaintext header with md5 blanker applied
|
||||
u16 unk2;
|
||||
};
|
||||
|
||||
struct HEADER
|
||||
{
|
||||
Data_Bin_HDR hdr;
|
||||
u8 BNR[FULL_BNR_MAX];
|
||||
};
|
||||
|
||||
struct BK_Header // Not encrypted
|
||||
{
|
||||
u32 size; // 0x00000070
|
||||
// u16 magic; // 'Bk'
|
||||
// u16 magic2; // or version (0x0001)
|
||||
u32 magic; // 0x426B0001
|
||||
u32 NGid;
|
||||
u32 numberOfFiles;
|
||||
u32 sizeOfFiles;
|
||||
u32 unk1;
|
||||
u32 unk2;
|
||||
u32 totalSize;
|
||||
u8 unk3[64];
|
||||
u64 SaveGameTitle;
|
||||
u8 MACaddress[6];
|
||||
u8 padding[0x12];
|
||||
};
|
||||
|
||||
struct FileHDR // encrypted
|
||||
{
|
||||
u32 magic; // 0x03adf17e
|
||||
u32 size;
|
||||
u8 Permissions;
|
||||
u8 attrib;
|
||||
u8 type; // (1=file, 2=directory)
|
||||
u8 name[0x45];
|
||||
u8 IV[0x10];
|
||||
u8 unk[0x20];
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
HEADER m_header;
|
||||
HEADER m_encrypted_header;
|
||||
BK_Header m_bk_hdr;
|
||||
};
|
|
@ -20,7 +20,7 @@
|
|||
#include "Core/ConfigManager.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/DVD/DVDInterface.h"
|
||||
#include "Core/HW/WiiSaveCrypted.h"
|
||||
#include "Core/HW/WiiSave.h"
|
||||
#include "Core/WiiUtils.h"
|
||||
#include "DiscIO/Blob.h"
|
||||
#include "DiscIO/Enums.h"
|
||||
|
@ -258,14 +258,16 @@ void GameList::OpenProperties()
|
|||
|
||||
void GameList::ExportWiiSave()
|
||||
{
|
||||
QMessageBox result_dialog(this);
|
||||
const QString export_dir = QFileDialog::getExistingDirectory(
|
||||
this, tr("Select Export Directory"), QString::fromStdString(File::GetUserPath(D_USER_IDX)),
|
||||
QFileDialog::ShowDirsOnly);
|
||||
if (export_dir.isEmpty())
|
||||
return;
|
||||
|
||||
const bool success = CWiiSaveCrypted::ExportWiiSave(GetSelectedGame()->GetTitleID());
|
||||
|
||||
result_dialog.setIcon(success ? QMessageBox::Information : QMessageBox::Critical);
|
||||
result_dialog.setText(success ? tr("Successfully exported save files") :
|
||||
tr("Failed to export save files!"));
|
||||
result_dialog.exec();
|
||||
if (WiiSave::Export(GetSelectedGame()->GetTitleID(), export_dir.toStdString()))
|
||||
QMessageBox::information(this, tr("Save Export"), tr("Successfully exported save files"));
|
||||
else
|
||||
QMessageBox::critical(this, tr("Save Export"), tr("Failed to export save files."));
|
||||
}
|
||||
|
||||
void GameList::OpenWiki()
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
#include "Core/Core.h"
|
||||
#include "Core/Debugger/RSO.h"
|
||||
#include "Core/HLE/HLE.h"
|
||||
#include "Core/HW/WiiSaveCrypted.h"
|
||||
#include "Core/HW/WiiSave.h"
|
||||
#include "Core/HW/Wiimote.h"
|
||||
#include "Core/Host.h"
|
||||
#include "Core/IOS/ES/ES.h"
|
||||
|
@ -896,13 +896,26 @@ void MenuBar::ImportWiiSave()
|
|||
tr("Wii save files (*.bin);;"
|
||||
"All Files (*)"));
|
||||
|
||||
if (!file.isEmpty())
|
||||
CWiiSaveCrypted::ImportWiiSave(file.toStdString());
|
||||
if (file.isEmpty())
|
||||
return;
|
||||
|
||||
if (WiiSave::Import(file.toStdString()))
|
||||
QMessageBox::information(this, tr("Save Import"), tr("Successfully imported save files."));
|
||||
else
|
||||
QMessageBox::critical(this, tr("Save Import"), tr("Failed to import save files."));
|
||||
}
|
||||
|
||||
void MenuBar::ExportWiiSaves()
|
||||
{
|
||||
CWiiSaveCrypted::ExportAllSaves();
|
||||
const QString export_dir = QFileDialog::getExistingDirectory(
|
||||
this, tr("Select Export Directory"), QString::fromStdString(File::GetUserPath(D_USER_IDX)),
|
||||
QFileDialog::ShowDirsOnly);
|
||||
if (export_dir.isEmpty())
|
||||
return;
|
||||
|
||||
const size_t count = WiiSave::ExportAll(export_dir.toStdString());
|
||||
QMessageBox::information(this, tr("Save Export"),
|
||||
tr("Exported %n save(s)", "", static_cast<int>(count)));
|
||||
}
|
||||
|
||||
void MenuBar::CheckNAND()
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
#include "Core/HW/GCPad.h"
|
||||
#include "Core/HW/ProcessorInterface.h"
|
||||
#include "Core/HW/SI/SI_Device.h"
|
||||
#include "Core/HW/WiiSaveCrypted.h"
|
||||
#include "Core/HW/WiiSave.h"
|
||||
#include "Core/HW/Wiimote.h"
|
||||
#include "Core/Host.h"
|
||||
#include "Core/HotkeyManager.h"
|
||||
|
@ -1204,7 +1204,7 @@ void CFrame::OnLoadGameCubeIPLEUR(wxCommandEvent&)
|
|||
|
||||
void CFrame::OnExportAllSaves(wxCommandEvent& WXUNUSED(event))
|
||||
{
|
||||
CWiiSaveCrypted::ExportAllSaves();
|
||||
WiiSave::ExportAll(File::GetUserPath(D_USER_IDX));
|
||||
}
|
||||
|
||||
void CFrame::OnImportSave(wxCommandEvent& WXUNUSED(event))
|
||||
|
@ -1215,7 +1215,7 @@ void CFrame::OnImportSave(wxCommandEvent& WXUNUSED(event))
|
|||
wxFD_OPEN | wxFD_PREVIEW | wxFD_FILE_MUST_EXIST, this);
|
||||
|
||||
if (!path.IsEmpty())
|
||||
CWiiSaveCrypted::ImportWiiSave(WxStrToStr(path));
|
||||
WiiSave::Import(WxStrToStr(path));
|
||||
}
|
||||
|
||||
void CFrame::OnShowCheatsWindow(wxCommandEvent& WXUNUSED(event))
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
#include "Core/ConfigManager.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/DVD/DVDInterface.h"
|
||||
#include "Core/HW/WiiSaveCrypted.h"
|
||||
#include "Core/HW/WiiSave.h"
|
||||
#include "Core/Movie.h"
|
||||
#include "Core/SysConf.h"
|
||||
#include "Core/TitleDatabase.h"
|
||||
|
@ -988,7 +988,7 @@ void GameListCtrl::OnExportSave(wxCommandEvent& WXUNUSED(event))
|
|||
{
|
||||
const UICommon::GameFile* iso = GetSelectedISO();
|
||||
if (iso)
|
||||
CWiiSaveCrypted::ExportWiiSave(iso->GetTitleID());
|
||||
WiiSave::Export(iso->GetTitleID(), File::GetUserPath(D_USER_IDX));
|
||||
}
|
||||
|
||||
// Save this file as the default file
|
||||
|
|
Loading…
Reference in New Issue