//+----------------------------------------------------------------------------+ //| Description: Magic Set Editor - Program to make Magic (tm) cards | //| Copyright: (C) Twan van Laarhoven and the other MSE developers | //| License: GNU General Public License 2 or later (see file COPYING) | //+----------------------------------------------------------------------------+ // ----------------------------------------------------------------------------- : Includes #include #include #include #include #include #include #include #include #include #include #include #include // ----------------------------------------------------------------------------- : PackageManager : in memory PackageManager package_manager; void PackageManager::init() { local.init(true); global.init(false); if (!(local.valid() || global.valid())) throw Error(_("The MSE data files can not be found, there should be a directory called 'data' with these files. ") _("The expected place to find it in was either ") + wxStandardPaths::Get().GetDataDir() + _(" or ") + wxStandardPaths::Get().GetUserDataDir()); } void PackageManager::destroy() { loaded_packages.clear(); } void PackageManager::reset() { loaded_packages.clear(); } PackagedP PackageManager::openAny(const String& name_, bool just_header) { String name = trim(name_); if (starts_with(name,_("/"))) name = name.substr(1); if (starts_with(name,_(":NO-WARN-DEP:"))) name = name.substr(13); // Attempt to load local data first. String filename; if (wxFileName(name).IsRelative()) { // local data dir? filename = normalize_filename(local.name(name)); if (!wxFileExists(filename) && !wxDirExists(filename)) { // global data dir filename = normalize_filename(global.name(name)); } } else { // Absolute filename filename = normalize_filename(name); } // Is this package already loaded? PackagedP& p = loaded_packages[filename]; if (!p) { // load with the right type, based on extension wxFileName fn(filename); if (fn.GetExt() == _("mse-game")) p = make_intrusive(); else if (fn.GetExt() == _("mse-style")) p = make_intrusive(); else if (fn.GetExt() == _("mse-locale")) p = make_intrusive(); else if (fn.GetExt() == _("mse-include")) p = make_intrusive(); else if (fn.GetExt() == _("mse-symbol-font")) p = make_intrusive(); else if (fn.GetExt() == _("mse-export-template")) p = make_intrusive(); else { throw PackageError(_("Unrecognized package type: '") + fn.GetExt() + _("'\nwhile trying to open: ") + name); } p->open(filename, just_header); } else if (!just_header) { p->loadFully(); } return p; } void PackageManager::findMatching(const String& pattern, vector& out) { // first find local packages String file = local.findFirstMatching(pattern); while (!file.empty()) { out.push_back(openAny(file, true)); file = wxFindNextFile(); } // then global packages not already in the list file = global.findFirstMatching(pattern); while (!file.empty()) { PackagedP p = openAny(file, true); if (find(out.begin(), out.end(), p) == out.end()) { out.push_back(p); } file = wxFindNextFile(); } } pair,Packaged*> PackageManager::openFileFromPackage(Packaged* package, const String& name) { if (!name.empty() && name.GetChar(0) == _('/')) { // absolute name; break name size_t start = name.find_first_not_of(_("/\\"), 1); // allow "//package/name" from incorrect scripts size_t pos = name.find_first_of(_("/\\"), start); if (start < pos && pos != String::npos) { // open package PackagedP p = openAny(name.substr(start, pos-start)); if (package && !is_substr(name,start,_(":NO-WARN-DEP:"))) { package->requireDependency(p.get()); } return {p->openIn(name.substr(pos + 1)), p.get()}; } } else if (package) { // relative name return {package->openIn(name), package}; } throw FileNotFoundError(name, _("No package name specified, use '/package/filename'")); } pair, Packaged*> openFileFromPackage(Packaged* package, const String& name) { return package_manager.openFileFromPackage(package, name); } String PackageManager::openFilenameFromPackage(Packaged* package, const String& name) { if (!name.empty() && name.GetChar(0) == _('/')) { // absolute name; break name size_t start = name.find_first_not_of(_("/\\"), 1); // allow "//package/name" from incorrect scripts size_t pos = name.find_first_of(_("/\\"), start); if (start < pos && pos != String::npos) { // open package PackagedP p = openAny(name.substr(start, pos-start)); if (package && !is_substr(name,start,_(":NO-WARN-DEP:"))) { package->requireDependency(p.get()); } return p->absoluteFilename() + _("/") + name.substr(pos + 1); } } else if (package) { // relative name return package->absoluteFilename() + _("/") + name; } throw FileNotFoundError(name, _("No package name specified, use '/package/filename'")); } String PackageManager::getDictionaryDir(bool l) const { String dir = (l ? local : global).getDirectory(); if (dir.empty()) return wxEmptyString; else return dir + _("/dictionaries/"); } // ----------------------------------------------------------------------------- : PackageManager : on disk bool PackageManager::checkDependency(const PackageDependency& dep, bool report_errors) { // mse package? if (dep.package == mse_package) { if (app_version < dep.version) { queue_message(MESSAGE_WARNING, _ERROR_3_("package out of date", _("Magic Set Editor"), app_version.toString(), dep.version.toString())); } return true; } // does the package exist? if (!local.exists(dep.package) && !global.exists(dep.package)) { if (report_errors) queue_message(MESSAGE_WARNING, _ERROR_1_("package not found", dep.package)); return false; } PackagedP package = openAny(dep.package, true); if (package->version < dep.version) { if (report_errors) queue_message(MESSAGE_WARNING, _ERROR_3_("package out of date", dep.package, package->version.toString(), dep.version.toString())); return false; } return true; } bool PackageManager::installedVersion(const String& package_name, Version& version_out) { if (package_name == mse_package) { version_out = app_version; return true; } else { if (!local.exists(package_name) && !global.exists(package_name)) return false; PackagedP package = openAny(package_name, true); version_out = package->version; return true; } } void PackageManager::findAllInstalledPackages(vector& packages) { // from directories vector more_packages; global.installedPackages(packages); local.installedPackages(more_packages); merge(packages, more_packages); // the magic appliation package packages.push_back(mse_installable_package()); // invariant: sorted: sort(packages); } bool PackageManager::install(const InstallablePackage& package) { bool install_local = package.has(PACKAGE_ACT_LOCAL); return (install_local ? local : global).install(package); } // ----------------------------------------------------------------------------- : PackageDirectory void PackageDirectory::init(bool local) { is_local = local; if (local) { init(wxStandardPaths::Get().GetUserDataDir() + _("/data")); } else { // determine data directory String dir = wxStandardPaths::Get().GetDataDir(); // check if this is the actual data directory, especially during debugging, // the data may be higher up: // exe path = mse/build/debug/mse.exe // data path = mse/data while (!wxDirExists(dir + _("/data"))) { String d = dir; dir = wxPathOnly(dir); if (d == dir) { // we are at the root -> 'data' not found anywhere in the path dir = wxStandardPaths::Get().GetDataDir(); break; } } init(dir + _("/data")); } } void PackageDirectory::init(const String& dir) { if (wxDirExists(dir)) directory = dir; else directory.clear(); } String PackageDirectory::name(const String& name) const { return directory + _("/") + name; } bool PackageDirectory::exists(const String& filename) const { String fn = name(filename); return wxFileExists(fn) || wxDirExists(fn); } String PackageDirectory::findFirstMatching(const String& pattern) const { if (!wxDirExists(directory)) return String(); return wxFindFirstFile(directory + _("/") + pattern, 0); } bool compare_name(const PackageVersionP& a, const PackageVersionP& b) { return a->name < b->name; } void PackageDirectory::installedPackages(vector& packages_out) { loadDatabase(); // find all package files vector in_dir; for (String s = findFirstMatching(_("*.mse-*")) ; !s.empty() ; s = wxFindNextFile()) { size_t pos = s.find_last_of(_("/\\")); if (pos != String::npos) s = s.substr(pos+1); // TODO : check for valid package names in_dir.push_back(s); } sort(in_dir.begin(), in_dir.end()); // merge with package database bool db_changed = false; vector::const_iterator it1 = packages.begin(); vector::const_iterator it2 = in_dir.begin(); while (it2 != in_dir.end()) { if (it1 == packages.end() || (*it1)->name > *it2) { // add new package to db try { PackagedP pack = package_manager.openAny(*it2, true); db_changed = true; PackageVersionP ver(new PackageVersion( is_local ? PackageVersion::STATUS_LOCAL : PackageVersion::STATUS_GLOBAL)); ver->check_status(*pack); packages_out.push_back(make_intrusive(make_intrusive(*pack), ver)); } catch (const Error&) {} ++it2; } else if ((*it1)->name < *it2) { // delete package from db db_changed = true; ++it1; } else { // ok, a package already in the db try { PackagedP pack = package_manager.openAny(*it2, true); (*it1)->check_status(*pack); packages_out.push_back(make_intrusive(make_intrusive(*pack), *it1)); } catch (const Error&) { db_changed = true; } ++it1, ++it2; } } if (it1 != packages.end()) db_changed = true; // has the database of installed packages changed? if (db_changed) { packages.clear(); FOR_EACH(p, packages_out) { if (p->installed) packages.push_back(p->installed); } saveDatabase(); } } void PackageDirectory::bless(const String& package_name) { PackagedP pack = package_manager.openAny(package_name, true); // already have this package? FOR_EACH(ver, packages) { if (ver->name == package_name) { ver->check_status(*pack); ver->bless(); return; } } // a new package PackageVersionP ver(new PackageVersion( is_local ? PackageVersion::STATUS_LOCAL : PackageVersion::STATUS_GLOBAL)); ver->check_status(*pack); ver->bless(); packages.push_back(ver); sort(packages.begin(), packages.end(), compare_name); } void PackageDirectory::removeFromDatabase(const String& package_name) { size_t i = 0, j = 0; for ( ; i < packages.size() ; ++i) { if (packages[i]->name != package_name) { packages[j++] = packages[i]; } } packages.resize(j); } IMPLEMENT_REFLECTION(PackageDirectory) { REFLECT(packages); } void PackageDirectory::loadDatabase() { if (!packages.empty()) return; String filename = databaseFile(); if (wxFileExists(filename)) { // packages file not existing is not an error wxFileInputStream file_stream = {filename}; if (!file_stream.Ok()) return; // failure is not an error Reader reader(file_stream, nullptr, filename); reader.handle_greedy(*this); sort(packages.begin(), packages.end(), compare_name); } } void PackageDirectory::saveDatabase() { wxFileOutputStream stream(databaseFile()); Writer writer(stream, app_version); writer.handle(*this); } String PackageDirectory::databaseFile() { return name(_("packages")); } // ----------------------------------------------------------------------------- : PackageDirectory : installing bool PackageDirectory::install(const InstallablePackage& package) { String n = name(package.description->name); if (package.action & PACKAGE_ACT_REMOVE) { if (!remove_file_or_dir(n)) return false; removeFromDatabase(package.description->name); } else if (package.action & PACKAGE_ACT_INSTALL) { if (!remove_file_or_dir(n + _(".new"))) return false; bool ok = actual_install(package, n + _(".new")); if (!ok) return false; move_ignored_files(n, n + _(".new")); // copy over files from the old installed version to the new one if (!remove_file_or_dir(n)) return false; if (!rename_file_or_dir(n + _(".new"), n)) return false; bless(package.description->name); } saveDatabase(); return true; } bool PackageDirectory::actual_install(const InstallablePackage& package, const String& install_dir) { String name = package.description->name; if (!package.installer->installer) { queue_message(MESSAGE_ERROR, _("Installer not found for package: ") + name); return false; } Installer& installer = *package.installer->installer; // install files const Packaged::FileInfos& file_infos = installer.getFileInfos(); for (Packaged::FileInfos::const_iterator it = file_infos.begin() ; it != file_infos.end() ; ++it) { String file = it->first; if (!is_substr_i(file,0,name)) continue; // not the right package // correct filename String local_file = install_dir + file.substr(name.length()); create_parent_dirs(local_file); // copy file auto in_stream = installer.openIn(file); wxFileOutputStream out_stream(local_file); if (!out_stream.IsOk()) { int act = wxMessageBox(_ERROR_1_("cannot create file", file), _TITLE_("cannot create file"), wxICON_ERROR | wxYES_NO); if (act == wxNO) return false; } out_stream.Write(*in_stream); } // update package database // TODO: bless the package? return true; } // ----------------------------------------------------------------------------- : PackageVersion template <> void Writer::handle(const PackageVersion::FileInfo& f) { if (f.status == PackageVersion::FILE_DELETED) { handle(_("D ") + f.file); } else { handle(format_string(_("%s%s %s"), f.status == PackageVersion::FILE_ADDED ? _("A") : f.status == PackageVersion::FILE_MODIFIED ? _("M") : _(""), f.time.Format(_("%Y%m%dT%H%M%S")), f.file)); } } template <> void Reader::handle(PackageVersion::FileInfo& f) { String s; handle(s); // read status if (s.size() < 2) {f.status = PackageVersion::FILE_ADDED; return; } f.status = s.GetChar(0) == _('M') ? PackageVersion::FILE_MODIFIED : s.GetChar(0) == _('A') ? PackageVersion::FILE_ADDED : s.GetChar(0) == _('D') ? PackageVersion::FILE_DELETED : PackageVersion::FILE_UNCHANGED; if (f.status == PackageVersion::FILE_DELETED) { if (s.GetChar(1) != _(' ')) {f.status = PackageVersion::FILE_ADDED; return; } f.file = s.substr(2); return; } else if (f.status != PackageVersion::FILE_UNCHANGED) { s = s.substr(1); } if (s.size() < 8+1+6+1) {f.status = PackageVersion::FILE_ADDED; return; } if (s.GetChar(8+1+6) != _(' ')) {f.status = PackageVersion::FILE_ADDED; return; } // invalid format // read time, filename f.time.ParseFormat(s, _("%Y%m%dT%H%M%S")); f.file = s.substr(8+1+6+1); } IMPLEMENT_REFLECTION_NO_SCRIPT(PackageVersion) { REFLECT_NO_SCRIPT(name); REFLECT_NO_SCRIPT(version); REFLECT_NO_SCRIPT(status); REFLECT_NO_SCRIPT(files); } void PackageVersion::check_status(Packaged& package) { status &= ~STATUS_MODIFIED; if (!(status & STATUS_BLESSED)) status |= STATUS_MODIFIED; name = package.relativeFilename(); version = package.version; // Merge our files list with the list from the package vector new_files; Package::FileInfos fis = package.getFileInfos(); Package::FileInfos::const_iterator it1 = fis.begin(); vector::iterator it2 = files.begin(); //% size_t it2 = 0, size = files.size(); //% bool need_sort = false; while(it1 != fis.end() || it2 != files.end()) { if (it1 != fis.end() && it2 != files.end() && it1->first == it2->file) { DateTime mtime = package.modificationTime(*it1); if (mtime != it2->time) { it2->time = mtime; it2->status = FILE_MODIFIED; new_files.push_back(*it2); status |= STATUS_MODIFIED; } ++it1; ++it2; } else if (it1 != fis.end() && (it2 == files.end() || it1->first < it2->file)) { // this is a new file DateTime mtime = package.modificationTime(*it1); new_files.push_back(FileInfo(it1->first, mtime, FILE_ADDED)); status |= STATUS_MODIFIED; ++it1; } else { // this file is no longer in the package, it was deleted if (it2->status != FILE_ADDED) { it2->status = FILE_DELETED; new_files.push_back(*it2); status |= STATUS_MODIFIED; } ++it2; } } swap(files,new_files); } inline bool is_deleted(PackageVersion::FileInfo f) { return f.status == PackageVersion::FILE_DELETED; } void PackageVersion::bless() { files.erase(remove_if(files.begin(),files.end(),is_deleted),files.end()); FOR_EACH(f,files) { f.status = FILE_UNCHANGED; } status &= ~STATUS_MODIFIED; status |= STATUS_BLESSED; }