// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See portable_switch.h. #include "portable_switch.h" #include "installer.h" #include "locate.h" #include "placement.h" #include #include #include #include #include #include #include namespace install { namespace { namespace fs = std::filesystem; Result Fail(Error error, std::string message, const fs::path& path = {}) { Result r; r.error = error; r.message = std::move(message); r.file = path.string(); return r; } // Not there, or a folder with nothing in it (hidden files count). bool EmptyOrMissing(const fs::path& dir) { std::error_code ec; fs::file_status status = fs::symlink_status(dir, ec); if (status.type() == fs::file_type::not_found) return true; if (ec || !fs::is_directory(status)) return false; fs::directory_iterator it(dir, ec); return !ec && it == fs::directory_iterator(); } // An unfinished copy from an earlier attempt goes (only one carrying // the tag: anything else of that name is the player's). Result ClearPartial(const fs::path& partial, std::vector& said) { std::error_code ec; fs::file_status status = fs::symlink_status(partial, ec); if (status.type() == fs::file_type::not_found) return {}; std::error_code tagEc; if (!fs::is_directory(status) || !fs::is_regular_file(fs::symlink_status(partial / kPortablePartialTag, tagEc))) return Fail(Error::DestinationNotEmpty, std::format("{} is in the way, and it isn't an unfinished copy of " "SpeedBreaker's. Move it or delete it, then try again.", partial.string()), partial); fs::remove_all(partial, ec); if (ec) return Fail(Error::WriteError, std::format("Couldn't delete {}, an unfinished copy from an earlier attempt ({}). " "Delete it, then try again.", partial.string(), ec.message()), partial); said.push_back(std::format("Deleted {}, an unfinished copy from an earlier attempt.", partial.string())); return {}; } Result StartPartial(const fs::path& partial) { std::error_code ec; fs::create_directories(partial.parent_path(), ec); fs::create_directory(partial, ec); if (!ec) { FILE* f = fopen((partial / kPortablePartialTag).string().c_str(), "wb"); if (f && fputs("An unfinished copy of SpeedBreaker's data (speedbreaker.sh --portable).\n", f) >= 0 && fclose(f) == 0) return {}; if (f) fclose(f); ec = std::error_code(errno ? errno : EIO, std::generic_category()); } return Fail(Error::DestinationNotWritable, std::format("SpeedBreaker can't write to {} ({}): nothing was copied.", partial.parent_path().string(), ec.message()), partial); } // The installed game a data folder's record and default point to, as // FindGameInstall picks it: the recorded one if it's usable, else the // default one. struct FoundGame { fs::path dir; // the game to copy (empty: none) std::optional lost; // a recorded install that isn't usable (and no default one is) std::vector skipped; // left out of the data copy: the game folders and their in-between folders }; FoundGame FindGame(const fs::path& data, const portable::Layout& layout, const Manifest& manifest) { FoundGame found; fs::path fallback = data / "game"; std::optional recorded = ReadInstallRecord(data / "game-location", layout); if (recorded && IsUsableInstall(*recorded, true, manifest)) found.dir = *recorded; else if (IsUsableInstall(fallback, true, manifest)) found.dir = fallback; else if (recorded) found.lost = recorded; for (const std::optional& game : { std::optional(fallback), recorded }) if (game) { fs::path dir = ResolvedFolder(*game); found.skipped.push_back(dir); found.skipped.push_back(fs::path(dir.string() + ".partial")); found.skipped.push_back(fs::path(dir.string() + ".previous")); } return found; } struct CopyStats { uintmax_t files = 0, bytes = 0; std::vector skipped; // links and special files, not copied }; // `from`'s contents into the folder `to` (made by the caller): folders // and files, with their modification times (the bug report and the log // rotation sort by them). Left out: the game's record at the top, and // the folders in `skip` (the game and its in-between folders). Result CopyData(const fs::path& from, const fs::path& to, const std::vector& skip, CopyStats& stats, bool top) { std::error_code ec; fs::directory_iterator it(from, ec); for (fs::directory_iterator end; !ec && it != end; it.increment(ec)) { const fs::path source = it->path(); const fs::path name = source.filename(); if (top && (name == "game-location" || name == "game-location.tmp" || name == ".write-check")) continue; if (std::any_of(skip.begin(), skip.end(), [&](const fs::path& s) { return IsAtOrInside(source, s); })) continue; std::error_code typeEc; fs::file_status status = it->symlink_status(typeEc); const fs::path target = to / name; if (fs::is_symlink(status) || (!fs::is_directory(status) && !fs::is_regular_file(status))) { stats.skipped.push_back(source.string()); continue; } std::error_code copyEc; if (fs::is_directory(status)) { fs::create_directory(target, copyEc); if (copyEc) return Fail(Error::WriteError, std::format("Couldn't make {} ({}): nothing was changed.", target.string(), copyEc.message()), target); if (Result r = CopyData(source, target, skip, stats, false); !r.Ok()) return r; } else { fs::copy_file(source, target, fs::copy_options::none, copyEc); if (copyEc) return Fail(Error::WriteError, std::format("Couldn't copy {} to {} ({}): nothing was changed.", source.string(), target.string(), copyEc.message()), source); stats.files++; stats.bytes += fs::file_size(source, typeEc); } std::error_code timeEc; fs::last_write_time(target, fs::last_write_time(source, timeEc), timeEc); } if (ec) return Fail(Error::ReadError, std::format("Couldn't read {} ({}): nothing was changed.", from.string(), ec.message()), from); return {}; } void SayCopied(const CopyStats& stats, const fs::path& from, const fs::path& to, std::vector& said) { if (stats.files == 0) said.push_back(std::format("{} holds no settings, saves or logs: nothing of them to copy.", from.string())); else said.push_back(std::format("Copied settings, saves and logs ({} files, {}) from {} to {}.", stats.files, FormatSize(stats.bytes), from.string(), to.string())); for (const std::string& path : stats.skipped) said.push_back(std::format("Not copied (a link or a special file): {}", path)); } // A name for the old folder that isn't taken: .before-portable-off--