diff --git a/Core/FileSystems/ISOFileSystem.cpp b/Core/FileSystems/ISOFileSystem.cpp index d168a54cbb..20994af8b4 100644 --- a/Core/FileSystems/ISOFileSystem.cpp +++ b/Core/FileSystems/ISOFileSystem.cpp @@ -18,8 +18,10 @@ #include #include #include +#include #include "Common/CommonTypes.h" +#include "Common/File/FileUtil.h" // for localtime_r on Windows #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" #include "Common/StringUtils.h" @@ -138,6 +140,39 @@ struct VolDescriptor { char zeroos[653]; }; +// From http://howardhinnant.github.io/date_algorithms.html - same one sceRtc.cpp uses. Beats +// timegm(), which isn't portable, and mktime(), which would drag the host's timezone in. +static s64 DaysFromCivil(s64 y, u32 m, u32 d) { + y -= m <= 2; + const s64 era = (y >= 0 ? y : y - 399) / 400; + const u32 yoe = (u32)(y - era * 400); // [0, 399] + const u32 doy = (153 * (m > 2 ? m - 3 : m + 9) + 2) / 5 + d - 1; // [0, 365] + const u32 doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096] + return era * 146097 + (s64)doe - 719468; +} + +// The 7-byte date in a directory record, as Unix UTC seconds. Returns 0 if there isn't a real +// date in there - plenty of ISOs leave it zeroed, and "1900-00-00" isn't worth propagating. +static s64 UnixTimeFromDirectoryEntry(const DirectoryEntry &dir) { + if (dir.month < 1 || dir.month > 12 || dir.day < 1 || dir.day > 31) { + return 0; + } + // years is an offset from 1900, and offsetFromGMT is a signed count of 15-minute steps. + const s64 days = DaysFromCivil(1900 + (s64)dir.years, dir.month, dir.day); + return days * 86400 + dir.hour * 3600 + dir.minute * 60 + dir.second - (s64)(s8)dir.offsetFromGMT * 15 * 60; +} + +// ISO9660 has one timestamp per file, so it goes into all three of the PSP's. Local time, to +// match what the other file systems report. +static void FillFileTimes(PSPFileInfo *info, s64 unixTime) { + if (unixTime == 0) { + return; + } + const time_t t = (time_t)unixTime; + localtime_r(&t, &info->mtime); + info->atime = info->ctime = info->mtime; +} + // Removes version numbers from filenames. static std::string_view CleanISOFileName(std::string_view name) { auto pos = name.find(';'); @@ -258,6 +293,7 @@ void ISOFileSystem::ReadDirectory(TreeEntry *root) const { entry->startsector = dir.firstDataSector; entry->dirsize = dir.dataLength; entry->valid = isFile; // Can pre-mark as valid if file, as we don't recurse into those. + entry->recordTime = UnixTimeFromDirectoryEntry(dir); VERBOSE_LOG(Log::FileSystem, "%s: %s %08x %08x %d", entry->isDirectory ? "D" : "F", entry->name.c_str(), (u32)dir.firstDataSector, entry->startingPosition, entry->startingPosition); // Round down to avoid any false reports. @@ -662,6 +698,7 @@ PSPFileInfo ISOFileSystem::GetFileInfo(std::string filename) { x.type = entry->isDirectory ? FILETYPE_DIRECTORY : FILETYPE_NORMAL; x.isOnSectorSystem = true; x.startSector = entry->startingPosition / 2048; + FillFileTimes(&x, entry->recordTime); } return x; } @@ -679,6 +716,7 @@ PSPFileInfo ISOFileSystem::GetFileInfoByHandle(u32 handle) { x.type = entry->isDirectory ? FILETYPE_DIRECTORY : FILETYPE_NORMAL; x.isOnSectorSystem = true; x.startSector = entry->startingPosition / 2048; + FillFileTimes(&x, entry->recordTime); } return x; } @@ -714,6 +752,7 @@ std::vector ISOFileSystem::GetDirListing(std::string_view path, boo x.type = e->isDirectory ? FILETYPE_DIRECTORY : FILETYPE_NORMAL; x.isOnSectorSystem = true; x.startSector = e->startingPosition/2048; + FillFileTimes(&x, e->recordTime); x.sectorSize = sectorSize; x.numSectors = (u32)((e->size + sectorSize - 1) / sectorSize); myVector.push_back(x); diff --git a/Core/FileSystems/ISOFileSystem.h b/Core/FileSystems/ISOFileSystem.h index c59c6d884a..66aa516fa4 100644 --- a/Core/FileSystems/ISOFileSystem.h +++ b/Core/FileSystems/ISOFileSystem.h @@ -72,6 +72,10 @@ private: u32 startingPosition = 0; s64 size = 0; bool isDirectory = false; + // The date/time from the ISO9660 directory record, as Unix UTC seconds (0 if there + // wasn't a usable one). ISO9660 only has the one timestamp per file, so it gets + // reported as all three of atime/ctime/mtime. + s64 recordTime = 0; u32 startsector = 0; u32 dirsize = 0; diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index 131859d658..c0cf887409 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -876,12 +876,20 @@ static std::string VersionFromUpdaterTitle(std::string_view title) { return std::string(title); } -static std::string VersionFromSFO(const std::vector &sfo) { +static std::string TitleFromSFO(const std::vector &sfo) { ParamSFOData paramSFO; if (sfo.empty() || !paramSFO.ReadSFO(sfo)) { return std::string(); } - return VersionFromUpdaterTitle(paramSFO.GetValueString("TITLE")); + return paramSFO.GetValueString("TITLE"); +} + +static std::string VersionFromSFO(const std::vector &sfo) { + const std::string title = TitleFromSFO(sfo); + if (title.empty()) { + return std::string(); + } + return VersionFromUpdaterTitle(title); } // Opens filename as a disc image, if it is one. Returns null otherwise, which is the normal @@ -919,6 +927,18 @@ static bool ReadWholeFile(IFileSystem *fs, const char *path, std::vector *ou return true; } +// Just the first few bytes, for a magic check. ReadWholeFile isn't an option when the file we're +// sniffing can be a hundred megabytes. +static bool ReadFileMagic(IFileSystem *fs, const std::string &path, u8 *out, size_t size) { + const int handle = fs->OpenFile(path, FILEACCESS_READ); + if (handle < 0) { + return false; + } + const size_t read = fs->ReadFile(handle, out, size); + fs->CloseFile(handle); + return read == size; +} + // Pulls the archive out of whatever this is - see the header for the shapes we accept. static bool ReadUpdaterPSAR(const Path &filename, std::vector *psar, std::string *sfoVersion, std::string *error) { FileLoader *loader = ConstructFileLoader(filename); @@ -990,6 +1010,58 @@ static bool ReadUpdaterPSAR(const Path &filename, std::vector *psar, std::st return false; } +std::string BundledUpdateInfo::Describe() const { + if (!present) { + return std::string(); + } + std::string desc = version.empty() ? "?" : version; + if (mtime != 0) { + const time_t t = (time_t)mtime; + tm local{}; + localtime_r(&t, &local); + desc += StringFromFormat(" (%04d-%02d-%02d)", local.tm_year + 1900, local.tm_mon + 1, local.tm_mday); + } + return desc; +} + +bool ReadBundledUpdateInfo(IFileSystem *fs, std::string_view pathPrefix, BundledUpdateInfo *info) { + *info = BundledUpdateInfo{}; + + const std::string psarPath = std::string(pathPrefix) + UPDATE_PSAR_SUFFIX; + const PSPFileInfo psarFileInfo = fs->GetFileInfo(psarPath); + if (!psarFileInfo.exists || psarFileInfo.size == 0) { + return false; + } + + // Check that the file is actually an updater. + u8 magic[4]{}; + if (!ReadFileMagic(fs, psarPath, magic, sizeof(magic)) || ReadU32(magic) != PSAR_MAGIC) { + DEBUG_LOG(Log::Loader, "Disc has a %lld byte %s, but it isn't a PSAR - ignoring it", + (long long)psarFileInfo.size, UPDATE_PSAR_SUFFIX); + return false; + } + + info->present = true; + info->archiveSize = psarFileInfo.size; + // PSPFileInfo reports local time, since that's what the PSP wants. Back to UTC seconds - + // mktime is the exact inverse of the localtime_r the file system used. Not every kind of + // "disc" we can mount records a date at all, hence the check. + if (psarFileInfo.mtime.tm_mday != 0) { + tm local = psarFileInfo.mtime; + const time_t t = mktime(&local); + info->mtime = t == (time_t)-1 ? 0 : (s64)t; + } + + std::vector sfo; + if (ReadWholeFile(fs, (std::string(pathPrefix) + UPDATE_SFO_SUFFIX).c_str(), &sfo)) { + info->title = TitleFromSFO(sfo); + if (!info->title.empty()) { + info->version = VersionFromUpdaterTitle(info->title); + } + } + return true; +} + std::string ReadUpdaterVersion(const Path &filename) { std::vector psar; std::string version; diff --git a/Core/Util/PSARUnpack.h b/Core/Util/PSARUnpack.h index 42f3afe3b3..f604298170 100644 --- a/Core/Util/PSARUnpack.h +++ b/Core/Util/PSARUnpack.h @@ -17,12 +17,15 @@ #pragma once +#include #include +#include #include #include "Common/CommonTypes.h" class Path; +class IFileSystem; // Unpacks the firmware image inside an official PSP updater (PSP/GAME/UPDATE/EBOOT.PBP). // @@ -105,6 +108,27 @@ bool UnpackPSAR(const u8 *psar, size_t psarSize, const Path &outputDir, const PS // from whatever game the user already has rather than a separate download. bool UnpackUpdater(const Path &filename, const Path &outputDir, const PSARUnpackOptions &options, PSARUnpackStats *stats, std::string *error); +// What a game disc's bundled firmware updater says about itself. All of this comes from the +// PARAM.SFO and the directory entry next to the archive, so gathering it costs a couple of small +// reads - no decryption, and the archive itself is never touched. +struct BundledUpdateInfo { + bool present = false; + std::string version; // "6.61". Can be empty even when present, if the SFO is unreadable. + std::string title; // The updater's full SFO title, e.g. "PSP(tm) Update ver 6.61". + s64 archiveSize = 0; // Size of DATA.BIN, i.e. how much firmware is in there. + // When DATA.BIN was written, as Unix UTC seconds. 0 if the disc doesn't record one, which + // is normal for the shapes that aren't really an ISO. + s64 mtime = 0; + + // "6.61 (2011-01-25)", or just the version if there's no date. Empty if there's no updater. + std::string Describe() const; +}; + +// Reads the above out of a disc that's already open, whether that's an ISOFileSystem the caller +// mounted or the running game's disc0:. pathPrefix is what to stick in front of "PSP_GAME/..." - +// "/" for a freshly mounted image, "disc0:/" for the meta file system. +bool ReadBundledUpdateInfo(IFileSystem *fs, std::string_view pathPrefix, BundledUpdateInfo *info); + // The version string an updater advertises ("6.61"), read from the PARAM.SFO next to it - no // decryption needed, so it's cheap enough to check every disc with. Empty if there's no updater. std::string ReadUpdaterVersion(const Path &filename); diff --git a/UI/GameInfoCache.cpp b/UI/GameInfoCache.cpp index 480c966a09..c57e373f89 100644 --- a/UI/GameInfoCache.cpp +++ b/UI/GameInfoCache.cpp @@ -851,6 +851,15 @@ handleELF: } } + // Most UMDs carry a firmware updater, which is a source of things like the + // system fonts. Just note down what's there - unpacking it is a separate step. + if (flags_ & GameInfoFlags::BUNDLED_UPDATE_INFO) { + BundledUpdateInfo update; + ReadBundledUpdateInfo(&umd, "/", &update); + std::lock_guard lock(info_->lock); + info_->bundledUpdate = update; + } + if (flags_ & GameInfoFlags::PIC0) { info_->pic0.dataLoaded = ReadFileToString(&umd, join(gameRoot, "PIC0.PNG"), &info_->pic0.data, &info_->lock); } diff --git a/UI/GameInfoCache.h b/UI/GameInfoCache.h index 39cf389e17..51d9f7bf49 100644 --- a/UI/GameInfoCache.h +++ b/UI/GameInfoCache.h @@ -25,6 +25,7 @@ #include "Common/Thread/Event.h" #include "Core/ELF/ParamSFO.h" +#include "Core/Util/PSARUnpack.h" #include "Common/File/Path.h" namespace Draw { @@ -50,6 +51,7 @@ enum class GameInfoFlags { UNCOMPRESSED_SIZE = 0x80, SAVEDATA_SIZE = 0x100, ICON1_PMF = 0x200, + BUNDLED_UPDATE_INFO = 0x400, // The firmware updater that most game discs carry. ISO only. }; ENUM_CLASS_BITOPS(GameInfoFlags); @@ -170,6 +172,10 @@ public: u64 saveDataSize = 0; u64 installDataSize = 0; + // The firmware updater bundled on the disc, if any - see GameInfoFlags::BUNDLED_UPDATE_INFO. + // Always left empty for anything that isn't an ISO. + BundledUpdateInfo bundledUpdate; + std::string errorString; protected: diff --git a/UI/GameScreen.cpp b/UI/GameScreen.cpp index b654674330..28fe1705ab 100644 --- a/UI/GameScreen.cpp +++ b/UI/GameScreen.cpp @@ -66,7 +66,7 @@ void copyDeepLinkForPath(std::string_view filePath); void copyDeepLinkForPath(std::string_view) {} #endif -constexpr GameInfoFlags g_desiredFlags = GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON | GameInfoFlags::PIC0 | GameInfoFlags::PIC1 | GameInfoFlags::ICON1_PMF | GameInfoFlags::UNCOMPRESSED_SIZE | GameInfoFlags::SIZE | GameInfoFlags::SAVEDATA_SIZE; +constexpr GameInfoFlags g_desiredFlags = GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON | GameInfoFlags::PIC0 | GameInfoFlags::PIC1 | GameInfoFlags::ICON1_PMF | GameInfoFlags::UNCOMPRESSED_SIZE | GameInfoFlags::SIZE | GameInfoFlags::SAVEDATA_SIZE | GameInfoFlags::BUNDLED_UPDATE_INFO; class PMFView : public UI::InertView { public: @@ -387,6 +387,15 @@ void GameScreen::CreateContentViews(UI::ViewGroup *parent) { tvGameSize->SetShadow(true); } + // Most game discs carry a firmware updater, which holds files we'd like to have, like the fonts. + if ((knownFlags_ & GameInfoFlags::BUNDLED_UPDATE_INFO) && info_->bundledUpdate.present) { + char temp[256]; + snprintf(temp, sizeof(temp), "%s: %s, %s", ga->T_cstr("Firmware update on disc"), + info_->bundledUpdate.Describe().c_str(), NiceSizeFormat(info_->bundledUpdate.archiveSize).c_str()); + TextView *tvUpdate = mainGameInfo->Add(new TextView(temp, ALIGN_LEFT, true, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); + tvUpdate->SetShadow(true); + } + if ((knownFlags_ & GameInfoFlags::SAVEDATA_SIZE)) { char temp[256]; if (info_->saveDataSize > 0) { diff --git a/assets/lang/en_US.ini b/assets/lang/en_US.ini index 2f7d45b472..c1489a5521 100644 --- a/assets/lang/en_US.ini +++ b/assets/lang/en_US.ini @@ -624,6 +624,7 @@ Delete Save Data = Delete savedata Desktop shortcut created = Desktop shortcut created Europe = Europe File size incorrect, bad or modified ISO = File size incorrect, bad or modified ISO +Firmware update on disc = Firmware update on disc Game = Game Game ID unknown - not in the ReDump database = Game ID unknown - not in the ReDump database Game Settings = Game settings