From ac973e61bbc2309ca76e7cf93d157c5a1a6342df Mon Sep 17 00:00:00 2001
From: Lioncash <mathew1800@gmail.com>
Date: Mon, 23 Jan 2017 22:22:31 -0500
Subject: [PATCH] ES: Separate behavior of IOCtlV into separate functions

This function is exceptionally large. Everything within a switch like this
also makes it quite error prone. Separating the functions out makes it
easier to change a certain request implementation as well as improving
code locality.
---
 Source/Core/Core/IOS/ES/ES.cpp | 1906 +++++++++++++++++---------------
 Source/Core/Core/IOS/ES/ES.h   |   38 +
 2 files changed, 1029 insertions(+), 915 deletions(-)

diff --git a/Source/Core/Core/IOS/ES/ES.cpp b/Source/Core/Core/IOS/ES/ES.cpp
index 1cac5ee73c..b974c92a7c 100644
--- a/Source/Core/Core/IOS/ES/ES.cpp
+++ b/Source/Core/Core/IOS/ES/ES.cpp
@@ -249,970 +249,1046 @@ u32 ES::OpenTitleContent(u32 CFD, u64 TitleID, u16 Index)
 IPCCommandResult ES::IOCtlV(const IOCtlVRequest& request)
 {
   DEBUG_LOG(IOS_ES, "%s (0x%x)", GetDeviceName().c_str(), request.request);
+
   // Clear the IO buffers. Note that this is unsafe for other ioctlvs.
   for (const auto& io_vector : request.io_vectors)
   {
     if (!request.HasInputVectorWithAddress(io_vector.address))
       Memory::Memset(io_vector.address, 0, io_vector.size);
   }
+
   switch (request.request)
   {
   case IOCTL_ES_ADDTICKET:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 3,
-                     "IOCTL_ES_ADDTICKET wrong number of inputs");
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_ADDTICKET");
-    std::vector<u8> ticket(request.in_vectors[0].size);
-    Memory::CopyFromEmu(ticket.data(), request.in_vectors[0].address, request.in_vectors[0].size);
-    DiscIO::AddTicket(ticket);
-    break;
-  }
-
+    return AddTicket(request);
   case IOCTL_ES_ADDTITLESTART:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 4,
-                     "IOCTL_ES_ADDTITLESTART wrong number of inputs");
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_ADDTITLESTART");
-    std::vector<u8> tmd(request.in_vectors[0].size);
-    Memory::CopyFromEmu(tmd.data(), request.in_vectors[0].address, request.in_vectors[0].size);
-
-    m_addtitle_tmd.SetBytes(tmd);
-    if (!m_addtitle_tmd.IsValid())
-    {
-      ERROR_LOG(IOS_ES, "Invalid TMD while adding title (size = %zd)", tmd.size());
-      return GetDefaultReply(ES_INVALID_TMD);
-    }
-
-    // Write the TMD to title storage.
-    std::string tmd_path =
-        Common::GetTMDFileName(m_addtitle_tmd.GetTitleId(), Common::FROM_SESSION_ROOT);
-    File::CreateFullPath(tmd_path);
-
-    File::IOFile fp(tmd_path, "wb");
-    fp.WriteBytes(tmd.data(), tmd.size());
-    break;
-  }
-
+    return AddTitleStart(request);
   case IOCTL_ES_ADDCONTENTSTART:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 2,
-                     "IOCTL_ES_ADDCONTENTSTART wrong number of inputs");
-
-    u64 title_id = Memory::Read_U64(request.in_vectors[0].address);
-    u32 content_id = Memory::Read_U32(request.in_vectors[1].address);
-
-    if (m_addtitle_content_id != 0xFFFFFFFF)
-    {
-      ERROR_LOG(IOS_ES, "Trying to add content when we haven't finished adding "
-                        "another content. Unsupported.");
-      return GetDefaultReply(ES_WRITE_FAILURE);
-    }
-    m_addtitle_content_id = content_id;
-
-    m_addtitle_content_buffer.clear();
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTSTART: title id %016" PRIx64 ", "
-                     "content id %08x",
-             title_id, m_addtitle_content_id);
-
-    if (title_id != m_addtitle_tmd.GetTitleId())
-    {
-      ERROR_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTSTART: title id %016" PRIx64 " != "
-                        "TMD title id %016lx, ignoring",
-                title_id, m_addtitle_tmd.GetTitleId());
-    }
-
-    // We're supposed to return a "content file descriptor" here, which is
-    // passed to further AddContentData / AddContentFinish. But so far there is
-    // no known content installer which performs content addition concurrently.
-    // Instead we just log an error (see above) if this condition is detected.
-    s32 content_fd = 0;
-    return GetDefaultReply(content_fd);
-  }
-
+    return AddContentStart(request);
   case IOCTL_ES_ADDCONTENTDATA:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 2,
-                     "IOCTL_ES_ADDCONTENTDATA wrong number of inputs");
-
-    u32 content_fd = Memory::Read_U32(request.in_vectors[0].address);
-    INFO_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTDATA: content fd %08x, "
-                     "size %d",
-             content_fd, request.in_vectors[1].size);
-
-    u8* data_start = Memory::GetPointer(request.in_vectors[1].address);
-    u8* data_end = data_start + request.in_vectors[1].size;
-    m_addtitle_content_buffer.insert(m_addtitle_content_buffer.end(), data_start, data_end);
-    break;
-  }
-
+    return AddContentData(request);
   case IOCTL_ES_ADDCONTENTFINISH:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 1,
-                     "IOCTL_ES_ADDCONTENTFINISH wrong number of inputs");
-
-    u32 content_fd = Memory::Read_U32(request.in_vectors[0].address);
-    INFO_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTFINISH: content fd %08x", content_fd);
-
-    // Try to find the title key from a pre-installed ticket.
-    std::vector<u8> ticket = DiscIO::FindSignedTicket(m_addtitle_tmd.GetTitleId());
-    if (ticket.size() == 0)
-    {
-      return GetDefaultReply(ES_NO_TICKET_INSTALLED);
-    }
-
-    mbedtls_aes_context aes_ctx;
-    mbedtls_aes_setkey_dec(&aes_ctx, DiscIO::GetKeyFromTicket(ticket).data(), 128);
-
-    // The IV for title content decryption is the lower two bytes of the
-    // content index, zero extended.
-    TMDReader::Content content_info;
-    if (!m_addtitle_tmd.FindContentById(m_addtitle_content_id, &content_info))
-    {
-      return GetDefaultReply(ES_INVALID_TMD);
-    }
-    u8 iv[16] = {0};
-    iv[0] = (content_info.index >> 8) & 0xFF;
-    iv[1] = content_info.index & 0xFF;
-    std::vector<u8> decrypted_data(m_addtitle_content_buffer.size());
-    mbedtls_aes_crypt_cbc(&aes_ctx, MBEDTLS_AES_DECRYPT, m_addtitle_content_buffer.size(), iv,
-                          m_addtitle_content_buffer.data(), decrypted_data.data());
-
-    std::string path = StringFromFormat(
-        "%s%08x.app",
-        Common::GetTitleContentPath(m_addtitle_tmd.GetTitleId(), Common::FROM_SESSION_ROOT).c_str(),
-        m_addtitle_content_id);
-
-    File::IOFile fp(path, "wb");
-    fp.WriteBytes(decrypted_data.data(), decrypted_data.size());
-
-    m_addtitle_content_id = 0xFFFFFFFF;
-    break;
-  }
-
+    return AddContentFinish(request);
   case IOCTL_ES_ADDTITLEFINISH:
-  {
-    INFO_LOG(IOS_ES, "IOCTL_ES_ADDTITLEFINISH");
-    break;
-  }
-
+    return AddTitleFinish(request);
   case IOCTL_ES_GETDEVICEID:
-  {
-    _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1, "IOCTL_ES_GETDEVICEID no io vectors");
-
-    EcWii& ec = EcWii::GetInstance();
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETDEVICEID %08X", ec.getNgId());
-    Memory::Write_U32(ec.getNgId(), request.io_vectors[0].address);
-    return GetDefaultReply(IPC_SUCCESS);
-  }
-
+    return ESGetDeviceID(request);
   case IOCTL_ES_GETTITLECONTENTSCNT:
-  {
-    _dbg_assert_(IOS_ES, request.in_vectors.size() == 1);
-    _dbg_assert_(IOS_ES, request.io_vectors.size() == 1);
-
-    u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
-
-    const DiscIO::CNANDContentLoader& rNANDContent = AccessContentDevice(TitleID);
-    u16 NumberOfPrivateContent = 0;
-    s32 return_value = IPC_SUCCESS;
-    if (rNANDContent.IsValid())  // Not sure if dolphin will ever fail this check
-    {
-      NumberOfPrivateContent = rNANDContent.GetNumEntries();
-
-      if ((u32)(TitleID >> 32) == 0x00010000)
-        Memory::Write_U32(0, request.io_vectors[0].address);
-      else
-        Memory::Write_U32(NumberOfPrivateContent, request.io_vectors[0].address);
-    }
-    else
-    {
-      return_value = static_cast<s32>(rNANDContent.GetContentSize());
-    }
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLECONTENTSCNT: TitleID: %08x/%08x  content count %i",
-             (u32)(TitleID >> 32), (u32)TitleID,
-             rNANDContent.IsValid() ? NumberOfPrivateContent : (u32)rNANDContent.GetContentSize());
-
-    return GetDefaultReply(return_value);
-  }
-  break;
-
+    return GetTitleContentsCount(request);
   case IOCTL_ES_GETTITLECONTENTS:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 2,
-                     "IOCTL_ES_GETTITLECONTENTS bad in buffer");
-    _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1,
-                     "IOCTL_ES_GETTITLECONTENTS bad out buffer");
-
-    u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
-
-    const DiscIO::CNANDContentLoader& rNANDContent = AccessContentDevice(TitleID);
-    s32 return_value = IPC_SUCCESS;
-    if (rNANDContent.IsValid())  // Not sure if dolphin will ever fail this check
-    {
-      for (u16 i = 0; i < rNANDContent.GetNumEntries(); i++)
-      {
-        Memory::Write_U32(rNANDContent.GetContentByIndex(i)->m_ContentID,
-                          request.io_vectors[0].address + i * 4);
-        INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLECONTENTS: Index %d: %08x", i,
-                 rNANDContent.GetContentByIndex(i)->m_ContentID);
-      }
-    }
-    else
-    {
-      return_value = static_cast<s32>(rNANDContent.GetContentSize());
-      ERROR_LOG(IOS_ES, "IOCTL_ES_GETTITLECONTENTS: Unable to open content %zu",
-                rNANDContent.GetContentSize());
-    }
-
-    return GetDefaultReply(return_value);
-  }
-  break;
-
+    return GetTitleContents(request);
   case IOCTL_ES_OPENTITLECONTENT:
-  {
-    _dbg_assert_(IOS_ES, request.in_vectors.size() == 3);
-    _dbg_assert_(IOS_ES, request.io_vectors.size() == 0);
-
-    u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
-    u32 Index = Memory::Read_U32(request.in_vectors[2].address);
-
-    s32 CFD = OpenTitleContent(m_AccessIdentID++, TitleID, Index);
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_OPENTITLECONTENT: TitleID: %08x/%08x  Index %i -> got CFD %x",
-             (u32)(TitleID >> 32), (u32)TitleID, Index, CFD);
-
-    return GetDefaultReply(CFD);
-  }
-  break;
-
+    return OpenTitleContent(request);
   case IOCTL_ES_OPENCONTENT:
-  {
-    _dbg_assert_(IOS_ES, request.in_vectors.size() == 1);
-    _dbg_assert_(IOS_ES, request.io_vectors.size() == 0);
-    u32 Index = Memory::Read_U32(request.in_vectors[0].address);
-
-    s32 CFD = OpenTitleContent(m_AccessIdentID++, m_TitleID, Index);
-    INFO_LOG(IOS_ES, "IOCTL_ES_OPENCONTENT: Index %i -> got CFD %x", Index, CFD);
-
-    return GetDefaultReply(CFD);
-  }
-  break;
-
+    return OpenContent(request);
   case IOCTL_ES_READCONTENT:
-  {
-    _dbg_assert_(IOS_ES, request.in_vectors.size() == 1);
-    _dbg_assert_(IOS_ES, request.io_vectors.size() == 1);
-
-    u32 CFD = Memory::Read_U32(request.in_vectors[0].address);
-    u32 Size = request.io_vectors[0].size;
-    u32 Addr = request.io_vectors[0].address;
-
-    auto itr = m_ContentAccessMap.find(CFD);
-    if (itr == m_ContentAccessMap.end())
-    {
-      return GetDefaultReply(-1);
-    }
-    SContentAccess& rContent = itr->second;
-
-    u8* pDest = Memory::GetPointer(Addr);
-
-    if (rContent.m_Position + Size > rContent.m_Size)
-    {
-      Size = rContent.m_Size - rContent.m_Position;
-    }
-
-    if (Size > 0)
-    {
-      if (pDest)
-      {
-        const DiscIO::CNANDContentLoader& ContentLoader = AccessContentDevice(rContent.m_TitleID);
-        // ContentLoader should never be invalid; rContent has been created by it.
-        if (ContentLoader.IsValid())
-        {
-          const DiscIO::SNANDContent* pContent = ContentLoader.GetContentByIndex(rContent.m_Index);
-          if (!pContent->m_Data->GetRange(rContent.m_Position, Size, pDest))
-            ERROR_LOG(IOS_ES, "ES: failed to read %u bytes from %u!", Size, rContent.m_Position);
-        }
-
-        rContent.m_Position += Size;
-      }
-      else
-      {
-        PanicAlert("IOCTL_ES_READCONTENT - bad destination");
-      }
-    }
-
-    DEBUG_LOG(IOS_ES,
-              "IOCTL_ES_READCONTENT: CFD %x, Address 0x%x, Size %i -> stream pos %i (Index %i)",
-              CFD, Addr, Size, rContent.m_Position, rContent.m_Index);
-
-    return GetDefaultReply(Size);
-  }
-  break;
-
+    return ReadContent(request);
   case IOCTL_ES_CLOSECONTENT:
-  {
-    _dbg_assert_(IOS_ES, request.in_vectors.size() == 1);
-    _dbg_assert_(IOS_ES, request.io_vectors.size() == 0);
-
-    u32 CFD = Memory::Read_U32(request.in_vectors[0].address);
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_CLOSECONTENT: CFD %x", CFD);
-
-    auto itr = m_ContentAccessMap.find(CFD);
-    if (itr == m_ContentAccessMap.end())
-    {
-      return GetDefaultReply(-1);
-    }
-
-    const DiscIO::CNANDContentLoader& ContentLoader = AccessContentDevice(itr->second.m_TitleID);
-    // ContentLoader should never be invalid; we shouldn't be here if ES_OPENCONTENT failed before.
-    if (ContentLoader.IsValid())
-    {
-      const DiscIO::SNANDContent* pContent = ContentLoader.GetContentByIndex(itr->second.m_Index);
-      pContent->m_Data->Close();
-    }
-
-    m_ContentAccessMap.erase(itr);
-
-    return GetDefaultReply(IPC_SUCCESS);
-  }
-  break;
-
+    return CloseContent(request);
   case IOCTL_ES_SEEKCONTENT:
-  {
-    _dbg_assert_(IOS_ES, request.in_vectors.size() == 3);
-    _dbg_assert_(IOS_ES, request.io_vectors.size() == 0);
-
-    u32 CFD = Memory::Read_U32(request.in_vectors[0].address);
-    u32 Addr = Memory::Read_U32(request.in_vectors[1].address);
-    u32 Mode = Memory::Read_U32(request.in_vectors[2].address);
-
-    auto itr = m_ContentAccessMap.find(CFD);
-    if (itr == m_ContentAccessMap.end())
-    {
-      return GetDefaultReply(-1);
-    }
-    SContentAccess& rContent = itr->second;
-
-    switch (Mode)
-    {
-    case 0:  // SET
-      rContent.m_Position = Addr;
-      break;
-
-    case 1:  // CUR
-      rContent.m_Position += Addr;
-      break;
-
-    case 2:  // END
-      rContent.m_Position = rContent.m_Size + Addr;
-      break;
-    }
-
-    DEBUG_LOG(IOS_ES, "IOCTL_ES_SEEKCONTENT: CFD %x, Address 0x%x, Mode %i -> Pos %i", CFD, Addr,
-              Mode, rContent.m_Position);
-
-    return GetDefaultReply(rContent.m_Position);
-  }
-  break;
-
+    return SeekContent(request);
   case IOCTL_ES_GETTITLEDIR:
-  {
-    _dbg_assert_(IOS_ES, request.in_vectors.size() == 1);
-    _dbg_assert_(IOS_ES, request.io_vectors.size() == 1);
-
-    u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
-
-    char* Path = (char*)Memory::GetPointer(request.io_vectors[0].address);
-    sprintf(Path, "/title/%08x/%08x/data", (u32)(TitleID >> 32), (u32)TitleID);
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLEDIR: %s", Path);
-  }
-  break;
-
+    return GetTitleDirectory(request);
   case IOCTL_ES_GETTITLEID:
-  {
-    _dbg_assert_(IOS_ES, request.in_vectors.size() == 0);
-    _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1, "IOCTL_ES_GETTITLEID no out buffer");
-
-    Memory::Write_U64(m_TitleID, request.io_vectors[0].address);
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLEID: %08x/%08x", (u32)(m_TitleID >> 32), (u32)m_TitleID);
-  }
-  break;
-
+    return GetTitleID(request);
   case IOCTL_ES_SETUID:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 1, "IOCTL_ES_SETUID no in buffer");
-    _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 0,
-                     "IOCTL_ES_SETUID has a payload, it shouldn't");
-    // TODO: fs permissions based on this
-    u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
-    INFO_LOG(IOS_ES, "IOCTL_ES_SETUID titleID: %08x/%08x", (u32)(TitleID >> 32), (u32)TitleID);
-  }
-  break;
-
+    return SetUID(request);
   case IOCTL_ES_GETTITLECNT:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 0,
-                     "IOCTL_ES_GETTITLECNT has an in buffer");
-    _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1,
-                     "IOCTL_ES_GETTITLECNT has no out buffer");
-    _dbg_assert_msg_(IOS_ES, request.io_vectors[0].size == 4,
-                     "IOCTL_ES_GETTITLECNT payload[0].size != 4");
-
-    Memory::Write_U32((u32)m_TitleIDs.size(), request.io_vectors[0].address);
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLECNT: Number of Titles %zu", m_TitleIDs.size());
-
-    return GetDefaultReply(IPC_SUCCESS);
-  }
-  break;
-
+    return GetTitleCount(request);
   case IOCTL_ES_GETTITLES:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 1, "IOCTL_ES_GETTITLES has an in buffer");
-    _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1,
-                     "IOCTL_ES_GETTITLES has no out buffer");
-
-    u32 MaxCount = Memory::Read_U32(request.in_vectors[0].address);
-    u32 Count = 0;
-    for (int i = 0; i < (int)m_TitleIDs.size(); i++)
-    {
-      Memory::Write_U64(m_TitleIDs[i], request.io_vectors[0].address + i * 8);
-      INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLES: %08x/%08x", (u32)(m_TitleIDs[i] >> 32),
-               (u32)m_TitleIDs[i]);
-      Count++;
-      if (Count >= MaxCount)
-        break;
-    }
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLES: Number of titles returned %i", Count);
-    return GetDefaultReply(IPC_SUCCESS);
-  }
-  break;
-
+    return GetTitles(request);
   case IOCTL_ES_GETVIEWCNT:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 1, "IOCTL_ES_GETVIEWCNT no in buffer");
-    _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1, "IOCTL_ES_GETVIEWCNT no out buffer");
-
-    u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
-
-    u32 retVal = 0;
-    const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
-    u32 ViewCount =
-        static_cast<u32>(Loader.GetTicket().size()) / DiscIO::CNANDContentLoader::TICKET_SIZE;
-
-    if (!ViewCount)
-    {
-      std::string TicketFilename = Common::GetTicketFileName(TitleID, Common::FROM_SESSION_ROOT);
-      if (File::Exists(TicketFilename))
-      {
-        u32 FileSize = (u32)File::GetSize(TicketFilename);
-        _dbg_assert_msg_(IOS_ES, (FileSize % DiscIO::CNANDContentLoader::TICKET_SIZE) == 0,
-                         "IOCTL_ES_GETVIEWCNT ticket file size seems to be wrong");
-
-        ViewCount = FileSize / DiscIO::CNANDContentLoader::TICKET_SIZE;
-        _dbg_assert_msg_(IOS_ES, (ViewCount > 0) && (ViewCount <= 4),
-                         "IOCTL_ES_GETVIEWCNT ticket count seems to be wrong");
-      }
-      else if (TitleID >> 32 == 0x00000001)
-      {
-        // Fake a ticket view to make IOS reload work.
-        ViewCount = 1;
-      }
-      else
-      {
-        ViewCount = 0;
-        if (TitleID == TITLEID_SYSMENU)
-        {
-          PanicAlertT("There must be a ticket for 00000001/00000002. Your NAND dump is probably "
-                      "incomplete.");
-        }
-        // retVal = ES_NO_TICKET_INSTALLED;
-      }
-    }
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETVIEWCNT for titleID: %08x/%08x (View Count = %i)",
-             (u32)(TitleID >> 32), (u32)TitleID, ViewCount);
-
-    Memory::Write_U32(ViewCount, request.io_vectors[0].address);
-    return GetDefaultReply(retVal);
-  }
-  break;
-
+    return GetViewCount(request);
   case IOCTL_ES_GETVIEWS:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 2, "IOCTL_ES_GETVIEWS no in buffer");
-    _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1, "IOCTL_ES_GETVIEWS no out buffer");
-
-    u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
-    u32 maxViews = Memory::Read_U32(request.in_vectors[1].address);
-    u32 retVal = 0;
-
-    const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
-
-    const std::vector<u8>& ticket = Loader.GetTicket();
-
-    if (ticket.empty())
-    {
-      std::string TicketFilename = Common::GetTicketFileName(TitleID, Common::FROM_SESSION_ROOT);
-      if (File::Exists(TicketFilename))
-      {
-        File::IOFile pFile(TicketFilename, "rb");
-        if (pFile)
-        {
-          u8 FileTicket[DiscIO::CNANDContentLoader::TICKET_SIZE];
-          for (unsigned int View = 0;
-               View != maxViews &&
-               pFile.ReadBytes(FileTicket, DiscIO::CNANDContentLoader::TICKET_SIZE);
-               ++View)
-          {
-            Memory::Write_U32(View, request.io_vectors[0].address + View * 0xD8);
-            Memory::CopyToEmu(request.io_vectors[0].address + 4 + View * 0xD8, FileTicket + 0x1D0,
-                              212);
-          }
-        }
-      }
-      else if (TitleID >> 32 == 0x00000001)
-      {
-        // For IOS titles, the ticket view isn't normally parsed by either the
-        // SDK or libogc, just passed to LaunchTitle, so this
-        // shouldn't matter at all.  Just fill out some fields just
-        // to be on the safe side.
-        u32 Address = request.io_vectors[0].address;
-        Memory::Memset(Address, 0, 0xD8);
-        Memory::Write_U64(TitleID, Address + 4 + (0x1dc - 0x1d0));  // title ID
-        Memory::Write_U16(0xffff, Address + 4 + (0x1e4 - 0x1d0));   // unnnown
-        Memory::Write_U32(0xff00, Address + 4 + (0x1ec - 0x1d0));   // access mask
-        Memory::Memset(Address + 4 + (0x222 - 0x1d0), 0xff, 0x20);  // content permissions
-      }
-      else
-      {
-        // retVal = ES_NO_TICKET_INSTALLED;
-        PanicAlertT("IOCTL_ES_GETVIEWS: Tried to get data from an unknown ticket: %08x/%08x",
-                    (u32)(TitleID >> 32), (u32)TitleID);
-      }
-    }
-    else
-    {
-      u32 view_count =
-          static_cast<u32>(Loader.GetTicket().size()) / DiscIO::CNANDContentLoader::TICKET_SIZE;
-      for (unsigned int view = 0; view != maxViews && view < view_count; ++view)
-      {
-        Memory::Write_U32(view, request.io_vectors[0].address + view * 0xD8);
-        Memory::CopyToEmu(request.io_vectors[0].address + 4 + view * 0xD8,
-                          &ticket[0x1D0 + (view * DiscIO::CNANDContentLoader::TICKET_SIZE)], 212);
-      }
-    }
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETVIEWS for titleID: %08x/%08x (MaxViews = %i)",
-             (u32)(TitleID >> 32), (u32)TitleID, maxViews);
-
-    return GetDefaultReply(retVal);
-  }
-  break;
-
+    return GetViews(request);
   case IOCTL_ES_GETTMDVIEWCNT:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 1, "IOCTL_ES_GETTMDVIEWCNT no in buffer");
-    _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1,
-                     "IOCTL_ES_GETTMDVIEWCNT no out buffer");
-
-    u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
-
-    const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
-
-    u32 TMDViewCnt = 0;
-    if (Loader.IsValid())
-    {
-      TMDViewCnt += DiscIO::CNANDContentLoader::TMD_VIEW_SIZE;
-      TMDViewCnt += 2;  // title version
-      TMDViewCnt += 2;  // num entries
-      TMDViewCnt +=
-          (u32)Loader.GetContentSize() * (4 + 2 + 2 + 8);  // content id, index, type, size
-    }
-    Memory::Write_U32(TMDViewCnt, request.io_vectors[0].address);
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETTMDVIEWCNT: title: %08x/%08x (view size %i)",
-             (u32)(TitleID >> 32), (u32)TitleID, TMDViewCnt);
-    return GetDefaultReply(IPC_SUCCESS);
-  }
-  break;
-
+    return GetTMDViewCount(request);
   case IOCTL_ES_GETTMDVIEWS:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 2, "IOCTL_ES_GETTMDVIEWCNT no in buffer");
-    _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1,
-                     "IOCTL_ES_GETTMDVIEWCNT no out buffer");
-
-    u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
-    u32 MaxCount = Memory::Read_U32(request.in_vectors[1].address);
-
-    const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETTMDVIEWCNT: title: %08x/%08x   buffer size: %i",
-             (u32)(TitleID >> 32), (u32)TitleID, MaxCount);
-
-    if (Loader.IsValid())
-    {
-      u32 Address = request.io_vectors[0].address;
-
-      Memory::CopyToEmu(Address, Loader.GetTMDView(), DiscIO::CNANDContentLoader::TMD_VIEW_SIZE);
-      Address += DiscIO::CNANDContentLoader::TMD_VIEW_SIZE;
-
-      Memory::Write_U16(Loader.GetTitleVersion(), Address);
-      Address += 2;
-      Memory::Write_U16(Loader.GetNumEntries(), Address);
-      Address += 2;
-
-      const std::vector<DiscIO::SNANDContent>& rContent = Loader.GetContent();
-      for (size_t i = 0; i < Loader.GetContentSize(); i++)
-      {
-        Memory::Write_U32(rContent[i].m_ContentID, Address);
-        Address += 4;
-        Memory::Write_U16(rContent[i].m_Index, Address);
-        Address += 2;
-        Memory::Write_U16(rContent[i].m_Type, Address);
-        Address += 2;
-        Memory::Write_U64(rContent[i].m_Size, Address);
-        Address += 8;
-      }
-
-      _dbg_assert_(IOS_ES, (Address - request.io_vectors[0].address) == request.io_vectors[0].size);
-    }
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETTMDVIEWS: title: %08x/%08x (buffer size: %i)",
-             (u32)(TitleID >> 32), (u32)TitleID, MaxCount);
-    return GetDefaultReply(IPC_SUCCESS);
-  }
-  break;
-
-  case IOCTL_ES_GETCONSUMPTION:  // This is at least what crediar's ES module does
-    Memory::Write_U32(0, request.io_vectors[1].address);
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETCONSUMPTION");
-    return GetDefaultReply(IPC_SUCCESS);
-
+    return GetTMDViews(request);
+  case IOCTL_ES_GETCONSUMPTION:
+    return GetConsumption(request);
   case IOCTL_ES_DELETETICKET:
-  {
-    u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
-    INFO_LOG(IOS_ES, "IOCTL_ES_DELETETICKET: title: %08x/%08x", (u32)(TitleID >> 32), (u32)TitleID);
-    // Presumably return -1017 when delete fails
-    if (!File::Delete(Common::GetTicketFileName(TitleID, Common::FROM_SESSION_ROOT)))
-      return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
-    return GetDefaultReply(IPC_SUCCESS);
-  }
-
+    return DeleteTicket(request);
   case IOCTL_ES_DELETETITLECONTENT:
-  {
-    u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
-    INFO_LOG(IOS_ES, "IOCTL_ES_DELETETITLECONTENT: title: %08x/%08x", (u32)(TitleID >> 32),
-             (u32)TitleID);
-    // Presumably return -1017 when title not installed TODO verify
-    if (!DiscIO::CNANDContentManager::Access().RemoveTitle(TitleID, Common::FROM_SESSION_ROOT))
-      return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
-    return GetDefaultReply(IPC_SUCCESS);
-  }
-
+    return DeleteTitleContent(request);
   case IOCTL_ES_GETSTOREDTMDSIZE:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 1,
-                     "IOCTL_ES_GETSTOREDTMDSIZE no in buffer");
-    // _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1, "IOCTL_ES_ES_GETSTOREDTMDSIZE
-    // no out buffer");
-
-    u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
-    const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
-
-    _dbg_assert_(IOS_ES, Loader.IsValid());
-    u32 TMDCnt = 0;
-    if (Loader.IsValid())
-    {
-      TMDCnt += DiscIO::CNANDContentLoader::TMD_HEADER_SIZE;
-      TMDCnt += (u32)Loader.GetContentSize() * DiscIO::CNANDContentLoader::CONTENT_HEADER_SIZE;
-    }
-    if (request.io_vectors.size())
-      Memory::Write_U32(TMDCnt, request.io_vectors[0].address);
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETSTOREDTMDSIZE: title: %08x/%08x (view size %i)",
-             (u32)(TitleID >> 32), (u32)TitleID, TMDCnt);
-    return GetDefaultReply(IPC_SUCCESS);
-  }
-  break;
+    return GetStoredTMDSize(request);
   case IOCTL_ES_GETSTOREDTMD:
-  {
-    _dbg_assert_msg_(IOS_ES, request.in_vectors.size() > 0, "IOCTL_ES_GETSTOREDTMD no in buffer");
-    // requires 1 inbuffer and no outbuffer, presumably outbuffer required when second inbuffer is
-    // used for maxcount (allocated mem?)
-    // called with 1 inbuffer after deleting a titleid
-    //_dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1, "IOCTL_ES_GETSTOREDTMD no out
-    // buffer");
-
-    u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
-    u32 MaxCount = 0;
-    if (request.in_vectors.size() > 1)
-    {
-      // TODO: actually use this param in when writing to the outbuffer :/
-      MaxCount = Memory::Read_U32(request.in_vectors[1].address);
-    }
-    const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETSTOREDTMD: title: %08x/%08x   buffer size: %i",
-             (u32)(TitleID >> 32), (u32)TitleID, MaxCount);
-
-    if (Loader.IsValid() && request.io_vectors.size())
-    {
-      u32 Address = request.io_vectors[0].address;
-
-      Memory::CopyToEmu(Address, Loader.GetTMDHeader(),
-                        DiscIO::CNANDContentLoader::TMD_HEADER_SIZE);
-      Address += DiscIO::CNANDContentLoader::TMD_HEADER_SIZE;
-
-      const std::vector<DiscIO::SNANDContent>& rContent = Loader.GetContent();
-      for (size_t i = 0; i < Loader.GetContentSize(); i++)
-      {
-        Memory::CopyToEmu(Address, rContent[i].m_Header,
-                          DiscIO::CNANDContentLoader::CONTENT_HEADER_SIZE);
-        Address += DiscIO::CNANDContentLoader::CONTENT_HEADER_SIZE;
-      }
-
-      _dbg_assert_(IOS_ES, (Address - request.io_vectors[0].address) == request.io_vectors[0].size);
-    }
-
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETSTOREDTMD: title: %08x/%08x (buffer size: %i)",
-             (u32)(TitleID >> 32), (u32)TitleID, MaxCount);
-    return GetDefaultReply(IPC_SUCCESS);
-  }
-  break;
-
+    return GetStoredTMD(request);
   case IOCTL_ES_ENCRYPT:
-  {
-    u32 keyIndex = Memory::Read_U32(request.in_vectors[0].address);
-    u8* IV = Memory::GetPointer(request.in_vectors[1].address);
-    u8* source = Memory::GetPointer(request.in_vectors[2].address);
-    u32 size = request.in_vectors[2].size;
-    u8* newIV = Memory::GetPointer(request.io_vectors[0].address);
-    u8* destination = Memory::GetPointer(request.io_vectors[1].address);
-
-    mbedtls_aes_context AES_ctx;
-    mbedtls_aes_setkey_enc(&AES_ctx, s_key_table[keyIndex], 128);
-    memcpy(newIV, IV, 16);
-    mbedtls_aes_crypt_cbc(&AES_ctx, MBEDTLS_AES_ENCRYPT, size, newIV, source, destination);
-
-    _dbg_assert_msg_(IOS_ES, keyIndex == 6,
-                     "IOCTL_ES_ENCRYPT: Key type is not SD, data will be crap");
-  }
-  break;
-
+    return Encrypt(request);
   case IOCTL_ES_DECRYPT:
-  {
-    u32 keyIndex = Memory::Read_U32(request.in_vectors[0].address);
-    u8* IV = Memory::GetPointer(request.in_vectors[1].address);
-    u8* source = Memory::GetPointer(request.in_vectors[2].address);
-    u32 size = request.in_vectors[2].size;
-    u8* newIV = Memory::GetPointer(request.io_vectors[0].address);
-    u8* destination = Memory::GetPointer(request.io_vectors[1].address);
-
-    DecryptContent(keyIndex, IV, source, size, newIV, destination);
-
-    _dbg_assert_msg_(IOS_ES, keyIndex == 6,
-                     "IOCTL_ES_DECRYPT: Key type is not SD, data will be crap");
-  }
-  break;
-
+    return Decrypt(request);
   case IOCTL_ES_LAUNCH:
-  {
-    _dbg_assert_(IOS_ES, request.in_vectors.size() == 2);
-    bool bSuccess = false;
-    bool bReset = false;
-
-    u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
-    u32 view = Memory::Read_U32(request.in_vectors[1].address);
-    u64 ticketid = Memory::Read_U64(request.in_vectors[1].address + 4);
-    u32 devicetype = Memory::Read_U32(request.in_vectors[1].address + 12);
-    u64 titleid = Memory::Read_U64(request.in_vectors[1].address + 16);
-    u16 access = Memory::Read_U16(request.in_vectors[1].address + 24);
-
-    // ES_LAUNCH should probably reset thw whole state, which at least means closing all open files.
-    // leaving them open through ES_LAUNCH may cause hangs and other funky behavior
-    // (supposedly when trying to re-open those files).
-    DiscIO::CNANDContentManager::Access().ClearCache();
-
-    u64 ios_to_load = 0;
-    std::string tContentFile;
-    if ((u32)(TitleID >> 32) == 0x00000001 && TitleID != TITLEID_SYSMENU)
-    {
-      ios_to_load = TitleID;
-      bSuccess = true;
-    }
-    else
-    {
-      const DiscIO::CNANDContentLoader& ContentLoader = AccessContentDevice(TitleID);
-      if (ContentLoader.IsValid())
-      {
-        ios_to_load = 0x0000000100000000ULL | ContentLoader.GetIosVersion();
-
-        u32 bootInd = ContentLoader.GetBootIndex();
-        const DiscIO::SNANDContent* pContent = ContentLoader.GetContentByIndex(bootInd);
-        if (pContent)
-        {
-          tContentFile = Common::GetTitleContentPath(TitleID, Common::FROM_SESSION_ROOT);
-          std::unique_ptr<CDolLoader> pDolLoader =
-              std::make_unique<CDolLoader>(pContent->m_Data->Get());
-
-          if (pDolLoader->IsValid())
-          {
-            pDolLoader->Load();
-            // TODO: Check why sysmenu does not load the DOL correctly
-            // NAND titles start with address translation off at 0x3400 (via the PPC bootstub)
-            //
-            // The state of other CPU registers (like the BAT registers) doesn't matter much
-            // because the realmode code at 0x3400 initializes everything itself anyway.
-            MSR = 0;
-            PC = 0x3400;
-            bSuccess = true;
-          }
-          else
-          {
-            PanicAlertT("IOCTL_ES_LAUNCH: The DOL file is invalid!");
-          }
-        }
-      }
-    }
-
-    if (!bSuccess)
-    {
-      PanicAlertT(
-          "IOCTL_ES_LAUNCH: Game tried to reload a title that is not available in your NAND dump\n"
-          "TitleID %016" PRIx64 ".\n Dolphin will likely hang now.",
-          TitleID);
-    }
-    else
-    {
-      bool* wiiMoteConnected = new bool[MAX_BBMOTES];
-      if (!SConfig::GetInstance().m_bt_passthrough_enabled)
-      {
-        BluetoothEmu* s_Usb = GetUsbPointer();
-        for (unsigned int i = 0; i < MAX_BBMOTES; i++)
-          wiiMoteConnected[i] = s_Usb->m_WiiMotes[i].IsConnected();
-      }
-
-      Reset(true);
-      Reinit();
-      SetupMemory(ios_to_load);
-      bReset = true;
-
-      if (!SConfig::GetInstance().m_bt_passthrough_enabled)
-      {
-        BluetoothEmu* s_Usb = GetUsbPointer();
-        for (unsigned int i = 0; i < MAX_BBMOTES; i++)
-        {
-          if (wiiMoteConnected[i])
-          {
-            s_Usb->m_WiiMotes[i].Activate(false);
-            s_Usb->m_WiiMotes[i].Activate(true);
-          }
-          else
-          {
-            s_Usb->m_WiiMotes[i].Activate(false);
-          }
-        }
-      }
-      delete[] wiiMoteConnected;
-      SetDefaultContentFile(tContentFile);
-    }
-
-    // Note: If we just reset the PPC, don't write anything to the command buffer. This
-    // could clobber the DOL we just loaded.
-
-    ERROR_LOG(IOS_ES, "IOCTL_ES_LAUNCH %016" PRIx64 " %08x %016" PRIx64 " %08x %016" PRIx64 " %04x",
-              TitleID, view, ticketid, devicetype, titleid, access);
-    //                     IOCTL_ES_LAUNCH 0001000248414341 00000001 0001c0fef3df2cfa 00000000
-    //                     0001000248414341 ffff
-
-    // This is necessary because Reset(true) above deleted this object.  Ew.
-
-    if (!bReset)
-    {
-      // The command type is overwritten with the reply type.
-      Memory::Write_U32(IPC_REPLY, request.address);
-      // IOS also writes back the command that was responded to in the FD field.
-      Memory::Write_U32(IPC_CMD_IOCTLV, request.address + 8);
-    }
-
-    // Generate a "reply" to the IPC command.  ES_LAUNCH is unique because it
-    // involves restarting IOS; IOS generates two acknowledgements in a row.
-    EnqueueCommandAcknowledgement(request.address, 0);
-    return GetNoReply();
-  }
-  break;
-
-  case IOCTL_ES_CHECKKOREAREGION:  // note by DacoTaco : name is unknown, i just tried to name it
-                                   // SOMETHING
-    // IOS70 has this to let system menu 4.2 check if the console is region changed. it returns
-    // -1017
-    // if the IOS didn't find the Korean keys and 0 if it does. 0 leads to a error 003
-    INFO_LOG(IOS_ES, "IOCTL_ES_CHECKKOREAREGION: Title checked for Korean keys.");
-    return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
-
-  case IOCTL_ES_GETDEVICECERT:  // (Input: none, Output: 384 bytes)
-  {
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETDEVICECERT");
-    _dbg_assert_(IOS_ES, request.io_vectors.size() == 1);
-    u8* destination = Memory::GetPointer(request.io_vectors[0].address);
-
-    EcWii& ec = EcWii::GetInstance();
-    get_ng_cert(destination, ec.getNgId(), ec.getNgKeyId(), ec.getNgPriv(), ec.getNgSig());
-  }
-  break;
-
+    return Launch(request);
+  case IOCTL_ES_CHECKKOREAREGION:
+    return CheckKoreaRegion(request);
+  case IOCTL_ES_GETDEVICECERT:
+    return GetDeviceCertificate(request);
   case IOCTL_ES_SIGN:
-  {
-    INFO_LOG(IOS_ES, "IOCTL_ES_SIGN");
-    u8* ap_cert_out = Memory::GetPointer(request.io_vectors[1].address);
-    u8* data = Memory::GetPointer(request.in_vectors[0].address);
-    u32 data_size = request.in_vectors[0].size;
-    u8* sig_out = Memory::GetPointer(request.io_vectors[0].address);
-
-    EcWii& ec = EcWii::GetInstance();
-    get_ap_sig_and_cert(sig_out, ap_cert_out, m_TitleID, data, data_size, ec.getNgPriv(),
-                        ec.getNgId());
-  }
-  break;
-
+    return Sign(request);
   case IOCTL_ES_GETBOOT2VERSION:
-  {
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETBOOT2VERSION");
-
-    Memory::Write_U32(
-        4, request.io_vectors[0].address);  // as of 26/02/2012, this was latest bootmii version
-  }
-  break;
-
-  // ===============================================================================================
-  // unsupported functions
-  // ===============================================================================================
-  case IOCTL_ES_DIGETTICKETVIEW:  // (Input: none, Output: 216 bytes) bug crediar :D
-    WARN_LOG(IOS_ES, "IOCTL_ES_DIGETTICKETVIEW: this looks really wrong...");
-    break;
+    return GetBoot2Version(request);
 
+  // Unsupported functions
+  case IOCTL_ES_DIGETTICKETVIEW:
+    return DIGetTicketView(request);
   case IOCTL_ES_GETOWNEDTITLECNT:
-    INFO_LOG(IOS_ES, "IOCTL_ES_GETOWNEDTITLECNT");
-    Memory::Write_U32(0, request.io_vectors[0].address);
-    break;
-
+    return GetOwnedTitleCount(request);
   default:
     request.DumpUnknown(GetDeviceName(), LogTypes::IOS);
+    break;
   }
 
   return GetDefaultReply(IPC_SUCCESS);
 }
 
+IPCCommandResult ES::AddTicket(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 3,
+                   "IOCTL_ES_ADDTICKET wrong number of inputs");
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_ADDTICKET");
+  std::vector<u8> ticket(request.in_vectors[0].size);
+  Memory::CopyFromEmu(ticket.data(), request.in_vectors[0].address, request.in_vectors[0].size);
+  DiscIO::AddTicket(ticket);
+
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::AddTitleStart(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 4,
+                   "IOCTL_ES_ADDTITLESTART wrong number of inputs");
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_ADDTITLESTART");
+  std::vector<u8> tmd(request.in_vectors[0].size);
+  Memory::CopyFromEmu(tmd.data(), request.in_vectors[0].address, request.in_vectors[0].size);
+
+  m_addtitle_tmd.SetBytes(tmd);
+  if (!m_addtitle_tmd.IsValid())
+  {
+    ERROR_LOG(IOS_ES, "Invalid TMD while adding title (size = %zd)", tmd.size());
+    return GetDefaultReply(ES_INVALID_TMD);
+  }
+
+  // Write the TMD to title storage.
+  std::string tmd_path =
+      Common::GetTMDFileName(m_addtitle_tmd.GetTitleId(), Common::FROM_SESSION_ROOT);
+  File::CreateFullPath(tmd_path);
+
+  File::IOFile fp(tmd_path, "wb");
+  fp.WriteBytes(tmd.data(), tmd.size());
+
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::AddContentStart(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 2,
+                   "IOCTL_ES_ADDCONTENTSTART wrong number of inputs");
+
+  u64 title_id = Memory::Read_U64(request.in_vectors[0].address);
+  u32 content_id = Memory::Read_U32(request.in_vectors[1].address);
+
+  if (m_addtitle_content_id != 0xFFFFFFFF)
+  {
+    ERROR_LOG(IOS_ES, "Trying to add content when we haven't finished adding "
+                      "another content. Unsupported.");
+    return GetDefaultReply(ES_WRITE_FAILURE);
+  }
+  m_addtitle_content_id = content_id;
+
+  m_addtitle_content_buffer.clear();
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTSTART: title id %016" PRIx64 ", "
+                   "content id %08x",
+           title_id, m_addtitle_content_id);
+
+  if (title_id != m_addtitle_tmd.GetTitleId())
+  {
+    ERROR_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTSTART: title id %016" PRIx64 " != "
+                      "TMD title id %016lx, ignoring",
+              title_id, m_addtitle_tmd.GetTitleId());
+  }
+
+  // We're supposed to return a "content file descriptor" here, which is
+  // passed to further AddContentData / AddContentFinish. But so far there is
+  // no known content installer which performs content addition concurrently.
+  // Instead we just log an error (see above) if this condition is detected.
+  s32 content_fd = 0;
+  return GetDefaultReply(content_fd);
+}
+
+IPCCommandResult ES::AddContentData(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 2,
+                   "IOCTL_ES_ADDCONTENTDATA wrong number of inputs");
+
+  u32 content_fd = Memory::Read_U32(request.in_vectors[0].address);
+  INFO_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTDATA: content fd %08x, "
+                   "size %d",
+           content_fd, request.in_vectors[1].size);
+
+  u8* data_start = Memory::GetPointer(request.in_vectors[1].address);
+  u8* data_end = data_start + request.in_vectors[1].size;
+  m_addtitle_content_buffer.insert(m_addtitle_content_buffer.end(), data_start, data_end);
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::AddContentFinish(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 1,
+                   "IOCTL_ES_ADDCONTENTFINISH wrong number of inputs");
+
+  u32 content_fd = Memory::Read_U32(request.in_vectors[0].address);
+  INFO_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTFINISH: content fd %08x", content_fd);
+
+  // Try to find the title key from a pre-installed ticket.
+  std::vector<u8> ticket = DiscIO::FindSignedTicket(m_addtitle_tmd.GetTitleId());
+  if (ticket.size() == 0)
+  {
+    return GetDefaultReply(ES_NO_TICKET_INSTALLED);
+  }
+
+  mbedtls_aes_context aes_ctx;
+  mbedtls_aes_setkey_dec(&aes_ctx, DiscIO::GetKeyFromTicket(ticket).data(), 128);
+
+  // The IV for title content decryption is the lower two bytes of the
+  // content index, zero extended.
+  TMDReader::Content content_info;
+  if (!m_addtitle_tmd.FindContentById(m_addtitle_content_id, &content_info))
+  {
+    return GetDefaultReply(ES_INVALID_TMD);
+  }
+  u8 iv[16] = {0};
+  iv[0] = (content_info.index >> 8) & 0xFF;
+  iv[1] = content_info.index & 0xFF;
+  std::vector<u8> decrypted_data(m_addtitle_content_buffer.size());
+  mbedtls_aes_crypt_cbc(&aes_ctx, MBEDTLS_AES_DECRYPT, m_addtitle_content_buffer.size(), iv,
+                        m_addtitle_content_buffer.data(), decrypted_data.data());
+
+  std::string path = StringFromFormat(
+      "%s%08x.app",
+      Common::GetTitleContentPath(m_addtitle_tmd.GetTitleId(), Common::FROM_SESSION_ROOT).c_str(),
+      m_addtitle_content_id);
+
+  File::IOFile fp(path, "wb");
+  fp.WriteBytes(decrypted_data.data(), decrypted_data.size());
+
+  m_addtitle_content_id = 0xFFFFFFFF;
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::AddTitleFinish(const IOCtlVRequest& request)
+{
+  INFO_LOG(IOS_ES, "IOCTL_ES_ADDTITLEFINISH");
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::ESGetDeviceID(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1, "IOCTL_ES_GETDEVICEID no io vectors");
+
+  EcWii& ec = EcWii::GetInstance();
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETDEVICEID %08X", ec.getNgId());
+  Memory::Write_U32(ec.getNgId(), request.io_vectors[0].address);
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::GetTitleContentsCount(const IOCtlVRequest& request)
+{
+  _dbg_assert_(IOS_ES, request.in_vectors.size() == 1);
+  _dbg_assert_(IOS_ES, request.io_vectors.size() == 1);
+
+  u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
+
+  const DiscIO::CNANDContentLoader& rNANDContent = AccessContentDevice(TitleID);
+  u16 NumberOfPrivateContent = 0;
+  s32 return_value = IPC_SUCCESS;
+  if (rNANDContent.IsValid())  // Not sure if dolphin will ever fail this check
+  {
+    NumberOfPrivateContent = rNANDContent.GetNumEntries();
+
+    if ((u32)(TitleID >> 32) == 0x00010000)
+      Memory::Write_U32(0, request.io_vectors[0].address);
+    else
+      Memory::Write_U32(NumberOfPrivateContent, request.io_vectors[0].address);
+  }
+  else
+  {
+    return_value = static_cast<s32>(rNANDContent.GetContentSize());
+  }
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLECONTENTSCNT: TitleID: %08x/%08x  content count %i",
+           (u32)(TitleID >> 32), (u32)TitleID,
+           rNANDContent.IsValid() ? NumberOfPrivateContent : (u32)rNANDContent.GetContentSize());
+
+  return GetDefaultReply(return_value);
+}
+
+IPCCommandResult ES::GetTitleContents(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 2,
+                   "IOCTL_ES_GETTITLECONTENTS bad in buffer");
+  _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1,
+                   "IOCTL_ES_GETTITLECONTENTS bad out buffer");
+
+  u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
+
+  const DiscIO::CNANDContentLoader& rNANDContent = AccessContentDevice(TitleID);
+  s32 return_value = IPC_SUCCESS;
+  if (rNANDContent.IsValid())  // Not sure if dolphin will ever fail this check
+  {
+    for (u16 i = 0; i < rNANDContent.GetNumEntries(); i++)
+    {
+      Memory::Write_U32(rNANDContent.GetContentByIndex(i)->m_ContentID,
+                        request.io_vectors[0].address + i * 4);
+      INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLECONTENTS: Index %d: %08x", i,
+               rNANDContent.GetContentByIndex(i)->m_ContentID);
+    }
+  }
+  else
+  {
+    return_value = static_cast<s32>(rNANDContent.GetContentSize());
+    ERROR_LOG(IOS_ES, "IOCTL_ES_GETTITLECONTENTS: Unable to open content %zu",
+              rNANDContent.GetContentSize());
+  }
+
+  return GetDefaultReply(return_value);
+}
+
+IPCCommandResult ES::OpenTitleContent(const IOCtlVRequest& request)
+{
+  _dbg_assert_(IOS_ES, request.in_vectors.size() == 3);
+  _dbg_assert_(IOS_ES, request.io_vectors.size() == 0);
+
+  u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
+  u32 Index = Memory::Read_U32(request.in_vectors[2].address);
+
+  s32 CFD = OpenTitleContent(m_AccessIdentID++, TitleID, Index);
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_OPENTITLECONTENT: TitleID: %08x/%08x  Index %i -> got CFD %x",
+           (u32)(TitleID >> 32), (u32)TitleID, Index, CFD);
+
+  return GetDefaultReply(CFD);
+}
+
+IPCCommandResult ES::OpenContent(const IOCtlVRequest& request)
+{
+  _dbg_assert_(IOS_ES, request.in_vectors.size() == 1);
+  _dbg_assert_(IOS_ES, request.io_vectors.size() == 0);
+  u32 Index = Memory::Read_U32(request.in_vectors[0].address);
+
+  s32 CFD = OpenTitleContent(m_AccessIdentID++, m_TitleID, Index);
+  INFO_LOG(IOS_ES, "IOCTL_ES_OPENCONTENT: Index %i -> got CFD %x", Index, CFD);
+
+  return GetDefaultReply(CFD);
+}
+
+IPCCommandResult ES::ReadContent(const IOCtlVRequest& request)
+{
+  _dbg_assert_(IOS_ES, request.in_vectors.size() == 1);
+  _dbg_assert_(IOS_ES, request.io_vectors.size() == 1);
+
+  u32 CFD = Memory::Read_U32(request.in_vectors[0].address);
+  u32 Size = request.io_vectors[0].size;
+  u32 Addr = request.io_vectors[0].address;
+
+  auto itr = m_ContentAccessMap.find(CFD);
+  if (itr == m_ContentAccessMap.end())
+  {
+    return GetDefaultReply(-1);
+  }
+  SContentAccess& rContent = itr->second;
+
+  u8* pDest = Memory::GetPointer(Addr);
+
+  if (rContent.m_Position + Size > rContent.m_Size)
+  {
+    Size = rContent.m_Size - rContent.m_Position;
+  }
+
+  if (Size > 0)
+  {
+    if (pDest)
+    {
+      const DiscIO::CNANDContentLoader& ContentLoader = AccessContentDevice(rContent.m_TitleID);
+      // ContentLoader should never be invalid; rContent has been created by it.
+      if (ContentLoader.IsValid())
+      {
+        const DiscIO::SNANDContent* pContent = ContentLoader.GetContentByIndex(rContent.m_Index);
+        if (!pContent->m_Data->GetRange(rContent.m_Position, Size, pDest))
+          ERROR_LOG(IOS_ES, "ES: failed to read %u bytes from %u!", Size, rContent.m_Position);
+      }
+
+      rContent.m_Position += Size;
+    }
+    else
+    {
+      PanicAlert("IOCTL_ES_READCONTENT - bad destination");
+    }
+  }
+
+  DEBUG_LOG(IOS_ES,
+            "IOCTL_ES_READCONTENT: CFD %x, Address 0x%x, Size %i -> stream pos %i (Index %i)", CFD,
+            Addr, Size, rContent.m_Position, rContent.m_Index);
+
+  return GetDefaultReply(Size);
+}
+
+IPCCommandResult ES::CloseContent(const IOCtlVRequest& request)
+{
+  _dbg_assert_(IOS_ES, request.in_vectors.size() == 1);
+  _dbg_assert_(IOS_ES, request.io_vectors.size() == 0);
+
+  u32 CFD = Memory::Read_U32(request.in_vectors[0].address);
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_CLOSECONTENT: CFD %x", CFD);
+
+  auto itr = m_ContentAccessMap.find(CFD);
+  if (itr == m_ContentAccessMap.end())
+  {
+    return GetDefaultReply(-1);
+  }
+
+  const DiscIO::CNANDContentLoader& ContentLoader = AccessContentDevice(itr->second.m_TitleID);
+  // ContentLoader should never be invalid; we shouldn't be here if ES_OPENCONTENT failed before.
+  if (ContentLoader.IsValid())
+  {
+    const DiscIO::SNANDContent* pContent = ContentLoader.GetContentByIndex(itr->second.m_Index);
+    pContent->m_Data->Close();
+  }
+
+  m_ContentAccessMap.erase(itr);
+
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::SeekContent(const IOCtlVRequest& request)
+{
+  _dbg_assert_(IOS_ES, request.in_vectors.size() == 3);
+  _dbg_assert_(IOS_ES, request.io_vectors.size() == 0);
+
+  u32 CFD = Memory::Read_U32(request.in_vectors[0].address);
+  u32 Addr = Memory::Read_U32(request.in_vectors[1].address);
+  u32 Mode = Memory::Read_U32(request.in_vectors[2].address);
+
+  auto itr = m_ContentAccessMap.find(CFD);
+  if (itr == m_ContentAccessMap.end())
+  {
+    return GetDefaultReply(-1);
+  }
+  SContentAccess& rContent = itr->second;
+
+  switch (Mode)
+  {
+  case 0:  // SET
+    rContent.m_Position = Addr;
+    break;
+
+  case 1:  // CUR
+    rContent.m_Position += Addr;
+    break;
+
+  case 2:  // END
+    rContent.m_Position = rContent.m_Size + Addr;
+    break;
+  }
+
+  DEBUG_LOG(IOS_ES, "IOCTL_ES_SEEKCONTENT: CFD %x, Address 0x%x, Mode %i -> Pos %i", CFD, Addr,
+            Mode, rContent.m_Position);
+
+  return GetDefaultReply(rContent.m_Position);
+}
+
+IPCCommandResult ES::GetTitleDirectory(const IOCtlVRequest& request)
+{
+  _dbg_assert_(IOS_ES, request.in_vectors.size() == 1);
+  _dbg_assert_(IOS_ES, request.io_vectors.size() == 1);
+
+  u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
+
+  char* Path = (char*)Memory::GetPointer(request.io_vectors[0].address);
+  sprintf(Path, "/title/%08x/%08x/data", (u32)(TitleID >> 32), (u32)TitleID);
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLEDIR: %s", Path);
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::GetTitleID(const IOCtlVRequest& request)
+{
+  _dbg_assert_(IOS_ES, request.in_vectors.size() == 0);
+  _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1, "IOCTL_ES_GETTITLEID no out buffer");
+
+  Memory::Write_U64(m_TitleID, request.io_vectors[0].address);
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLEID: %08x/%08x", (u32)(m_TitleID >> 32), (u32)m_TitleID);
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::SetUID(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 1, "IOCTL_ES_SETUID no in buffer");
+  _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 0,
+                   "IOCTL_ES_SETUID has a payload, it shouldn't");
+
+  // TODO: fs permissions based on this
+  u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
+  INFO_LOG(IOS_ES, "IOCTL_ES_SETUID titleID: %08x/%08x", (u32)(TitleID >> 32), (u32)TitleID);
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::GetTitleCount(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 0, "IOCTL_ES_GETTITLECNT has an in buffer");
+  _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1,
+                   "IOCTL_ES_GETTITLECNT has no out buffer");
+  _dbg_assert_msg_(IOS_ES, request.io_vectors[0].size == 4,
+                   "IOCTL_ES_GETTITLECNT payload[0].size != 4");
+
+  Memory::Write_U32((u32)m_TitleIDs.size(), request.io_vectors[0].address);
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLECNT: Number of Titles %zu", m_TitleIDs.size());
+
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::GetTitles(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 1, "IOCTL_ES_GETTITLES has an in buffer");
+  _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1, "IOCTL_ES_GETTITLES has no out buffer");
+
+  u32 MaxCount = Memory::Read_U32(request.in_vectors[0].address);
+  u32 Count = 0;
+  for (int i = 0; i < (int)m_TitleIDs.size(); i++)
+  {
+    Memory::Write_U64(m_TitleIDs[i], request.io_vectors[0].address + i * 8);
+    INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLES: %08x/%08x", (u32)(m_TitleIDs[i] >> 32),
+             (u32)m_TitleIDs[i]);
+    Count++;
+    if (Count >= MaxCount)
+      break;
+  }
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLES: Number of titles returned %i", Count);
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::GetViewCount(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 1, "IOCTL_ES_GETVIEWCNT no in buffer");
+  _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1, "IOCTL_ES_GETVIEWCNT no out buffer");
+
+  u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
+
+  u32 retVal = 0;
+  const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
+  u32 ViewCount =
+      static_cast<u32>(Loader.GetTicket().size()) / DiscIO::CNANDContentLoader::TICKET_SIZE;
+
+  if (!ViewCount)
+  {
+    std::string TicketFilename = Common::GetTicketFileName(TitleID, Common::FROM_SESSION_ROOT);
+    if (File::Exists(TicketFilename))
+    {
+      u32 FileSize = (u32)File::GetSize(TicketFilename);
+      _dbg_assert_msg_(IOS_ES, (FileSize % DiscIO::CNANDContentLoader::TICKET_SIZE) == 0,
+                       "IOCTL_ES_GETVIEWCNT ticket file size seems to be wrong");
+
+      ViewCount = FileSize / DiscIO::CNANDContentLoader::TICKET_SIZE;
+      _dbg_assert_msg_(IOS_ES, (ViewCount > 0) && (ViewCount <= 4),
+                       "IOCTL_ES_GETVIEWCNT ticket count seems to be wrong");
+    }
+    else if (TitleID >> 32 == 0x00000001)
+    {
+      // Fake a ticket view to make IOS reload work.
+      ViewCount = 1;
+    }
+    else
+    {
+      ViewCount = 0;
+      if (TitleID == TITLEID_SYSMENU)
+      {
+        PanicAlertT("There must be a ticket for 00000001/00000002. Your NAND dump is probably "
+                    "incomplete.");
+      }
+      // retVal = ES_NO_TICKET_INSTALLED;
+    }
+  }
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETVIEWCNT for titleID: %08x/%08x (View Count = %i)",
+           (u32)(TitleID >> 32), (u32)TitleID, ViewCount);
+
+  Memory::Write_U32(ViewCount, request.io_vectors[0].address);
+  return GetDefaultReply(retVal);
+}
+
+IPCCommandResult ES::GetViews(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 2, "IOCTL_ES_GETVIEWS no in buffer");
+  _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1, "IOCTL_ES_GETVIEWS no out buffer");
+
+  u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
+  u32 maxViews = Memory::Read_U32(request.in_vectors[1].address);
+  u32 retVal = 0;
+
+  const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
+
+  const std::vector<u8>& ticket = Loader.GetTicket();
+
+  if (ticket.empty())
+  {
+    std::string TicketFilename = Common::GetTicketFileName(TitleID, Common::FROM_SESSION_ROOT);
+    if (File::Exists(TicketFilename))
+    {
+      File::IOFile pFile(TicketFilename, "rb");
+      if (pFile)
+      {
+        u8 FileTicket[DiscIO::CNANDContentLoader::TICKET_SIZE];
+        for (unsigned int View = 0;
+             View != maxViews &&
+             pFile.ReadBytes(FileTicket, DiscIO::CNANDContentLoader::TICKET_SIZE);
+             ++View)
+        {
+          Memory::Write_U32(View, request.io_vectors[0].address + View * 0xD8);
+          Memory::CopyToEmu(request.io_vectors[0].address + 4 + View * 0xD8, FileTicket + 0x1D0,
+                            212);
+        }
+      }
+    }
+    else if (TitleID >> 32 == 0x00000001)
+    {
+      // For IOS titles, the ticket view isn't normally parsed by either the
+      // SDK or libogc, just passed to LaunchTitle, so this
+      // shouldn't matter at all.  Just fill out some fields just
+      // to be on the safe side.
+      u32 Address = request.io_vectors[0].address;
+      Memory::Memset(Address, 0, 0xD8);
+      Memory::Write_U64(TitleID, Address + 4 + (0x1dc - 0x1d0));  // title ID
+      Memory::Write_U16(0xffff, Address + 4 + (0x1e4 - 0x1d0));   // unnnown
+      Memory::Write_U32(0xff00, Address + 4 + (0x1ec - 0x1d0));   // access mask
+      Memory::Memset(Address + 4 + (0x222 - 0x1d0), 0xff, 0x20);  // content permissions
+    }
+    else
+    {
+      // retVal = ES_NO_TICKET_INSTALLED;
+      PanicAlertT("IOCTL_ES_GETVIEWS: Tried to get data from an unknown ticket: %08x/%08x",
+                  (u32)(TitleID >> 32), (u32)TitleID);
+    }
+  }
+  else
+  {
+    u32 view_count =
+        static_cast<u32>(Loader.GetTicket().size()) / DiscIO::CNANDContentLoader::TICKET_SIZE;
+    for (unsigned int view = 0; view != maxViews && view < view_count; ++view)
+    {
+      Memory::Write_U32(view, request.io_vectors[0].address + view * 0xD8);
+      Memory::CopyToEmu(request.io_vectors[0].address + 4 + view * 0xD8,
+                        &ticket[0x1D0 + (view * DiscIO::CNANDContentLoader::TICKET_SIZE)], 212);
+    }
+  }
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETVIEWS for titleID: %08x/%08x (MaxViews = %i)", (u32)(TitleID >> 32),
+           (u32)TitleID, maxViews);
+
+  return GetDefaultReply(retVal);
+}
+
+IPCCommandResult ES::GetTMDViewCount(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 1, "IOCTL_ES_GETTMDVIEWCNT no in buffer");
+  _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1, "IOCTL_ES_GETTMDVIEWCNT no out buffer");
+
+  u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
+
+  const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
+
+  u32 TMDViewCnt = 0;
+  if (Loader.IsValid())
+  {
+    TMDViewCnt += DiscIO::CNANDContentLoader::TMD_VIEW_SIZE;
+    TMDViewCnt += 2;                                               // title version
+    TMDViewCnt += 2;                                               // num entries
+    TMDViewCnt += (u32)Loader.GetContentSize() * (4 + 2 + 2 + 8);  // content id, index, type, size
+  }
+  Memory::Write_U32(TMDViewCnt, request.io_vectors[0].address);
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETTMDVIEWCNT: title: %08x/%08x (view size %i)", (u32)(TitleID >> 32),
+           (u32)TitleID, TMDViewCnt);
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::GetTMDViews(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 2, "IOCTL_ES_GETTMDVIEWCNT no in buffer");
+  _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1, "IOCTL_ES_GETTMDVIEWCNT no out buffer");
+
+  u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
+  u32 MaxCount = Memory::Read_U32(request.in_vectors[1].address);
+
+  const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETTMDVIEWCNT: title: %08x/%08x   buffer size: %i",
+           (u32)(TitleID >> 32), (u32)TitleID, MaxCount);
+
+  if (Loader.IsValid())
+  {
+    u32 Address = request.io_vectors[0].address;
+
+    Memory::CopyToEmu(Address, Loader.GetTMDView(), DiscIO::CNANDContentLoader::TMD_VIEW_SIZE);
+    Address += DiscIO::CNANDContentLoader::TMD_VIEW_SIZE;
+
+    Memory::Write_U16(Loader.GetTitleVersion(), Address);
+    Address += 2;
+    Memory::Write_U16(Loader.GetNumEntries(), Address);
+    Address += 2;
+
+    const std::vector<DiscIO::SNANDContent>& rContent = Loader.GetContent();
+    for (size_t i = 0; i < Loader.GetContentSize(); i++)
+    {
+      Memory::Write_U32(rContent[i].m_ContentID, Address);
+      Address += 4;
+      Memory::Write_U16(rContent[i].m_Index, Address);
+      Address += 2;
+      Memory::Write_U16(rContent[i].m_Type, Address);
+      Address += 2;
+      Memory::Write_U64(rContent[i].m_Size, Address);
+      Address += 8;
+    }
+
+    _dbg_assert_(IOS_ES, (Address - request.io_vectors[0].address) == request.io_vectors[0].size);
+  }
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETTMDVIEWS: title: %08x/%08x (buffer size: %i)", (u32)(TitleID >> 32),
+           (u32)TitleID, MaxCount);
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::GetConsumption(const IOCtlVRequest& request)
+{
+  // This is at least what crediar's ES module does
+  Memory::Write_U32(0, request.io_vectors[1].address);
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETCONSUMPTION");
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::DeleteTicket(const IOCtlVRequest& request)
+{
+  u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
+  INFO_LOG(IOS_ES, "IOCTL_ES_DELETETICKET: title: %08x/%08x", (u32)(TitleID >> 32), (u32)TitleID);
+
+  // Presumably return -1017 when delete fails
+  if (!File::Delete(Common::GetTicketFileName(TitleID, Common::FROM_SESSION_ROOT)))
+    return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
+
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::DeleteTitleContent(const IOCtlVRequest& request)
+{
+  u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
+  INFO_LOG(IOS_ES, "IOCTL_ES_DELETETITLECONTENT: title: %08x/%08x", (u32)(TitleID >> 32),
+           (u32)TitleID);
+
+  // Presumably return -1017 when title not installed TODO verify
+  if (!DiscIO::CNANDContentManager::Access().RemoveTitle(TitleID, Common::FROM_SESSION_ROOT))
+    return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
+
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::GetStoredTMDSize(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 1,
+                   "IOCTL_ES_GETSTOREDTMDSIZE no in buffer");
+  // _dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1,
+  //                  "IOCTL_ES_ES_GETSTOREDTMDSIZE no out buffer");
+
+  u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
+  const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
+
+  _dbg_assert_(IOS_ES, Loader.IsValid());
+  u32 TMDCnt = 0;
+  if (Loader.IsValid())
+  {
+    TMDCnt += DiscIO::CNANDContentLoader::TMD_HEADER_SIZE;
+    TMDCnt += (u32)Loader.GetContentSize() * DiscIO::CNANDContentLoader::CONTENT_HEADER_SIZE;
+  }
+
+  if (request.io_vectors.size())
+    Memory::Write_U32(TMDCnt, request.io_vectors[0].address);
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETSTOREDTMDSIZE: title: %08x/%08x (view size %i)",
+           (u32)(TitleID >> 32), (u32)TitleID, TMDCnt);
+
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::GetStoredTMD(const IOCtlVRequest& request)
+{
+  _dbg_assert_msg_(IOS_ES, request.in_vectors.size() > 0, "IOCTL_ES_GETSTOREDTMD no in buffer");
+  // requires 1 inbuffer and no outbuffer, presumably outbuffer required when second inbuffer is
+  // used for maxcount (allocated mem?)
+  // called with 1 inbuffer after deleting a titleid
+  //_dbg_assert_msg_(IOS_ES, request.io_vectors.size() == 1,
+  //                 "IOCTL_ES_GETSTOREDTMD no out buffer");
+
+  u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
+  u32 MaxCount = 0;
+  if (request.in_vectors.size() > 1)
+  {
+    // TODO: actually use this param in when writing to the outbuffer :/
+    MaxCount = Memory::Read_U32(request.in_vectors[1].address);
+  }
+  const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETSTOREDTMD: title: %08x/%08x   buffer size: %i",
+           (u32)(TitleID >> 32), (u32)TitleID, MaxCount);
+
+  if (Loader.IsValid() && request.io_vectors.size())
+  {
+    u32 Address = request.io_vectors[0].address;
+
+    Memory::CopyToEmu(Address, Loader.GetTMDHeader(), DiscIO::CNANDContentLoader::TMD_HEADER_SIZE);
+    Address += DiscIO::CNANDContentLoader::TMD_HEADER_SIZE;
+
+    const std::vector<DiscIO::SNANDContent>& rContent = Loader.GetContent();
+    for (size_t i = 0; i < Loader.GetContentSize(); i++)
+    {
+      Memory::CopyToEmu(Address, rContent[i].m_Header,
+                        DiscIO::CNANDContentLoader::CONTENT_HEADER_SIZE);
+      Address += DiscIO::CNANDContentLoader::CONTENT_HEADER_SIZE;
+    }
+
+    _dbg_assert_(IOS_ES, (Address - request.io_vectors[0].address) == request.io_vectors[0].size);
+  }
+
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETSTOREDTMD: title: %08x/%08x (buffer size: %i)",
+           (u32)(TitleID >> 32), (u32)TitleID, MaxCount);
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::Encrypt(const IOCtlVRequest& request)
+{
+  u32 keyIndex = Memory::Read_U32(request.in_vectors[0].address);
+  u8* IV = Memory::GetPointer(request.in_vectors[1].address);
+  u8* source = Memory::GetPointer(request.in_vectors[2].address);
+  u32 size = request.in_vectors[2].size;
+  u8* newIV = Memory::GetPointer(request.io_vectors[0].address);
+  u8* destination = Memory::GetPointer(request.io_vectors[1].address);
+
+  mbedtls_aes_context AES_ctx;
+  mbedtls_aes_setkey_enc(&AES_ctx, s_key_table[keyIndex], 128);
+  memcpy(newIV, IV, 16);
+  mbedtls_aes_crypt_cbc(&AES_ctx, MBEDTLS_AES_ENCRYPT, size, newIV, source, destination);
+
+  _dbg_assert_msg_(IOS_ES, keyIndex == 6,
+                   "IOCTL_ES_ENCRYPT: Key type is not SD, data will be crap");
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::Decrypt(const IOCtlVRequest& request)
+{
+  u32 keyIndex = Memory::Read_U32(request.in_vectors[0].address);
+  u8* IV = Memory::GetPointer(request.in_vectors[1].address);
+  u8* source = Memory::GetPointer(request.in_vectors[2].address);
+  u32 size = request.in_vectors[2].size;
+  u8* newIV = Memory::GetPointer(request.io_vectors[0].address);
+  u8* destination = Memory::GetPointer(request.io_vectors[1].address);
+
+  DecryptContent(keyIndex, IV, source, size, newIV, destination);
+
+  _dbg_assert_msg_(IOS_ES, keyIndex == 6,
+                   "IOCTL_ES_DECRYPT: Key type is not SD, data will be crap");
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::Launch(const IOCtlVRequest& request)
+{
+  _dbg_assert_(IOS_ES, request.in_vectors.size() == 2);
+  bool bSuccess = false;
+  bool bReset = false;
+
+  u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
+  u32 view = Memory::Read_U32(request.in_vectors[1].address);
+  u64 ticketid = Memory::Read_U64(request.in_vectors[1].address + 4);
+  u32 devicetype = Memory::Read_U32(request.in_vectors[1].address + 12);
+  u64 titleid = Memory::Read_U64(request.in_vectors[1].address + 16);
+  u16 access = Memory::Read_U16(request.in_vectors[1].address + 24);
+
+  // ES_LAUNCH should probably reset thw whole state, which at least means closing all open files.
+  // leaving them open through ES_LAUNCH may cause hangs and other funky behavior
+  // (supposedly when trying to re-open those files).
+  DiscIO::CNANDContentManager::Access().ClearCache();
+
+  u64 ios_to_load = 0;
+  std::string tContentFile;
+  if ((u32)(TitleID >> 32) == 0x00000001 && TitleID != TITLEID_SYSMENU)
+  {
+    ios_to_load = TitleID;
+    bSuccess = true;
+  }
+  else
+  {
+    const DiscIO::CNANDContentLoader& ContentLoader = AccessContentDevice(TitleID);
+    if (ContentLoader.IsValid())
+    {
+      ios_to_load = 0x0000000100000000ULL | ContentLoader.GetIosVersion();
+
+      u32 bootInd = ContentLoader.GetBootIndex();
+      const DiscIO::SNANDContent* pContent = ContentLoader.GetContentByIndex(bootInd);
+      if (pContent)
+      {
+        tContentFile = Common::GetTitleContentPath(TitleID, Common::FROM_SESSION_ROOT);
+        std::unique_ptr<CDolLoader> pDolLoader =
+            std::make_unique<CDolLoader>(pContent->m_Data->Get());
+
+        if (pDolLoader->IsValid())
+        {
+          pDolLoader->Load();
+          // TODO: Check why sysmenu does not load the DOL correctly
+          // NAND titles start with address translation off at 0x3400 (via the PPC bootstub)
+          //
+          // The state of other CPU registers (like the BAT registers) doesn't matter much
+          // because the realmode code at 0x3400 initializes everything itself anyway.
+          MSR = 0;
+          PC = 0x3400;
+          bSuccess = true;
+        }
+        else
+        {
+          PanicAlertT("IOCTL_ES_LAUNCH: The DOL file is invalid!");
+        }
+      }
+    }
+  }
+
+  if (!bSuccess)
+  {
+    PanicAlertT(
+        "IOCTL_ES_LAUNCH: Game tried to reload a title that is not available in your NAND dump\n"
+        "TitleID %016" PRIx64 ".\n Dolphin will likely hang now.",
+        TitleID);
+  }
+  else
+  {
+    bool* wiiMoteConnected = new bool[MAX_BBMOTES];
+    if (!SConfig::GetInstance().m_bt_passthrough_enabled)
+    {
+      BluetoothEmu* s_Usb = GetUsbPointer();
+      for (unsigned int i = 0; i < MAX_BBMOTES; i++)
+        wiiMoteConnected[i] = s_Usb->m_WiiMotes[i].IsConnected();
+    }
+
+    Reset(true);
+    Reinit();
+    SetupMemory(ios_to_load);
+    bReset = true;
+
+    if (!SConfig::GetInstance().m_bt_passthrough_enabled)
+    {
+      BluetoothEmu* s_Usb = GetUsbPointer();
+      for (unsigned int i = 0; i < MAX_BBMOTES; i++)
+      {
+        if (wiiMoteConnected[i])
+        {
+          s_Usb->m_WiiMotes[i].Activate(false);
+          s_Usb->m_WiiMotes[i].Activate(true);
+        }
+        else
+        {
+          s_Usb->m_WiiMotes[i].Activate(false);
+        }
+      }
+    }
+    delete[] wiiMoteConnected;
+    SetDefaultContentFile(tContentFile);
+  }
+
+  // Note: If we just reset the PPC, don't write anything to the command buffer. This
+  // could clobber the DOL we just loaded.
+
+  ERROR_LOG(IOS_ES, "IOCTL_ES_LAUNCH %016" PRIx64 " %08x %016" PRIx64 " %08x %016" PRIx64 " %04x",
+            TitleID, view, ticketid, devicetype, titleid, access);
+  //                     IOCTL_ES_LAUNCH 0001000248414341 00000001 0001c0fef3df2cfa 00000000
+  //                     0001000248414341 ffff
+
+  // This is necessary because Reset(true) above deleted this object.  Ew.
+
+  if (!bReset)
+  {
+    // The command type is overwritten with the reply type.
+    Memory::Write_U32(IPC_REPLY, request.address);
+    // IOS also writes back the command that was responded to in the FD field.
+    Memory::Write_U32(IPC_CMD_IOCTLV, request.address + 8);
+  }
+
+  // Generate a "reply" to the IPC command.  ES_LAUNCH is unique because it
+  // involves restarting IOS; IOS generates two acknowledgements in a row.
+  EnqueueCommandAcknowledgement(request.address, 0);
+  return GetNoReply();
+}
+
+IPCCommandResult ES::CheckKoreaRegion(const IOCtlVRequest& request)
+{
+  // note by DacoTaco : name is unknown, I just tried to name it SOMETHING.
+  // IOS70 has this to let system menu 4.2 check if the console is region changed. it returns
+  // -1017
+  // if the IOS didn't find the Korean keys and 0 if it does. 0 leads to a error 003
+  INFO_LOG(IOS_ES, "IOCTL_ES_CHECKKOREAREGION: Title checked for Korean keys.");
+  return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
+}
+
+IPCCommandResult ES::GetDeviceCertificate(const IOCtlVRequest& request)
+{
+  // (Input: none, Output: 384 bytes)
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETDEVICECERT");
+  _dbg_assert_(IOS_ES, request.io_vectors.size() == 1);
+  u8* destination = Memory::GetPointer(request.io_vectors[0].address);
+
+  EcWii& ec = EcWii::GetInstance();
+  get_ng_cert(destination, ec.getNgId(), ec.getNgKeyId(), ec.getNgPriv(), ec.getNgSig());
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::Sign(const IOCtlVRequest& request)
+{
+  INFO_LOG(IOS_ES, "IOCTL_ES_SIGN");
+  u8* ap_cert_out = Memory::GetPointer(request.io_vectors[1].address);
+  u8* data = Memory::GetPointer(request.in_vectors[0].address);
+  u32 data_size = request.in_vectors[0].size;
+  u8* sig_out = Memory::GetPointer(request.io_vectors[0].address);
+
+  EcWii& ec = EcWii::GetInstance();
+  get_ap_sig_and_cert(sig_out, ap_cert_out, m_TitleID, data, data_size, ec.getNgPriv(),
+                      ec.getNgId());
+
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::GetBoot2Version(const IOCtlVRequest& request)
+{
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETBOOT2VERSION");
+
+  // as of 26/02/2012, this was latest bootmii version
+  Memory::Write_U32(4, request.io_vectors[0].address);
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::DIGetTicketView(const IOCtlVRequest& request)
+{
+  // (Input: none, Output: 216 bytes) bug crediar :D
+  WARN_LOG(IOS_ES, "IOCTL_ES_DIGETTICKETVIEW: this looks really wrong...");
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
+IPCCommandResult ES::GetOwnedTitleCount(const IOCtlVRequest& request)
+{
+  INFO_LOG(IOS_ES, "IOCTL_ES_GETOWNEDTITLECNT");
+  Memory::Write_U32(0, request.io_vectors[0].address);
+  return GetDefaultReply(IPC_SUCCESS);
+}
+
 const DiscIO::CNANDContentLoader& ES::AccessContentDevice(u64 title_id)
 {
   // for WADs, the passed title id and the stored title id match; along with m_ContentFile being set
diff --git a/Source/Core/Core/IOS/ES/ES.h b/Source/Core/Core/IOS/ES/ES.h
index cf9642edd4..83acd77865 100644
--- a/Source/Core/Core/IOS/ES/ES.h
+++ b/Source/Core/Core/IOS/ES/ES.h
@@ -151,6 +151,44 @@ private:
     u8 padding[0x3c];
   };
 
+  IPCCommandResult AddTicket(const IOCtlVRequest& request);
+  IPCCommandResult AddTitleStart(const IOCtlVRequest& request);
+  IPCCommandResult AddContentStart(const IOCtlVRequest& request);
+  IPCCommandResult AddContentData(const IOCtlVRequest& request);
+  IPCCommandResult AddContentFinish(const IOCtlVRequest& request);
+  IPCCommandResult AddTitleFinish(const IOCtlVRequest& request);
+  IPCCommandResult ESGetDeviceID(const IOCtlVRequest& request);
+  IPCCommandResult GetTitleContentsCount(const IOCtlVRequest& request);
+  IPCCommandResult GetTitleContents(const IOCtlVRequest& request);
+  IPCCommandResult OpenTitleContent(const IOCtlVRequest& request);
+  IPCCommandResult OpenContent(const IOCtlVRequest& request);
+  IPCCommandResult ReadContent(const IOCtlVRequest& request);
+  IPCCommandResult CloseContent(const IOCtlVRequest& request);
+  IPCCommandResult SeekContent(const IOCtlVRequest& request);
+  IPCCommandResult GetTitleDirectory(const IOCtlVRequest& request);
+  IPCCommandResult GetTitleID(const IOCtlVRequest& request);
+  IPCCommandResult SetUID(const IOCtlVRequest& request);
+  IPCCommandResult GetTitleCount(const IOCtlVRequest& request);
+  IPCCommandResult GetTitles(const IOCtlVRequest& request);
+  IPCCommandResult GetViewCount(const IOCtlVRequest& request);
+  IPCCommandResult GetViews(const IOCtlVRequest& request);
+  IPCCommandResult GetTMDViewCount(const IOCtlVRequest& request);
+  IPCCommandResult GetTMDViews(const IOCtlVRequest& request);
+  IPCCommandResult GetConsumption(const IOCtlVRequest& request);
+  IPCCommandResult DeleteTicket(const IOCtlVRequest& request);
+  IPCCommandResult DeleteTitleContent(const IOCtlVRequest& request);
+  IPCCommandResult GetStoredTMDSize(const IOCtlVRequest& request);
+  IPCCommandResult GetStoredTMD(const IOCtlVRequest& request);
+  IPCCommandResult Encrypt(const IOCtlVRequest& request);
+  IPCCommandResult Decrypt(const IOCtlVRequest& request);
+  IPCCommandResult Launch(const IOCtlVRequest& request);
+  IPCCommandResult CheckKoreaRegion(const IOCtlVRequest& request);
+  IPCCommandResult GetDeviceCertificate(const IOCtlVRequest& request);
+  IPCCommandResult Sign(const IOCtlVRequest& request);
+  IPCCommandResult GetBoot2Version(const IOCtlVRequest& request);
+  IPCCommandResult DIGetTicketView(const IOCtlVRequest& request);
+  IPCCommandResult GetOwnedTitleCount(const IOCtlVRequest& request);
+
   const DiscIO::CNANDContentLoader& AccessContentDevice(u64 title_id);
   u32 OpenTitleContent(u32 CFD, u64 TitleID, u16 Index);