diff --git a/Core/Config.cpp b/Core/Config.cpp index 2f7e0952f3..1523209d19 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -299,6 +299,7 @@ static const ConfigSetting generalSettings[] = { ConfigSetting("AutoSaveSymbolMap", SETTING(g_Config, bAutoSaveSymbolMap), false, CfgFlag::PER_GAME), ConfigSetting("CompressSymbols", SETTING(g_Config, bCompressSymbols), true, CfgFlag::DEFAULT), ConfigSetting("CacheFullIsoInRam", SETTING(g_Config, bCacheFullIsoInRam), false, CfgFlag::PER_GAME), + ConfigSetting("AutoUpgradeFirmware", SETTING(g_Config, bAutoUpgradeFirmware), true, CfgFlag::DEFAULT), ConfigSetting("RemoteISOPort", SETTING(g_Config, iRemoteISOPort), 0, CfgFlag::DEFAULT), ConfigSetting("LastRemoteISOServer", SETTING(g_Config, sLastRemoteISOServer), "", CfgFlag::DEFAULT), ConfigSetting("LastRemoteISOPort", SETTING(g_Config, iLastRemoteISOPort), 0, CfgFlag::DEFAULT), diff --git a/Core/Config.h b/Core/Config.h index cb4ebc95a6..1cb3f20bc1 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -247,6 +247,7 @@ public: bool bAutoSaveSymbolMap; bool bCompressSymbols; bool bCacheFullIsoInRam; + bool bAutoUpgradeFirmware; int iRemoteISOPort; // Also used for serving a local remote debugger. std::string sLastRemoteISOServer; int iLastRemoteISOPort; diff --git a/Core/System.cpp b/Core/System.cpp index f90d35f494..740d6ddbee 100644 --- a/Core/System.cpp +++ b/Core/System.cpp @@ -63,6 +63,7 @@ #include "Core/Config.h" #include "Core/Core.h" #include "Core/Util/PathUtil.h" +#include "Core/Util/PSARUnpack.h" #include "Core/CoreTiming.h" #include "Core/CoreParameter.h" #include "Core/FileLoaders/RamCachingFileLoader.h" @@ -513,6 +514,11 @@ static bool CPU_Init(FileLoader *fileLoader, IdentifiedFileType type, std::strin g_CoreParameter.mountIsoLoader = ConstructFileLoader(g_CoreParameter.mountIso); } + // Most game discs carry a firmware updater, so this is where a NAND with nothing (or only the + // fonts) in it gets filled in. Has to happen before the mount below: the install erases and + // rewrites the very directory flash0:/flash1: point at. + AutoInstallFirmwareFromDisc(); + MountFileSystems(); // Game-specific settings are load from for example Load_PSP_ISO (which calls g_Config.LoadGameConfig). diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index 3d6ec49089..6f7d1d1c5d 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -22,11 +22,13 @@ #include "zlib.h" +#include "Common/Data/Text/I18n.h" #include "Common/File/DirListing.h" #include "Common/File/FileUtil.h" #include "Common/File/Path.h" #include "Common/Log.h" #include "Common/StringUtils.h" +#include "Common/System/OSD.h" #include "Core/Config.h" #include "Core/ELF/ParamSFO.h" #include "Core/ELF/PBPReader.h" @@ -587,9 +589,17 @@ bool PSARReader::Init(std::string *error) { decrypted_ = ReadU32(psar_ + 0x20) == PSAR_DECRYPTED_MARKER; overhead_ = decrypted_ ? 0 : PSAR_HEADER_SIZE; + // The header records how long the run of records is. Trusting it both stops us decoding + // trailing padding as an entry and, more importantly, catches a short archive: without this + // check a truncated one just looks like a smaller complete one, and unpacks to half a + // firmware that every stat reports as a clean install. limit_ = size_; const u32 declaredSize = ReadU32(psar_ + 8); - if (declaredSize >= 0x40 && declaredSize <= size_) { + if (declaredSize > size_) { + *error = StringFromFormat("Truncated archive: it declares %u bytes but only %u are here", declaredSize, (u32)size_); + return false; + } + if (declaredSize >= 0x40) { limit_ = declaredSize; } @@ -1171,6 +1181,29 @@ bool EraseInstalledFirmware(const Path &nandRoot, std::string *error) { return success; } +int FirmwareVersionToInt(std::string_view version) { + const size_t dot = version.find('.'); + if (dot == std::string_view::npos || dot == 0 || dot + 1 >= version.size()) { + return 0; + } + int numeric = 0; + for (size_t i = 0; i < version.size(); i++) { + if (i == dot) { + continue; + } + if (version[i] < '0' || version[i] > '9') { + return 0; + } + numeric = numeric * 10 + (version[i] - '0'); + } + if (version.size() - dot == 2) { // One digit after the dot. + numeric *= 10; + } else if (version.size() - dot != 3) { + return 0; + } + return numeric; +} + bool FirmwareVersionSupportsVSH(std::string_view version) { // Every firmware boots to an interactive XMB, checked one release at a time against all 39 // versions that ship on a disc - 1.50 through 6.60 - plus the download-only 6.61. So the @@ -1179,29 +1212,7 @@ bool FirmwareVersionSupportsVSH(std::string_view version) { // // The lower bound is a sanity check rather than a real limit. 1.50 is the oldest firmware // there is, so anything below it isn't a version string we wrote. - // - // "6.61" -> 661. Sony always writes the minor part with two digits, but don't rely on it: - // a single-digit one is a tens value ("5.5" is 5.50, not 5.05). - const size_t dot = version.find('.'); - if (dot == std::string_view::npos || dot == 0 || dot + 1 >= version.size()) { - return false; - } - int numeric = 0; - for (size_t i = 0; i < version.size(); i++) { - if (i == dot) { - continue; - } - if (version[i] < '0' || version[i] > '9') { - return false; - } - numeric = numeric * 10 + (version[i] - '0'); - } - if (version.size() - dot == 2) { // One digit after the dot. - numeric *= 10; - } else if (version.size() - dot != 3) { - return false; - } - return numeric >= 150; + return FirmwareVersionToInt(version) >= 150; } std::string BundledUpdateInfo::Describe() const { @@ -1277,16 +1288,57 @@ static bool ReadFromMountedDisc(const char *suffix, std::vector *out) { return !out->empty(); } +// Just the front of a file on the mounted disc. DATA.BIN is around a hundred MB, and the version +// we're after lives in its first record. +static bool ReadHeadFromMountedDisc(const char *suffix, size_t bytes, std::vector *out) { + const int fd = pspFileSystem.OpenFile(std::string("disc0:/") + suffix, FILEACCESS_READ); + if (fd < 0) { + return false; + } + out->resize(bytes); + const size_t read = pspFileSystem.ReadFile(fd, out->data(), (s64)bytes); + pspFileSystem.CloseFile(fd); + out->resize(std::min(read, bytes)); + return !out->empty(); +} + +// The version the archive declares about itself, which is in its first record - so this needs the +// front of DATA.BIN and nothing else. Worth having as a fallback because the PARAM.SFO sitting +// next to the updater is zero bytes long on a fair number of discs. +static std::string ReadUpdaterVersionFromArchiveHeader() { + std::vector head; + if (!ReadHeadFromMountedDisc(UPDATE_PSAR_SUFFIX, 64 * 1024, &head)) { + return std::string(); + } + if (head.size() < 0x40 || ReadU32(head.data()) != PSAR_MAGIC) { + // Very common, and not a problem: a PSN release keeps the updater's directory entry but + // zero-fills the file, so there's nothing on the disc to install in the first place. + DEBUG_LOG(Log::Loader, "The disc's DATA.BIN isn't an archive - a placeholder, most likely"); + return std::string(); + } + PSARReader reader(head.data(), head.size()); + std::string error; + if (!reader.Init(&error)) { + WARN_LOG(Log::Loader, "Couldn't read the disc updater's header: %s", error.c_str()); + return std::string(); + } + return reader.firmwareVersion(); +} + bool MountedDiscHasUpdater() { return pspFileSystem.GetFileInfo(std::string("disc0:/") + UPDATE_PSAR_SUFFIX).exists; } std::string ReadMountedDiscUpdaterVersion() { std::vector sfo; - if (!ReadFromMountedDisc(UPDATE_SFO_SUFFIX, &sfo)) { - return std::string(); + if (ReadFromMountedDisc(UPDATE_SFO_SUFFIX, &sfo)) { + const std::string version = VersionFromSFO(sfo); + if (!version.empty()) { + return version; + } } - return VersionFromSFO(sfo); + // No SFO, or an empty one - ask the archive itself. + return ReadUpdaterVersionFromArchiveHeader(); } bool UnpackUpdaterFromMountedDisc(const Path &outputDir, const PSARUnpackOptions &options, PSARUnpackStats *stats, std::string *error) { @@ -1317,3 +1369,168 @@ bool UnpackUpdater(const Path &filename, const Path &outputDir, const PSARUnpack } return UnpackPSAR(psar.data(), psar.size(), outputDir, options, stats, error); } + +// An install is assembled here first and only swapped into place once it's known to be complete. +// A subdirectory of the NAND root rather than a temp dir elsewhere, so it's on the same volume and +// the swap at the end is a rename per top-level entry instead of a second copy of the firmware. +static const char *FIRMWARE_STAGING_DIR = "install-staging"; + +// Replaces what's in the NAND with what's in the staging directory. Everything the unpack produced +// moves, not just flash0/flash1/ipl: an entry no file list claimed is written out under its short +// name, which lands at the top level. +static bool SwapStagingIntoPlace(const Path &nandRoot, const Path &staging, std::string *error) { + std::vector entries; + if (!File::GetFilesInDir(staging, &entries) || entries.empty()) { + *error = "Nothing to install - the staging directory is empty"; + return false; + } + + // Past this point the NAND is being rewritten. Everything that can fail has already been + // done, so the window where an interruption leaves a half-built NAND is these few renames. + if (!EraseInstalledFirmware(nandRoot, error)) { + return false; + } + + for (const File::FileInfo &entry : entries) { + const Path destination = nandRoot / entry.name; + // EraseInstalledFirmware covers the three directories a firmware normally has; a loose + // top-level file wouldn't be one of them. + if (File::Exists(destination)) { + const bool removed = entry.isDirectory ? File::DeleteDirRecursively(destination) : File::Delete(destination); + if (!removed) { + *error = "Couldn't replace " + destination.ToString(); + return false; + } + } + // MoveIfFast rather than Rename: the two are in different directories, which plain + // Rename refuses for content URIs, and this is the same-name case MoveIfFast handles. + if (!File::MoveIfFast(entry.fullName, destination)) { + *error = "Couldn't move " + entry.fullName.ToString() + " into place"; + return false; + } + } + return true; +} + +bool InstallFirmware(const Path &updater, const Path &nandRoot, const PSARUnpackOptions &options, PSARUnpackStats *stats, std::string *error) { + PSARUnpackStats localStats; + if (!stats) { + stats = &localStats; + } + std::string localError; + if (!error) { + error = &localError; + } + + INFO_LOG(Log::Loader, "Installing the firmware from %s into %s (model %s)", + updater.empty() ? "the mounted disc" : updater.c_str(), nandRoot.c_str(), + PSPModelGenerationToString(options.model)); + + // Unpack somewhere harmless first, so that everything below can refuse the install without + // having touched the firmware that's already there. Anything left over from an install that + // was interrupted has nothing to do with this one. + const Path staging = nandRoot / FIRMWARE_STAGING_DIR; + if (File::Exists(staging) && !File::DeleteDirRecursively(staging)) { + *error = "Couldn't clear " + staging.ToString(); + return false; + } + + bool ok = updater.empty() + ? UnpackUpdaterFromMountedDisc(staging, options, stats, error) + : UnpackUpdater(updater, staging, options, stats, error); + + if (ok && stats->written == 0) { + // Nothing came out, so the archive had no file list for the model we asked for - old + // firmwares predate the later models. Not something to call a success. + if (error->empty()) { + *error = "The updater has no firmware for this PSP model"; + } + ok = false; + } + if (ok && stats->failed > 0) { + // Some entry didn't survive - a bad dump or a failing disc (every record is hash-checked + // on the way out, so corruption can't slip past), or the disk filled up. A firmware with + // holes in it is worse than the one already installed: it boots far enough to be believed + // and, since it looks like a complete install, nothing would ever replace it. + if (error->empty()) { + *error = StringFromFormat("%d of %d files couldn't be unpacked", stats->failed, stats->failed + stats->written); + } + ok = false; + } + + if (ok) { + ok = SwapStagingIntoPlace(nandRoot, staging, error); + } + + // Whether it worked or not, don't leave the staging copy behind. + if (File::Exists(staging) && !File::DeleteDirRecursively(staging)) { + WARN_LOG(Log::Loader, "Couldn't clean up %s", staging.c_str()); + } + return ok; +} + +// The OSD id for the automatic install, so the progress bar updates in place. +static const char *AUTO_INSTALL_OSD_ID = "firmware_install"; + +bool AutoInstallFirmwareFromDisc() { + if (!g_Config.bAutoUpgradeFirmware || !MountedDiscHasUpdater()) { + return false; + } + + const std::string discVersion = ReadMountedDiscUpdaterVersion(); + const int discVersionNumber = FirmwareVersionToInt(discVersion); + if (discVersionNumber == 0) { + // Neither the updater's PARAM.SFO nor the archive itself says which version this is - + // usually because there's no real archive there, just a zero-filled placeholder. Without + // a version there's no telling an upgrade from a downgrade, so leave the NAND alone. + INFO_LOG(Log::Loader, "The disc has an updater, but no version to compare against ('%s')", discVersion.c_str()); + return false; + } + + const Path nandRoot = GetSysDirectory(DIRECTORY_NAND); + InstalledFirmwareInfo installed; + ReadInstalledFirmwareInfo(nandRoot, &installed, false); + + const int installedVersion = FirmwareVersionToInt(installed.version); + if (installedVersion != 0) { + if (discVersionNumber <= installedVersion) { + return false; + } + } else if (installed.kernelModuleCount > 0) { + // A real firmware whose version.txt we couldn't read. Replacing it on every single boot, + // on the off chance that the disc's is newer, isn't worth it. + WARN_LOG(Log::Loader, "A firmware is installed but doesn't say which version it is. Leaving it alone."); + return false; + } + // Otherwise there's either nothing installed or only a partial install like the fonts, and + // the disc's firmware is an upgrade either way. + + INFO_LOG(Log::Loader, "Auto-installing firmware %s from the disc (installed: '%s')", + discVersion.c_str(), installed.version.c_str()); + + auto sy = GetI18NCategory(I18NCat::SYSTEM); + const std::string message = std::string(sy->T("Installing firmware")) + " " + discVersion; + g_OSD.SetProgressBar(AUTO_INSTALL_OSD_ID, message, 0.0f, 1.0f, 0.0f, 0.0f); + + PSARUnpackOptions options; + options.model = EmulatedModelGeneration(); + options.progress = [&message](float progress) { + g_OSD.SetProgressBar(AUTO_INSTALL_OSD_ID, message, 0.0f, 1.0f, progress, 0.0f); + }; + + PSARUnpackStats stats; + std::string error; + const bool success = InstallFirmware(Path(), nandRoot, options, &stats, &error); + g_OSD.RemoveProgressBar(AUTO_INSTALL_OSD_ID, success, 0.0f); + + if (!success) { + ERROR_LOG(Log::Loader, "Failed to install the firmware from the disc: %s", error.c_str()); + g_OSD.Show(OSDType::MESSAGE_ERROR, sy->T("Firmware installation failed"), error, 5.0f); + return false; + } + + INFO_LOG(Log::Loader, "Installed firmware %s from the disc: %d files, %d failed", + stats.firmwareVersion.c_str(), stats.written, stats.failed); + g_OSD.Show(OSDType::MESSAGE_SUCCESS, sy->T("Installed firmware"), stats.firmwareVersion, 3.0f); + return true; +} diff --git a/Core/Util/PSARUnpack.h b/Core/Util/PSARUnpack.h index 6b524a543d..b448938c32 100644 --- a/Core/Util/PSARUnpack.h +++ b/Core/Util/PSARUnpack.h @@ -168,12 +168,38 @@ void ReadInstalledFirmwareInfo(const Path &nandRoot, InstalledFirmwareInfo *info // Wipes what's in the NAND directory: flash0, flash1 and ipl. Two firmwares can't be merged - // files a newer one dropped would linger and still get loaded - so an install starts from empty. +// Deliberately leaves an install's staging directory alone: InstallFirmware() calls this with a +// finished firmware sitting in there, waiting to be moved into the space this just cleared. bool EraseInstalledFirmware(const Path &nandRoot, std::string *error); +// "6.61" -> 661. Sony writes the minor part with two digits, but a single-digit one is still a +// tens value, so "5.5" is 550 and not 505. Returns 0 if it isn't a version string at all, which +// makes the result safe to compare with: an unknown version is older than every real one. +int FirmwareVersionToInt(std::string_view version); + // The firmware versions we can actually boot the VSH (XMB) on. Every other version loads, but // the module patches it needs are version-specific, so it won't get anywhere. bool FirmwareVersionSupportsVSH(std::string_view version); +// Installs a firmware into the NAND directory. Two firmwares can't be merged - a file the new one +// doesn't have would linger and still get loaded - so this replaces rather than overlays. +// +// The unpack goes to a staging directory inside the NAND root first, and what's installed is only +// erased once the new firmware is complete on disk, so a failure of any kind leaves the existing +// one untouched. The cost is needing room for both at once. "Complete" is strict: a single entry +// that didn't unpack fails the whole install, because a firmware with holes in it still looks +// installed and would never be replaced. +// +// updater is an updater EBOOT.PBP, a disc image, or the folder holding one; pass an empty path to +// install from the disc mounted as disc0:, i.e. the running game's own disc. +bool InstallFirmware(const Path &updater, const Path &nandRoot, const PSARUnpackOptions &options, PSARUnpackStats *stats, std::string *error); + +// Called while a game boots, with its disc mounted as disc0:. Most UMDs carry a firmware updater, +// and having a real firmware is what the LLE modules want - so if the disc's is newer than what's +// installed, or nothing identifiable is installed at all, unpack it. Shows progress on the OSD. +// Does nothing unless g_Config.bAutoUpgradeFirmware is set. Returns true if it installed one. +bool AutoInstallFirmwareFromDisc(); + // The same three, for the disc mounted as disc0: - i.e. the game that's running. These read // through the mounted filesystem instead of opening the image a second time, which also means // they work for the shapes that aren't an image at all, like a folder-based "disc". diff --git a/UI/FirmwareScreen.cpp b/UI/FirmwareScreen.cpp index 6bd2e1251a..d7e2da5722 100644 --- a/UI/FirmwareScreen.cpp +++ b/UI/FirmwareScreen.cpp @@ -132,6 +132,9 @@ void FirmwareScreen::CreateSettingsViews(UI::ViewGroup *parent) { install->SetEnabled(!gameRunning); } + parent->Add(new CheckBox(&g_Config.bAutoUpgradeFirmware, sy->T("Install or upgrade firmware from game discs"), + sy->T("AutoUpgradeFirmwareDesc", "Most game discs carry a firmware updater. Install it when there's no firmware yet, or when the disc's is newer."))); + if (info_.anythingInstalled) { Choice *erase = parent->Add(new Choice(sy->T("Erase firmware"), ImageID("I_TRASHCAN"))); erase->OnClick.Add([this](UI::EventParams &) { diff --git a/UI/InstallUpdateScreen.cpp b/UI/InstallUpdateScreen.cpp index 215e6f8b56..581a9f3a81 100644 --- a/UI/InstallUpdateScreen.cpp +++ b/UI/InstallUpdateScreen.cpp @@ -143,30 +143,12 @@ void InstallUpdateScreen::StartInstall() { PSARUnpackOptions options; options.model = EmulatedModelGeneration(); - INFO_LOG(Log::Loader, "Unpacking the updater %s into %s (model %s)", path_.c_str(), - destination_.c_str(), PSPModelGenerationToString(options.model)); - g_threadManager.EnqueueTask(new IndependentTask(TaskType::IO_BLOCKING, TaskPriority::NORMAL, [state = state_, path = path_, destination = destination_, options]() mutable { options.progress = [state](float progress) { state->progress = progress; }; - // Two firmwares can't be merged - a file the new one doesn't have would linger and still - // get loaded - so start from an empty NAND. - if (!EraseInstalledFirmware(destination, &state->error)) { - state->success = false; - state->done = true; - return; - } - state->success = UnpackUpdater(path, destination, options, &state->stats, &state->error); - if (state->success && state->stats.written == 0) { - // Nothing came out, so the archive had no file list for the model we asked for - - // old firmwares predate the later models. Not something to call a success. - state->success = false; - if (state->error.empty()) { - state->error = "The updater has no firmware for this PSP model"; - } - } + state->success = InstallFirmware(path, destination, options, &state->stats, &state->error); // Everything above is published by this store - see the atomic in InstallState. state->done = true; })); diff --git a/headless/Headless.cpp b/headless/Headless.cpp index f49f66d4ea..e659c4b242 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -768,6 +768,9 @@ int main(int argc, const char* argv[]) { g_Config.sMACAddress = "12:34:56:78:9A:BC"; g_Config.iFirmwareVersion = PSP_DEFAULT_FIRMWARE; g_Config.iPSPModel = PSP_MODEL_SLIM; + // Booting an ISO shouldn't rewrite the NAND out from under a test run - and the tests want + // whatever firmware is installed to stay put. Install one with --unpack-updater instead. + g_Config.bAutoUpgradeFirmware = false; g_Config.iGameVolume = VOLUMEHI_FULL; g_Config.iReverbVolume = VOLUMEHI_FULL; g_Config.internalDataDirectory.clear();