From d43ca37875497d1e4ce8959d2e2c3228150814dc Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Fri, 23 May 2025 17:02:35 +0100 Subject: [PATCH] add file hashing to the file browser (crc32, md5, sha1, sha256) --- sphaira/CMakeLists.txt | 1 + sphaira/include/hasher.hpp | 30 ++++ sphaira/include/ui/menus/filebrowser.hpp | 3 + sphaira/source/hasher.cpp | 184 +++++++++++++++++++++++ sphaira/source/ui/menus/appstore.cpp | 50 +----- sphaira/source/ui/menus/filebrowser.cpp | 88 ++++++++--- 6 files changed, 287 insertions(+), 69 deletions(-) create mode 100644 sphaira/include/hasher.hpp create mode 100644 sphaira/source/hasher.cpp diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index 507760f..08e7c97 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -78,6 +78,7 @@ add_executable(sphaira source/nxlink.cpp source/owo.cpp source/swkbd.cpp + source/hasher.cpp source/i18n.cpp source/ftpsrv_helper.cpp source/threaded_file_transfer.cpp diff --git a/sphaira/include/hasher.hpp b/sphaira/include/hasher.hpp new file mode 100644 index 0000000..caec499 --- /dev/null +++ b/sphaira/include/hasher.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "fs.hpp" +#include "ui/progress_box.hpp" +#include +#include +#include + +namespace sphaira::hash { + +enum class Type { + Crc32, + Md5, + Sha1, + Sha256, +}; + +struct BaseSource { + virtual ~BaseSource() = default; + virtual Result Size(s64* out) = 0; + virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0; +}; + +auto GetTypeStr(Type type) -> const char*; + +// returns the hash string. +Result Hash(ui::ProgressBox* pbox, Type type, std::shared_ptr source, std::string& out); +Result Hash(ui::ProgressBox* pbox, Type type, fs::Fs* fs, const fs::FsPath& path, std::string& out); + +} // namespace sphaira::hash diff --git a/sphaira/include/ui/menus/filebrowser.hpp b/sphaira/include/ui/menus/filebrowser.hpp index 42797ec..e1f2070 100644 --- a/sphaira/include/ui/menus/filebrowser.hpp +++ b/sphaira/include/ui/menus/filebrowser.hpp @@ -5,6 +5,7 @@ #include "nro.hpp" #include "fs.hpp" #include "option.hpp" +#include "hasher.hpp" // #include #include @@ -272,6 +273,8 @@ private: return (fs::FsNative*)m_fs.get(); } + void DisplayHash(hash::Type type); + private: static constexpr inline const char* INI_SECTION = "filebrowser"; diff --git a/sphaira/source/hasher.cpp b/sphaira/source/hasher.cpp new file mode 100644 index 0000000..9ac1d66 --- /dev/null +++ b/sphaira/source/hasher.cpp @@ -0,0 +1,184 @@ +#include "hasher.hpp" +#include + +namespace sphaira::hash { +namespace { + +consteval auto CalculateHashStrLen(s64 buf_size) { + return buf_size * 2 + 1; +} + +struct FileSource final : BaseSource { + FileSource(fs::Fs* fs, const fs::FsPath& path) : m_fs{fs} { + m_open_result = m_fs->OpenFile(path, FsOpenMode_Read, std::addressof(m_file)); + } + + ~FileSource() { + m_fs->FileClose(std::addressof(m_file)); + } + + Result Size(s64* out) { + return m_fs->FileGetSize(std::addressof(m_file), out); + } + + Result Read(void* buf, s64 off, s64 size, u64* bytes_read) { + return m_fs->FileRead(std::addressof(m_file), off, buf, size, 0, bytes_read); + } + +private: + fs::Fs* m_fs{}; + fs::File m_file{}; + Result m_open_result{}; +}; + +struct HashSource { + virtual ~HashSource() = default; + virtual void Update(const void* buf, s64 size) = 0; + virtual void Get(std::string& out) = 0; +}; + +struct HashCrc32 final : HashSource { + void Update(const void* buf, s64 size) { + m_seed = crc32CalculateWithSeed(m_seed, buf, size); + } + + void Get(std::string& out) { + char str[CalculateHashStrLen(sizeof(m_seed))]; + std::snprintf(str, sizeof(str), "%08x", m_seed); + out = str; + } + +private: + u32 m_seed{}; +}; + +struct HashMd5 final : HashSource { + HashMd5() { + mbedtls_md5_init(&m_ctx); + mbedtls_md5_starts_ret(&m_ctx); + } + + ~HashMd5() { + mbedtls_md5_free(&m_ctx); + } + + void Update(const void* buf, s64 size) { + mbedtls_md5_update_ret(&m_ctx, (const u8*)buf, size); + } + + void Get(std::string& out) { + u8 hash[16]; + mbedtls_md5_finish_ret(&m_ctx, hash); + + char str[CalculateHashStrLen(sizeof(hash))]; + for (u32 i = 0; i < sizeof(hash); i++) { + std::sprintf(str + i * 2, "%02x", hash[i]); + } + + out = str; + } + +private: + mbedtls_md5_context m_ctx{}; +}; + +struct HashSha1 final : HashSource { + HashSha1() { + sha1ContextCreate(&m_ctx); + } + + void Update(const void* buf, s64 size) { + sha1ContextUpdate(&m_ctx, buf, size); + } + + void Get(std::string& out) { + u8 hash[SHA1_HASH_SIZE]; + sha1ContextGetHash(&m_ctx, hash); + + char str[CalculateHashStrLen(sizeof(hash))]; + for (u32 i = 0; i < sizeof(hash); i++) { + std::sprintf(str + i * 2, "%02x", hash[i]); + } + + out = str; + } + +private: + Sha1Context m_ctx{}; +}; + +struct HashSha256 final : HashSource { + HashSha256() { + sha256ContextCreate(&m_ctx); + } + + void Update(const void* buf, s64 size) { + sha256ContextUpdate(&m_ctx, buf, size); + } + + void Get(std::string& out) { + u8 hash[SHA256_HASH_SIZE]; + sha256ContextGetHash(&m_ctx, hash); + + char str[CalculateHashStrLen(sizeof(hash))]; + for (u32 i = 0; i < sizeof(hash); i++) { + std::sprintf(str + i * 2, "%02x", hash[i]); + } + + out = str; + } + +private: + Sha256Context m_ctx{}; +}; + +Result Hash(ui::ProgressBox* pbox, std::unique_ptr hash, std::shared_ptr source, std::string& out) { + s64 size; + R_TRY(source->Size(&size)); + + s64 offset{}; + std::vector chunk(1024 * 512); + while (offset < size) { + R_TRY(pbox->ShouldExitResult()); + const auto rsize = std::min(chunk.size(), size - offset); + + u64 bytes_read; + R_TRY(source->Read(chunk.data(), offset, rsize, &bytes_read)); + hash->Update(chunk.data(), bytes_read); + + offset += bytes_read; + pbox->UpdateTransfer(offset, size); + } + + hash->Get(out); + R_SUCCEED(); +} + +} // namespace + +auto GetTypeStr(Type type) -> const char* { + switch (type) { + case Type::Crc32: return "CRC32"; + case Type::Md5: return "MD5"; + case Type::Sha1: return "SHA1"; + case Type::Sha256: return "SHA256"; + } + return ""; +} + +Result Hash(ui::ProgressBox* pbox, Type type, std::shared_ptr source, std::string& out) { + switch (type) { + case Type::Crc32: return Hash(pbox, std::make_unique(), source, out); + case Type::Md5: return Hash(pbox, std::make_unique(), source, out); + case Type::Sha1: return Hash(pbox, std::make_unique(), source, out); + case Type::Sha256: return Hash(pbox, std::make_unique(), source, out); + } + R_THROW(0x1); +} + +Result Hash(ui::ProgressBox* pbox, Type type, fs::Fs* fs, const fs::FsPath& path, std::string& out) { + auto source = std::make_shared(fs, path); + return Hash(pbox, type, source, out); +} + +} // namespace sphaira::has diff --git a/sphaira/source/ui/menus/appstore.cpp b/sphaira/source/ui/menus/appstore.cpp index 0d0b01c..fea1e1a 100644 --- a/sphaira/source/ui/menus/appstore.cpp +++ b/sphaira/source/ui/menus/appstore.cpp @@ -13,6 +13,7 @@ #include "yyjson_helper.hpp" #include "swkbd.hpp" #include "i18n.hpp" +#include "hasher.hpp" #include "nro.hpp" #include @@ -21,7 +22,6 @@ #include #include #include -#include #include #include @@ -356,51 +356,11 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> Result { pbox->NewTransfer("Checking MD5"_i18n); log_write("starting md5 check\n"); - FsFile f; - R_TRY(fs.OpenFile(zip_out, FsOpenMode_Read, &f)); - ON_SCOPE_EXIT(fsFileClose(&f)); + std::string hash_out; + R_TRY(hash::Hash(pbox, hash::Type::Md5, &fs, zip_out, hash_out)); - s64 size; - R_TRY(fsFileGetSize(&f, &size)); - - mbedtls_md5_context ctx; - mbedtls_md5_init(&ctx); - ON_SCOPE_EXIT(mbedtls_md5_free(&ctx)); - - if (mbedtls_md5_starts_ret(&ctx)) { - log_write("failed to start ret\n"); - } - - std::vector chunk(chunk_size); - s64 offset{}; - while (offset < size) { - R_TRY(pbox->ShouldExitResult()); - - u64 bytes_read; - R_TRY(fsFileRead(&f, offset, chunk.data(), chunk.size(), 0, &bytes_read)); - - if (mbedtls_md5_update_ret(&ctx, chunk.data(), bytes_read)) { - log_write("failed to update ret\n"); - R_THROW(0x1); - } - - offset += bytes_read; - pbox->UpdateTransfer(offset, size); - } - - u8 md5_out[16]; - if (mbedtls_md5_finish_ret(&ctx, (u8*)md5_out)) { - R_THROW(0x1); - } - - // convert md5 to hex string - char md5_str[sizeof(md5_out) * 2 + 1]; - for (u32 i = 0; i < sizeof(md5_out); i++) { - std::sprintf(md5_str + i * 2, "%02x", md5_out[i]); - } - - if (strncasecmp(md5_str, entry.md5.data(), entry.md5.length())) { - log_write("bad md5: %.*s vs %.*s\n", 32, md5_str, 32, entry.md5.c_str()); + if (strncasecmp(hash_out.data(), entry.md5.data(), entry.md5.length())) { + log_write("bad md5: %.*s vs %.*s\n", 32, hash_out.data(), 32, entry.md5.c_str()); R_THROW(0x1); } } diff --git a/sphaira/source/ui/menus/filebrowser.cpp b/sphaira/source/ui/menus/filebrowser.cpp index dc408cf..38159f6 100644 --- a/sphaira/source/ui/menus/filebrowser.cpp +++ b/sphaira/source/ui/menus/filebrowser.cpp @@ -18,6 +18,7 @@ #include "owo.hpp" #include "swkbd.hpp" #include "i18n.hpp" +#include "hasher.hpp" #include "location.hpp" #include "threaded_file_transfer.hpp" @@ -43,7 +44,7 @@ namespace sphaira::ui::menu::filebrowser { namespace { constexpr FsEntry FS_ENTRY_DEFAULT{ - "Sd", "/", FsType::Sd, FsEntryFlag_Assoc, + "microSD card", "/", FsType::Sd, FsEntryFlag_Assoc, }; constexpr FsEntry FS_ENTRIES[]{ @@ -599,6 +600,29 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 auto options = std::make_shared("Advanced Options"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(options)); + SidebarEntryArray::Items mount_items; + std::vector fs_entries; + for (const auto& e: FS_ENTRIES) { + fs_entries.emplace_back(e); + mount_items.push_back(i18n::get(e.name)); + } + + const auto stdio_locations = location::GetStdio(false); + for (const auto& e: stdio_locations) { + u32 flags{}; + if (e.write_protect) { + flags |= FsEntryFlag_ReadOnly; + } + + fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags); + mount_items.push_back(e.name); + } + + options->Add(std::make_shared("Mount"_i18n, mount_items, [this, fs_entries](s64& index_out){ + App::PopToMenu(); + SetFs(fs_entries[index_out].root, fs_entries[index_out]); + }, m_fs_entry.name)); + options->Add(std::make_shared("Create File"_i18n, [this](){ std::string out; if (R_SUCCEEDED(swkbd::ShowText(out, "Set File Name"_i18n.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) { @@ -654,33 +678,28 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 })); } + options->Add(std::make_shared("Hash"_i18n, [this](){ + auto options = std::make_shared("Hash Options"_i18n, Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(options)); + + options->Add(std::make_shared("CRC32"_i18n, [this](){ + DisplayHash(hash::Type::Crc32); + })); + options->Add(std::make_shared("MD5"_i18n, [this](){ + DisplayHash(hash::Type::Md5); + })); + options->Add(std::make_shared("SHA1"_i18n, [this](){ + DisplayHash(hash::Type::Sha1); + })); + options->Add(std::make_shared("SHA256"_i18n, [this](){ + DisplayHash(hash::Type::Sha256); + })); + })); + options->Add(std::make_shared("Ignore read only"_i18n, m_ignore_read_only.Get(), [this](bool& v_out){ m_ignore_read_only.Set(v_out); m_fs->SetIgnoreReadOnly(v_out); })); - - SidebarEntryArray::Items mount_items; - std::vector fs_entries; - for (const auto& e: FS_ENTRIES) { - fs_entries.emplace_back(e); - mount_items.push_back(i18n::get(e.name)); - } - - const auto stdio_locations = location::GetStdio(false); - for (const auto& e: stdio_locations) { - u32 flags{}; - if (e.write_protect) { - flags |= FsEntryFlag_ReadOnly; - } - - fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags); - mount_items.push_back(e.name); - } - - options->Add(std::make_shared("Mount"_i18n, mount_items, [this, fs_entries](s64& index_out){ - App::PopToMenu(); - SetFs(fs_entries[index_out].root, fs_entries[index_out]); - }, m_fs_entry.name)); })); }}) ); @@ -1933,6 +1952,27 @@ void Menu::SetFs(const fs::FsPath& new_path, const FsEntry& new_entry) { } } +void Menu::DisplayHash(hash::Type type) { + // hack because we cannot share output between threaded calls... + static std::string hash_out; + hash_out.clear(); + + App::Push(std::make_shared(0, "Hashing"_i18n, GetEntry().name, [this, type](auto pbox) -> Result { + R_TRY(hash::Hash(pbox, type, m_fs.get(), GetNewPathCurrent(), hash_out)); + + R_SUCCEED(); + }, [this, type](Result rc){ + App::PushErrorBox(rc, "Failed to hash file..."_i18n); + + if (R_SUCCEEDED(rc)) { + char buf[0x100]; + // std::snprintf(buf, sizeof(buf), "%s\n%s\n%s", hash::GetTypeStr(type), hash_out.c_str(), GetEntry().GetName()); + std::snprintf(buf, sizeof(buf), "%s\n%s", hash::GetTypeStr(type), hash_out.c_str()); + App::Push(std::make_shared(buf, "OK"_i18n)); + } + })); +} + } // namespace sphaira::ui::menu::filebrowser // options