add file hashing to the file browser (crc32, md5, sha1, sha256)

This commit is contained in:
ITotalJustice
2025-05-23 17:02:35 +01:00
parent 1b5e7401f2
commit d43ca37875
6 changed files with 287 additions and 69 deletions

184
sphaira/source/hasher.cpp Normal file
View File

@@ -0,0 +1,184 @@
#include "hasher.hpp"
#include <mbedtls/md5.h>
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<HashSource> hash, std::shared_ptr<BaseSource> source, std::string& out) {
s64 size;
R_TRY(source->Size(&size));
s64 offset{};
std::vector<u8> chunk(1024 * 512);
while (offset < size) {
R_TRY(pbox->ShouldExitResult());
const auto rsize = std::min<s64>(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<BaseSource> source, std::string& out) {
switch (type) {
case Type::Crc32: return Hash(pbox, std::make_unique<HashCrc32>(), source, out);
case Type::Md5: return Hash(pbox, std::make_unique<HashMd5>(), source, out);
case Type::Sha1: return Hash(pbox, std::make_unique<HashSha1>(), source, out);
case Type::Sha256: return Hash(pbox, std::make_unique<HashSha256>(), 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<FileSource>(fs, path);
return Hash(pbox, type, source, out);
}
} // namespace sphaira::has

View File

@@ -13,6 +13,7 @@
#include "yyjson_helper.hpp"
#include "swkbd.hpp"
#include "i18n.hpp"
#include "hasher.hpp"
#include "nro.hpp"
#include <minIni.h>
@@ -21,7 +22,6 @@
#include <yyjson.h>
#include <stb_image.h>
#include <minizip/unzip.h>
#include <mbedtls/md5.h>
#include <ranges>
#include <utility>
@@ -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<u8> 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);
}
}

View File

@@ -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<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
SidebarEntryArray::Items mount_items;
std::vector<FsEntry> 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<SidebarEntryArray>("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<SidebarEntryCallback>("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<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
}));
}
options->Add(std::make_shared<SidebarEntryCallback>("Hash"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Hash Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryCallback>("CRC32"_i18n, [this](){
DisplayHash(hash::Type::Crc32);
}));
options->Add(std::make_shared<SidebarEntryCallback>("MD5"_i18n, [this](){
DisplayHash(hash::Type::Md5);
}));
options->Add(std::make_shared<SidebarEntryCallback>("SHA1"_i18n, [this](){
DisplayHash(hash::Type::Sha1);
}));
options->Add(std::make_shared<SidebarEntryCallback>("SHA256"_i18n, [this](){
DisplayHash(hash::Type::Sha256);
}));
}));
options->Add(std::make_shared<SidebarEntryBool>("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<FsEntry> 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<SidebarEntryArray>("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<ProgressBox>(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<OptionBox>(buf, "OK"_i18n));
}
}));
}
} // namespace sphaira::ui::menu::filebrowser
// options