From 72d9dc9a3f63b314cea7b166b6f81000e20a14c3 Mon Sep 17 00:00:00 2001
From: Charles Lombardo <clombardo169@gmail.com>
Date: Fri, 9 Jun 2023 16:11:30 -0400
Subject: [PATCH] android: Add proper homebrew check

---
 .../java/org/yuzu/yuzu_emu/NativeLibrary.kt   |  2 ++
 .../yuzu/yuzu_emu/fragments/SearchFragment.kt |  8 +-------
 .../main/java/org/yuzu/yuzu_emu/model/Game.kt |  4 +++-
 .../org/yuzu/yuzu_emu/model/GamesViewModel.kt | 11 +++++++++-
 .../org/yuzu/yuzu_emu/utils/GameHelper.kt     |  4 ++--
 src/android/app/src/main/jni/native.cpp       | 20 ++++++++++++++++++-
 src/core/loader/nro.cpp                       | 13 +++++++++++-
 src/core/loader/nro.h                         |  2 ++
 8 files changed, 51 insertions(+), 13 deletions(-)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index c11b6bc169..22af9e435a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -223,6 +223,8 @@ object NativeLibrary {
 
     external fun getCompany(filename: String): String
 
+    external fun isHomebrew(filename: String): Boolean
+
     external fun setAppDirectory(directory: String)
 
     external fun initializeGpuDriver(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
index ebc0f164a2..adbe3696b4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
@@ -127,13 +127,7 @@ class SearchFragment : Fragment() {
                 }
             }
 
-            R.id.chip_homebrew -> {
-                baseList.filter {
-                    Log.error("Guh - ${it.path}")
-                    FileUtil.hasExtension(it.path, "nro")
-                            || FileUtil.hasExtension(it.path, "nso")
-                }
-            }
+            R.id.chip_homebrew -> baseList.filter { it.isHomebrew }
 
             R.id.chip_retail -> baseList.filter {
                 FileUtil.hasExtension(it.path, "xci")
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
index 2a17653b2f..3d6782c499 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
@@ -16,7 +16,8 @@ class Game(
     val regions: String,
     val path: String,
     val gameId: String,
-    val company: String
+    val company: String,
+    val isHomebrew: Boolean
 ) : Parcelable {
     val keyAddedToLibraryTime get() = "${gameId}_AddedToLibraryTime"
     val keyLastPlayedTime get() = "${gameId}_LastPlayed"
@@ -31,6 +32,7 @@ class Game(
                 && path == other.path
                 && gameId == other.gameId
                 && company == other.company
+                && isHomebrew == other.isHomebrew
     }
 
     companion object {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
index 7059856f13..d9b301210d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -13,6 +13,8 @@ import androidx.preference.PreferenceManager
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.MissingFieldException
 import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.json.Json
 import org.yuzu.yuzu_emu.NativeLibrary
@@ -20,6 +22,7 @@ import org.yuzu.yuzu_emu.YuzuApplication
 import org.yuzu.yuzu_emu.utils.GameHelper
 import java.util.Locale
 
+@OptIn(ExperimentalSerializationApi::class)
 class GamesViewModel : ViewModel() {
     private val _games = MutableLiveData<List<Game>>(emptyList())
     val games: LiveData<List<Game>> get() = _games
@@ -49,7 +52,13 @@ class GamesViewModel : ViewModel() {
         if (storedGames!!.isNotEmpty()) {
             val deserializedGames = mutableSetOf<Game>()
             storedGames.forEach {
-                val game: Game = Json.decodeFromString(it)
+                val game: Game
+                try {
+                    game = Json.decodeFromString(it)
+                } catch (e: MissingFieldException) {
+                    return@forEach
+                }
+
                 val gameExists =
                     DocumentFile.fromSingleUri(YuzuApplication.appContext, Uri.parse(game.path))
                         ?.exists()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
index ba6b5783ec..42b2076184 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
@@ -6,7 +6,6 @@ package org.yuzu.yuzu_emu.utils
 import android.content.SharedPreferences
 import android.net.Uri
 import androidx.preference.PreferenceManager
-import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.Json
 import org.yuzu.yuzu_emu.NativeLibrary
@@ -83,7 +82,8 @@ object GameHelper {
             NativeLibrary.getRegions(filePath),
             filePath,
             gameId,
-            NativeLibrary.getCompany(filePath)
+            NativeLibrary.getCompany(filePath),
+            NativeLibrary.isHomebrew(filePath)
         )
 
         val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L)
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index b87e04b3d8..03cb0b74b4 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -13,6 +13,7 @@
 
 #include <android/api-level.h>
 #include <android/native_window_jni.h>
+#include <core/loader/nro.h>
 
 #include "common/detached_tasks.h"
 #include "common/dynamic_library.h"
@@ -281,6 +282,10 @@ public:
         return GetRomMetadata(path).icon;
     }
 
+    bool GetIsHomebrew(const std::string& path) {
+        return GetRomMetadata(path).isHomebrew;
+    }
+
     void ResetRomMetadata() {
         m_rom_metadata_cache.clear();
     }
@@ -348,6 +353,7 @@ private:
     struct RomMetadata {
         std::string title;
         std::vector<u8> icon;
+        bool isHomebrew;
     };
 
     RomMetadata GetRomMetadata(const std::string& path) {
@@ -360,11 +366,17 @@ private:
 
     RomMetadata CacheRomMetadata(const std::string& path) {
         const auto file = Core::GetGameFileFromPath(m_vfs, path);
-        const auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0);
+        auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0);
 
         RomMetadata entry;
         loader->ReadTitle(entry.title);
         loader->ReadIcon(entry.icon);
+        if (loader->GetFileType() == Loader::FileType::NRO) {
+            auto loader_nro = dynamic_cast<Loader::AppLoader_NRO*>(loader.get());
+            entry.isHomebrew = loader_nro->IsHomebrew();
+        } else {
+            entry.isHomebrew = false;
+        }
 
         m_rom_metadata_cache[path] = entry;
 
@@ -662,6 +674,12 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCompany([[maybe_unused]] JNIEnv
     return env->NewStringUTF("");
 }
 
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHomebrew([[maybe_unused]] JNIEnv* env,
+                                                          [[maybe_unused]] jclass clazz,
+                                                          [[maybe_unused]] jstring j_filename) {
+    return EmulationSession::GetInstance().GetIsHomebrew(GetJString(env, j_filename));
+}
+
 void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmulation
     [[maybe_unused]] (JNIEnv* env, [[maybe_unused]] jclass clazz) {
     // Create the default config.ini.
diff --git a/src/core/loader/nro.cpp b/src/core/loader/nro.cpp
index 73d04d7ee5..7be6cf5f35 100644
--- a/src/core/loader/nro.cpp
+++ b/src/core/loader/nro.cpp
@@ -33,7 +33,8 @@ static_assert(sizeof(NroSegmentHeader) == 0x8, "NroSegmentHeader has incorrect s
 struct NroHeader {
     INSERT_PADDING_BYTES(0x4);
     u32_le module_header_offset;
-    INSERT_PADDING_BYTES(0x8);
+    u32 magic_ext1;
+    u32 magic_ext2;
     u32_le magic;
     INSERT_PADDING_BYTES(0x4);
     u32_le file_size;
@@ -124,6 +125,16 @@ FileType AppLoader_NRO::IdentifyType(const FileSys::VirtualFile& nro_file) {
     return FileType::Error;
 }
 
+bool AppLoader_NRO::IsHomebrew() {
+    // Read NSO header
+    NroHeader nro_header{};
+    if (sizeof(NroHeader) != file->ReadObject(&nro_header)) {
+        return false;
+    }
+    return nro_header.magic_ext1 == Common::MakeMagic('H', 'O', 'M', 'E') &&
+           nro_header.magic_ext2 == Common::MakeMagic('B', 'R', 'E', 'W');
+}
+
 static constexpr u32 PageAlignSize(u32 size) {
     return static_cast<u32>((size + Core::Memory::YUZU_PAGEMASK) & ~Core::Memory::YUZU_PAGEMASK);
 }
diff --git a/src/core/loader/nro.h b/src/core/loader/nro.h
index ccb77b581a..8de6eebc67 100644
--- a/src/core/loader/nro.h
+++ b/src/core/loader/nro.h
@@ -38,6 +38,8 @@ public:
      */
     static FileType IdentifyType(const FileSys::VirtualFile& nro_file);
 
+    bool IsHomebrew();
+
     FileType GetFileType() const override {
         return IdentifyType(file);
     }