OpenCPN Partial API docs
Loading...
Searching...
No Matches
plugin_handler.cpp
Go to the documentation of this file.
1/***************************************************************************
2 * Copyright (C) 2019 Alec Leamas *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
8 * *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, see <https://www.gnu.org/licenses/>. *
16 ***************************************************************************/
17
25#include <algorithm>
26#include <cstdio>
27#include <fstream>
28#include <iomanip>
29#include <memory>
30#include <ostream>
31#include <regex>
32#include <set>
33#include <sstream>
34#include <stdexcept>
35#include <streambuf>
36#include <unordered_map>
37
38#include "wx/wxprec.h"
39#ifndef WX_PRECOMP
40#include "wx/wx.h"
41#endif
42
43#include <wx/dir.h>
44#include <wx/file.h>
45#include <wx/filename.h>
46#include <wx/string.h>
47#include <wx/tokenzr.h>
48#include <wx/window.h>
49#include <wx/uri.h>
50
51#include <archive.h>
52#include <archive_entry.h>
53
54typedef __LA_INT64_T la_int64_t; // "older" libarchive versions support
55
56#if defined(__MINGW32__) && defined(Yield)
57#undef Yield // from win.h, conflicts with mingw headers
58#endif
59
60#include "config.h"
61#include "model/base_platform.h"
64#include "model/config_vars.h"
65#include "model/cmdline.h"
66#include "model/downloader.h"
67#include "model/logger.h"
68#include "model/ocpn_utils.h"
69#include "model/plugin_cache.h"
71#include "model/plugin_loader.h"
72#include "model/plugin_paths.h"
73
74#include "std_filesystem.h"
75
76#ifdef _WIN32
77static std::string SEP("\\");
78#else
79static std::string SEP("/");
80#endif
81
82#ifndef F_OK // windows: missing unistd.h.
83#define F_OK 0
84#endif
85
87static std::vector<std::string> split(const std::string& s,
88 const std::string& delim) {
89 std::vector<std::string> result;
90 size_t pos = s.find(delim);
91 if (pos == std::string::npos) {
92 result.push_back(s);
93 return result;
94 }
95 result.push_back(s.substr(0, pos));
96 result.push_back(s.substr(pos + delim.length()));
97 return result;
98}
99
100inline std::string basename(const std::string path) {
101 wxFileName wxFile(path);
102 return wxFile.GetFullName().ToStdString();
103}
104
105bool isRegularFile(const char* path) {
106 wxFileName wxFile(path);
107 return wxFile.FileExists() && !wxFile.IsDir();
108}
109
110static void mkdir(const std::string path) {
111#if defined(_WIN32) && !defined(__MINGW32__)
112 _mkdir(path.c_str());
113#elif defined(__MINGW32__)
114 mkdir(path.c_str());
115#else
116 mkdir(path.c_str(), 0755);
117#endif
118}
119
120static std::vector<std::string> glob_dir(const std::string& dir_path,
121 const std::string& pattern) {
122 std::vector<std::string> found;
123 wxString s;
124 wxDir dir(dir_path);
125 auto match = dir.GetFirst(&s, pattern);
126 while (match) {
127 static const std::string SEP =
128 wxString(wxFileName::GetPathSeparator()).ToStdString();
129 found.push_back(dir_path + SEP + s.ToStdString());
130 match = dir.GetNext(&s);
131 }
132 return found;
133}
134
139static ssize_t PlugInIxByName(const std::string& name,
140 const ArrayOfPlugIns* plugins) {
141 const auto lc_name = ocpn::tolower(name);
142 for (unsigned i = 0; i < plugins->GetCount(); i += 1) {
143 if (lc_name == plugins->Item(i)->m_common_name.Lower().ToStdString()) {
144 return i;
145 }
146 }
147 return -1;
148}
149
150static std::string pluginsConfigDir() {
151 auto pluginDataDir = g_BasePlatform->DefaultPrivateDataDir().ToStdString();
152 pluginDataDir += SEP + "plugins";
153 if (!ocpn::exists(pluginDataDir)) {
154 mkdir(pluginDataDir);
155 }
156 pluginDataDir += SEP + "install_data";
157 if (!ocpn::exists(pluginDataDir)) {
158 mkdir(pluginDataDir);
159 }
160 return pluginDataDir;
161}
162
163static std::string importsDir() {
164 auto path = pluginsConfigDir();
165 path = path + SEP + "imports";
166 if (!ocpn::exists(path)) {
167 mkdir(path);
168 }
169 return path;
170}
171
172static std::string dirListPath(std::string name) {
173 std::transform(name.begin(), name.end(), name.begin(), ::tolower);
174 return pluginsConfigDir() + SEP + name + ".dirs";
175}
176
178 return pluginsConfigDir();
179}
180
181static std::vector<std::string> LoadLinesFromFile(const std::string& path) {
182 std::vector<std::string> lines;
183 std::ifstream src(path);
184 while (!src.eof()) {
185 char line[256];
186 src.getline(line, sizeof(line));
187 lines.push_back(line);
188 }
189 return lines;
190}
191
192#ifdef _WIN32
193static std::string tmpfile_path() {
195 char fname[4096];
196 if (tmpnam(fname) == NULL) {
197 MESSAGE_LOG << "Cannot create temporary file";
198 return "";
199 }
200 return std::string(fname);
201}
202
203#else
204static std::string tmpfile_path() {
206 fs::path tmp_path = fs::temp_directory_path() / "ocpn-tmpXXXXXX";
207 char buff[PATH_MAX];
208 strncpy(buff, tmp_path.c_str(), PATH_MAX - 1);
209 int fd = mkstemp(buff);
210 if (fd == -1) {
211 MESSAGE_LOG << "Cannot create temporary file: " << strerror(errno);
212 return "";
213 }
214 assert(close(fd) == 0 && "Cannot close file?!");
215 return std::string(buff);
216}
217#endif // _WIN32
218
220class Plugin {
221public:
222 Plugin(const PluginMetadata& metadata) {
223 m_abi = metadata.target;
224 m_abi_version = metadata.target_version;
225 m_major_version = ocpn::split(m_abi_version.c_str(), ".")[0];
226 m_name = metadata.name;
227 DEBUG_LOG << "Plugin: setting up, name: " << m_name;
228 DEBUG_LOG << "Plugin: init: abi: " << m_abi
229 << ", abi_version: " << m_abi_version
230 << ", major ver: " << m_major_version;
231 }
232 const std::string& abi() const { return m_abi; }
233 const std::string& abi_version() const { return m_abi_version; }
234 const std::string& major_version() const { return m_major_version; }
235 const std::string& name() const { return m_name; }
236
237private:
238 std::string m_abi;
239 std::string m_abi_version;
240 std::string m_major_version;
241 std::string m_name;
242};
243
245class Host {
246public:
247 Host(CompatOs* compatOs) {
248 m_abi = compatOs->name();
249 m_abi_version = compatOs->version();
250 m_major_version = ocpn::split(m_abi_version.c_str(), ".")[0];
251 DEBUG_LOG << "Host: init: abi: " << m_abi
252 << ", abi_version: " << m_abi_version
253 << ", major ver: " << m_major_version;
254 }
255
256 bool is_version_compatible(const Plugin& plugin) const {
257 if (ocpn::startswith(plugin.abi(), "ubuntu")) {
258 return plugin.abi_version() == m_abi_version;
259 }
260 return plugin.major_version() == m_major_version;
261 }
262
263 // Test if plugin abi is a Debian version compatible with host's Ubuntu
264 // abi version on a x86_64 platform.
265 bool is_debian_plugin_compatible(const Plugin& plugin) const {
266 if (!ocpn::startswith(m_abi, "ubuntu")) return false;
267 static const std::vector<std::string> compat_versions = {
268 // clang-format: off
269 "debian-x86_64;11;ubuntu-gtk3-x86_64;20.04",
270 "debian-wx32-x86_64;11;ubuntu-wx32-x86_64;22.04",
271 "debian-x86_64;12;ubuntu-x86_64;23.04",
272 "debian-x86_64;12;ubuntu-x86_64;23.10",
273 "debian-x86_64;12;ubuntu-x86_64;24.04",
274 "debian-x86_64;13;ubuntu-x86_64;26.04",
275 "debian-x86_64;sid;ubuntu-x86_64;26.04",
276
277 "debian-arm64;11;ubuntu-gtk3-arm64;20.04",
278 "debian-wx32-arm64;11;ubuntu-wx32-arm64;22.04",
279 "debian-arm64;12;ubuntu-arm64;23.04",
280 "debian-arm64;12;ubuntu-arm64;23.10",
281 "debian-arm64;12;ubuntu-arm64;24.04",
282 "debian-arm64;13;ubuntu-arm64;26.04",
283 "debian-arm64;sid;ubuntu-arm64;26.04",
284
285 "debian-armhf;10;ubuntu-armhf;18.04",
286 "debian-gtk3-armhf;10;ubuntu-gtk3-armhf;18.04",
287 "debian-armhf;11;ubuntu-gtk3-armhf;20.04",
288 "debian-wx32-armhf;11;ubuntu-wx32-armhf;22.04",
289 "debian-armhf;12;ubuntu-armhf;23.04",
290 "debian-armhf;12;ubuntu-armhf;23.10",
291 "debian-armhf;12;ubuntu-armhf;24.04",
292 "debian-armhf;sid;ubuntu-armhf;24.04"}; // clang-format: on
293
294 if (ocpn::startswith(plugin.abi(), "debian")) {
295 DEBUG_LOG << "Checking for debian plugin on a ubuntu host";
296 const std::string compat_version = plugin.abi() + ";" +
297 plugin.major_version() + ";" + m_abi +
298 ";" + m_abi_version;
299 for (auto& cv : compat_versions) {
300 if (compat_version == cv) {
301 return true;
302 }
303 }
304 }
305 return false;
306 }
307
308 const std::string& abi() const { return m_abi; }
309
310 const std::string& abi_version() const { return m_abi_version; }
311
312 const std::string& major_version() const { return m_major_version; }
313
314private:
315 std::string m_abi;
316 std::string m_abi_version;
317 std::string m_major_version;
318};
319
320CompatOs* CompatOs::GetInstance() {
321 static std::string last_global_os("");
322 static CompatOs* instance = 0;
323
324 if (!instance || last_global_os != g_compatOS) {
325 instance = new (CompatOs);
326 last_global_os = g_compatOS;
327 }
328 return instance;
329};
330
331CompatOs::CompatOs() : _name(PKG_TARGET), _version(PKG_TARGET_VERSION) {
332 // Get the specified system definition,
333 // from the environment override,
334 // or the config file override
335 // or the baked in (build system) values.
336
337 std::string compatOS(_name);
338 std::string compatOsVersion(_version);
339
340 if (getenv("OPENCPN_COMPAT_TARGET") != 0) {
341 _name = getenv("OPENCPN_COMPAT_TARGET");
342 if (_name.find(':') != std::string::npos) {
343 auto tokens = ocpn::split(_name.c_str(), ":");
344 _name = tokens[0];
345 _version = tokens[1];
346 }
347 } else if (g_compatOS != "") {
348 // CompatOS and CompatOsVersion in opencpn.conf/.ini file.
349 _name = g_compatOS;
350 if (g_compatOsVersion != "") {
351 _version = g_compatOsVersion;
352 }
353 } else if (ocpn::startswith(_name, "ubuntu") && (_version == "22.04")) {
354 int wxv = wxMAJOR_VERSION * 10 + wxMINOR_VERSION;
355 if (wxv >= 32) {
356 auto tokens = ocpn::split(_name.c_str(), "-");
357 _name = std::string(tokens[0]) + std::string("-wx32");
358 if (tokens.size() > 1) _name = _name + std::string("-") + tokens[1];
359 }
360 }
361
362 _name = ocpn::tolower(_name);
363 _version = ocpn::tolower(_version);
364}
365
366PluginHandler::PluginHandler() {}
367
368bool PluginHandler::IsCompatible(const PluginMetadata& metadata, const char* os,
369 const char* os_version) {
370 static const SemanticVersion kMinApi = SemanticVersion(1, 16);
371 static const SemanticVersion kMaxApi = SemanticVersion(1, 22);
372 auto plugin_api = SemanticVersion::parse(metadata.api_version);
373 if (plugin_api.major == -1) {
374 DEBUG_LOG << "Cannot parse API version \"" << metadata.api_version << "\"";
375 return false;
376 }
377 if (plugin_api < kMinApi || plugin_api > kMaxApi) {
378 DEBUG_LOG << "Incompatible API version \"" << metadata.api_version << "\"";
379 return false;
380 }
381
382 static const std::vector<std::string> simple_abis = {
383 "msvc", "msvc-wx32", "android-armhf", "android-arm64"};
384
385 Plugin plugin(metadata);
386 if (plugin.abi() == "all") {
387 DEBUG_LOG << "Returning true for plugin abi \"all\"";
388 return true;
389 }
390 auto compatOS = CompatOs::GetInstance();
391 Host host(compatOS);
392
393 auto found = std::find(simple_abis.begin(), simple_abis.end(), plugin.abi());
394 if (found != simple_abis.end()) {
395 bool ok = plugin.abi() == host.abi();
396 DEBUG_LOG << "Returning " << (ok ? "ok" : "fail") << " for " << host.abi();
397 return ok;
398 }
399 bool rv = false;
400 if (host.abi() == plugin.abi() && host.is_version_compatible(plugin)) {
401 rv = true;
402 DEBUG_LOG << "Found matching abi version " << plugin.abi_version();
403 } else if (host.is_debian_plugin_compatible(plugin)) {
404 rv = true;
405 DEBUG_LOG << "Found Debian version matching Ubuntu host";
406 }
407 // macOS is an exception as packages with universal binaries can support both
408 // x86_64 and arm64 at the same time
409 if (host.abi() == "darwin-wx32" && plugin.abi() == "darwin-wx32") {
410 OCPN_OSDetail* detail = g_BasePlatform->GetOSDetail();
411 auto found = metadata.target_arch.find(detail->osd_arch);
412 if (found != std::string::npos) {
413 rv = true;
414 }
415 }
416 DEBUG_LOG << "Plugin compatibility check Final: "
417 << (rv ? "ACCEPTED: " : "REJECTED: ") << metadata.name;
418 return rv;
419}
420
421std::string PluginHandler::FileListPath(std::string name) {
422 std::transform(name.begin(), name.end(), name.begin(), ::tolower);
423 return pluginsConfigDir() + SEP + name + ".files";
424}
425
426std::string PluginHandler::VersionPath(std::string name) {
427 std::transform(name.begin(), name.end(), name.begin(), ::tolower);
428 return pluginsConfigDir() + SEP + name + ".version";
429}
430
431std::string PluginHandler::ImportedMetadataPath(std::string name) {
432 ;
433 std::transform(name.begin(), name.end(), name.begin(), ::tolower);
434 return importsDir() + SEP + name + ".xml";
435}
436
437typedef std::unordered_map<std::string, std::string> pathmap_t;
438
443static pathmap_t getInstallPaths() {
444 using namespace std;
445
446 pathmap_t pathmap;
448 pathmap["bin"] = paths->UserBindir();
449 pathmap["lib"] = paths->UserLibdir();
450 pathmap["lib64"] = paths->UserLibdir();
451 pathmap["share"] = paths->UserDatadir();
452 return pathmap;
453}
454
455static void saveFilelist(std::string filelist, std::string name) {
456 using namespace std;
457 string listpath = PluginHandler::FileListPath(name);
458 ofstream diskfiles(listpath);
459 if (!diskfiles.is_open()) {
460 MESSAGE_LOG << "Cannot create installed files list.";
461 return;
462 }
463 diskfiles << filelist;
464}
465
466static void saveDirlist(std::string name) {
467 using namespace std;
468 string path = dirListPath(name);
469 ofstream dirs(path);
470 if (!dirs.is_open()) {
471 MESSAGE_LOG << "Cannot create installed files list.";
472 return;
473 }
474 pathmap_t pathmap = getInstallPaths();
475 unordered_map<string, string>::iterator it;
476 for (it = pathmap.begin(); it != pathmap.end(); it++) {
477 dirs << it->first << ": " << it->second << endl;
478 }
479}
480
481static void saveVersion(const std::string& name, const std::string& version) {
482 using namespace std;
483 string path = PluginHandler::VersionPath(name);
484 ofstream stream(path);
485 if (!stream.is_open()) {
486 MESSAGE_LOG << "Cannot create version file.";
487 return;
488 }
489 stream << version << endl;
490}
491
492static int copy_data(struct archive* ar, struct archive* aw) {
493 int r;
494 const void* buff;
495 size_t size;
496 la_int64_t offset;
497
498 while (true) {
499 r = archive_read_data_block(ar, &buff, &size, &offset);
500 if (r == ARCHIVE_EOF) return (ARCHIVE_OK);
501 if (r < ARCHIVE_OK) {
502 std::string s(archive_error_string(ar));
503 return (r);
504 }
505 r = archive_write_data_block(aw, buff, size, offset);
506 if (r < ARCHIVE_OK) {
507 std::string s(archive_error_string(aw));
508 MESSAGE_LOG << "Error copying install data: " << archive_error_string(aw);
509 return (r);
510 }
511 }
512}
513
514static bool win_entry_set_install_path(struct archive_entry* entry,
515 pathmap_t installPaths) {
516 using namespace std;
517
518 string path = archive_entry_pathname(entry);
519 bool is_library = false;
520
521 // Check # components, drop the single top-level path
522 int slashes = count(path.begin(), path.end(), '/');
523 if (slashes < 1) {
524 archive_entry_set_pathname(entry, "");
525 return true;
526 }
527 if (ocpn::startswith(path, "./")) {
528 path = path.substr(1);
529 }
530
531 // Remove top-level directory part
532 int slashpos = path.find_first_of('/', 1);
533 if (slashpos < 0) {
534 archive_entry_set_pathname(entry, "");
535 return true;
536 }
537
538 string prefix = path.substr(0, slashpos);
539 path = path.substr(prefix.size() + 1);
540
541 // Map remaining path to installation directory
542 if (ocpn::endswith(path, ".dll") || ocpn::endswith(path, ".exe")) {
543 slashpos = path.find_first_of('/');
544 path = path.substr(slashpos + 1);
545 path = installPaths["bin"] + "\\" + path;
546 is_library = true;
547 } else if (ocpn::startswith(path, "share")) {
548 // The "share" directory should be a direct sibling of "plugins" directory
549 wxFileName fn(installPaths["share"].c_str(),
550 ""); // should point to .../opencpn/plugins
551 fn.RemoveLastDir(); // should point to ".../opencpn
552 path = fn.GetFullPath().ToStdString() + path;
553 } else if (ocpn::startswith(path, "plugins")) {
554 slashpos = path.find_first_of('/');
555 // share path already ends in plugins/, drop prefix from archive entry.
556 path = path.substr(slashpos + 1);
557 path = installPaths["share"] + "\\" + path;
558
559 } else if (archive_entry_filetype(entry) == AE_IFREG) {
560 wxString msg("PluginHandler::Invalid install path on file: ");
561 msg += wxString(path.c_str());
562 DEBUG_LOG << msg;
563 return false;
564 }
565 if (is_library) {
566 wxFileName nm(path);
567 PluginLoader::MarkAsLoadable(nm.GetName().ToStdString());
568 }
569 wxString s(path);
570 s.Replace("/", "\\"); // std::regex_replace FTBS on gcc 4.8.4
571 s.Replace("\\\\", "\\");
572 archive_entry_set_pathname(entry, s.c_str());
573 return true;
574}
575
576static bool flatpak_entry_set_install_path(struct archive_entry* entry,
577 pathmap_t installPaths) {
578 using namespace std;
579
580 string path = archive_entry_pathname(entry);
581 int slashes = count(path.begin(), path.end(), '/');
582 if (slashes < 2) {
583 archive_entry_set_pathname(entry, "");
584 return true;
585 }
586 if (ocpn::startswith(path, "./")) {
587 path = path.substr(2);
588 }
589 int slashpos = path.find_first_of('/', 1);
590 string prefix = path.substr(0, slashpos);
591 path = path.substr(prefix.size() + 1);
592 slashpos = path.find_first_of('/');
593 string location = path.substr(0, slashpos);
594 string suffix = path.substr(slashpos + 1);
595 if (installPaths.find(location) == installPaths.end() &&
596 archive_entry_filetype(entry) == AE_IFREG) {
597 wxString msg("PluginHandler::Invalid install path on file: ");
598 msg += wxString(path.c_str());
599 DEBUG_LOG << msg;
600 return false;
601 }
602 string dest = installPaths[location] + "/" + suffix;
603 archive_entry_set_pathname(entry, dest.c_str());
604
606 if (dest.find(paths->UserLibdir()) != std::string::npos) {
607 wxFileName nm(path);
608 PluginLoader::MarkAsLoadable(nm.GetName().ToStdString());
609 }
610
611 return true;
612}
613
614static bool linux_entry_set_install_path(struct archive_entry* entry,
615 pathmap_t installPaths) {
616 using namespace std;
617
618 string path = archive_entry_pathname(entry);
619 int slashes = count(path.begin(), path.end(), '/');
620 if (slashes < 2) {
621 archive_entry_set_pathname(entry, "");
622 return true;
623 }
624
625 int slashpos = path.find_first_of('/', 1);
626 if (ocpn::startswith(path, "./"))
627 slashpos = path.find_first_of('/', 2); // skip the './'
628
629 string prefix = path.substr(0, slashpos);
630 path = path.substr(prefix.size() + 1);
631 if (ocpn::startswith(path, "usr/")) {
632 path = path.substr(strlen("usr/"));
633 }
634 if (ocpn::startswith(path, "local/")) {
635 path = path.substr(strlen("local/"));
636 }
637 slashpos = path.find_first_of('/');
638 string location = path.substr(0, slashpos);
639 string suffix = path.substr(slashpos + 1);
640 if (installPaths.find(location) == installPaths.end() &&
641 archive_entry_filetype(entry) == AE_IFREG) {
642 wxString msg("PluginHandler::Invalid install path on file: ");
643 msg += wxString(path.c_str());
644 DEBUG_LOG << msg;
645 return false;
646 }
647
648 bool is_library = false;
649 string dest = installPaths[location] + "/" + suffix;
650
651 if (g_bportable) {
652 // A data dir?
653 if (ocpn::startswith(location, "share") &&
654 ocpn::startswith(suffix, "opencpn/plugins/")) {
655 slashpos = suffix.find_first_of("opencpn/plugins/");
656 suffix = suffix.substr(16);
657
658 dest = g_BasePlatform->GetPrivateDataDir().ToStdString() + "/plugins/" +
659 suffix;
660 }
661 if (ocpn::startswith(location, "lib") &&
662 ocpn::startswith(suffix, "opencpn/")) {
663 suffix = suffix.substr(8);
664 dest = g_BasePlatform->GetPrivateDataDir().ToStdString() +
665 "/plugins/lib/" + suffix;
666 is_library = true;
667 }
668 } else {
669 if (ocpn::startswith(location, "lib") &&
670 ocpn::startswith(suffix, "opencpn/") && ocpn::endswith(suffix, ".so")) {
671 is_library = true;
672 }
673 }
674
675 if (is_library) {
676 wxFileName nm(suffix);
677 PluginLoader::MarkAsLoadable(nm.GetName().ToStdString());
678 }
679
680 archive_entry_set_pathname(entry, dest.c_str());
681 return true;
682}
683
684static bool apple_entry_set_install_path(struct archive_entry* entry,
685 pathmap_t installPaths) {
686 using namespace std;
687
688 const string base = PluginPaths::GetInstance()->Homedir() +
689 "/Library/Application Support/OpenCPN";
690
691 string path = archive_entry_pathname(entry);
692 if (ocpn::startswith(path, "./")) path = path.substr(2);
693 bool is_library = false;
694
695 string dest("");
696 size_t slashes = count(path.begin(), path.end(), '/');
697 if (slashes < 3) {
698 archive_entry_set_pathname(entry, "");
699 return true;
700 }
701 auto parts = split(path, "Contents/Resources");
702 if (parts.size() >= 2) {
703 dest = base + "/Contents/Resources" + parts[1];
704 }
705 if (dest == "") {
706 parts = split(path, "Contents/SharedSupport");
707 if (parts.size() >= 2) {
708 dest = base + "/Contents/SharedSupport" + parts[1];
709 }
710 }
711 if (dest == "") {
712 parts = split(path, "Contents/PlugIns");
713 if (parts.size() >= 2) {
714 dest = base + "/Contents/PlugIns" + parts[1];
715 is_library = true;
716 }
717 }
718 if (dest == "" && archive_entry_filetype(entry) == AE_IFREG) {
719 wxString msg("PluginHandler::Invalid install path on file: ");
720 msg += wxString(path.c_str());
721 DEBUG_LOG << msg;
722 return false;
723 }
724 archive_entry_set_pathname(entry, dest.c_str());
725 if (is_library) {
726 wxFileName nm(dest);
727 PluginLoader::MarkAsLoadable(nm.GetName().ToStdString());
728 }
729
730 return true;
731}
732
733static bool android_entry_set_install_path(struct archive_entry* entry,
734 pathmap_t installPaths) {
735 using namespace std;
736
737 bool is_library = false;
738 string path = archive_entry_pathname(entry);
739 int slashes = count(path.begin(), path.end(), '/');
740 if (slashes < 2) {
741 archive_entry_set_pathname(entry, "");
742 return true;
743 ;
744 }
745
746 int slashpos = path.find_first_of('/', 1);
747 if (ocpn::startswith(path, "./"))
748 slashpos = path.find_first_of('/', 2); // skip the './'
749
750 string prefix = path.substr(0, slashpos);
751 path = path.substr(prefix.size() + 1);
752 if (ocpn::startswith(path, "usr/")) {
753 path = path.substr(strlen("usr/"));
754 }
755 if (ocpn::startswith(path, "local/")) {
756 path = path.substr(strlen("local/"));
757 }
758 slashpos = path.find_first_of('/');
759 string location = path.substr(0, slashpos);
760 string suffix = path.substr(slashpos + 1);
761 if (installPaths.find(location) == installPaths.end() &&
762 archive_entry_filetype(entry) == AE_IFREG) {
763 wxString msg("PluginHandler::Invalid install path on file: ");
764 msg += wxString(path.c_str());
765 DEBUG_LOG << msg;
766 return false;
767 }
768
769 if ((location == "lib") && ocpn::startswith(suffix, "opencpn")) {
770 auto parts = split(suffix, "/");
771 if (parts.size() == 2) suffix = parts[1];
772 is_library = true;
773 }
774
775 if ((location == "share") && ocpn::startswith(suffix, "opencpn")) {
776 auto parts = split(suffix, "opencpn/");
777 if (parts.size() == 2) suffix = parts[1];
778 }
779
781 string dest = installPaths[location] + "/" + suffix;
782
783 archive_entry_set_pathname(entry, dest.c_str());
784 if (is_library) {
785 wxFileName nm(suffix);
786 PluginLoader::MarkAsLoadable(nm.GetName().ToStdString());
787 }
788 return true;
789}
790
791static bool entry_set_install_path(struct archive_entry* entry,
792 pathmap_t installPaths) {
793 const std::string src = archive_entry_pathname(entry);
794 bool rv;
795#ifdef __ANDROID__
796 rv = android_entry_set_install_path(entry, installPaths);
797#else
798 const auto osSystemId = wxPlatformInfo::Get().GetOperatingSystemId();
799 if (g_BasePlatform->isFlatpacked()) {
800 rv = flatpak_entry_set_install_path(entry, installPaths);
801 } else if (osSystemId & wxOS_UNIX_LINUX) {
802 rv = linux_entry_set_install_path(entry, installPaths);
803 } else if (osSystemId & wxOS_WINDOWS) {
804 rv = win_entry_set_install_path(entry, installPaths);
805 } else if (osSystemId & wxOS_MAC) {
806 rv = apple_entry_set_install_path(entry, installPaths);
807 } else {
808 MESSAGE_LOG << "set_install_path() invoked, unsupported platform "
809 << wxPlatformInfo::Get().GetOperatingSystemDescription();
810 rv = false;
811 }
812#endif
813 const std::string dest = archive_entry_pathname(entry);
814 if (rv) {
815 if (dest.size()) {
816 DEBUG_LOG << "Installing " << src << " into " << dest << std::endl;
817 }
818 }
819 return rv;
820}
821
822bool PluginHandler::ArchiveCheck(int r, const char* msg, struct archive* a) {
823 if (r < ARCHIVE_OK) {
824 std::string s(msg);
825
826 if (archive_error_string(a)) s = s + ": " + archive_error_string(a);
827 MESSAGE_LOG << s;
828 last_error_msg = s;
829 }
830 return r >= ARCHIVE_WARN;
831}
832
833bool PluginHandler::ExplodeTarball(struct archive* src, struct archive* dest,
834 std::string& filelist,
835 const std::string& metadata_path,
836 bool only_metadata) {
837 struct archive_entry* entry = 0;
838 pathmap_t pathmap = getInstallPaths();
839 bool is_metadata_ok = false;
840 while (true) {
841 int r = archive_read_next_header(src, &entry);
842 if (r == ARCHIVE_EOF) {
843 if (!is_metadata_ok) {
844 MESSAGE_LOG << "Plugin tarball does not contain metadata.xml";
845 }
846 return is_metadata_ok;
847 }
848 if (!ArchiveCheck(r, "archive read header error", src)) {
849 return false;
850 }
851 std::string path = archive_entry_pathname(entry);
852 bool is_metadata = std::string::npos != path.find("metadata.xml");
853 if (is_metadata) {
854 is_metadata_ok = true;
855 if (metadata_path == "") {
856 continue;
857 } else {
858 archive_entry_set_pathname(entry, metadata_path.c_str());
859 DEBUG_LOG << "Extracted metadata.xml to " << metadata_path;
860 }
861 } else if (!entry_set_install_path(entry, pathmap))
862 continue;
863 if (strlen(archive_entry_pathname(entry)) == 0) {
864 continue;
865 }
866 if (!is_metadata && only_metadata) {
867 continue;
868 }
869 if (!is_metadata) {
870 filelist.append(std::string(archive_entry_pathname(entry)) + "\n");
871 }
872 r = archive_write_header(dest, entry);
873 ArchiveCheck(r, "archive write install header error", dest);
874 if (r >= ARCHIVE_OK && archive_entry_size(entry) > 0) {
875 r = copy_data(src, dest);
876 if (!ArchiveCheck(r, "archive copy data error", dest)) {
877 return false;
878 }
879 }
880 r = archive_write_finish_entry(dest);
881 if (!ArchiveCheck(r, "archive finish write error", dest)) {
882 return false;
883 }
884 }
885 return false; // notreached
886}
887
888/*
889 * Extract tarball into platform-specific user directories.
890 *
891 * The installed tarball has paths like topdir/dest/suffix_path... e. g.
892 * oesenc_pi_ubuntu_10_64/usr/local/share/opencpn/plugins/oesenc_pi/README.
893 * In this path, the topdir part must exist but is discarded. Next parts
894 * being being standard prefixes like /usr/local or /usr are also
895 * discarded. The remaining path (here share) is mapped to a user
896 * directory. On linux, it ends up in ~/.local/share. The suffix
897 * part is then installed as-is into this directory.
898 *
899 * Windows tarballs has dll and binary files in the top directory. They
900 * go to winInstallDir/Program Files. Message catalogs exists under a
901 * share/ toplevel directory, they go in winInstallDir/share. The
902 * plugin data is installed under winInstallDir/plugins/<plugin name>,
903 * and must be looked up by the plugins using GetPluginDataDir(plugin);
904 * Windows requires that PATH is set to include the binary dir and tha
905 * a bindtextdomain call is invoked to define the message catalog paths.
906 *
907 * For linux, the expected destinations are bin, lib and share.
908 *
909 * @param path path to tarball
910 * @param filelist: On return contains a list of files installed.
911 * @param metadata_path: if non-empty, location where to store metadata,
912 * @param only_metadata: If true don't install any files, just extract
913 * metadata.
914 * @return true if tarball contains metadata.xml file, false otherwise.
915 *
916 */
917bool PluginHandler::ExtractTarball(const std::string path,
918 std::string& filelist,
919 const std::string metadata_path,
920 bool only_metadata) {
921 struct archive* src = archive_read_new();
922 archive_read_support_filter_gzip(src);
923 archive_read_support_format_tar(src);
924 int r = archive_read_open_filename(src, path.c_str(), 10240);
925 if (r != ARCHIVE_OK) {
926 std::ostringstream os;
927 os << "Cannot read installation tarball: " << path;
928 MESSAGE_LOG << os.str();
929 last_error_msg = os.str();
930 return false;
931 }
932 struct archive* dest = archive_write_disk_new();
933 archive_write_disk_set_options(dest, ARCHIVE_EXTRACT_TIME);
934 bool ok = ExplodeTarball(src, dest, filelist, metadata_path, only_metadata);
935 archive_read_free(src);
936 archive_write_free(dest);
937 return ok;
938}
939
941 static PluginHandler* instance = 0;
942 if (!instance) {
943 instance = new (PluginHandler);
944 }
945 return instance;
946}
947
948bool PluginHandler::IsPluginWritable(std::string name) {
949 if (isRegularFile(PluginHandler::FileListPath(name).c_str())) {
950 return true;
951 }
952 auto loader = PluginLoader::GetInstance();
953 return PlugInIxByName(name, loader->GetPlugInArray()) == -1;
954}
955
956static std::string computeMetadataPath() {
957 std::string path = g_BasePlatform->GetPrivateDataDir().ToStdString();
958 path += SEP;
959 path += "ocpn-plugins.xml";
960 if (ocpn::exists(path)) {
961 return path;
962 }
963
964 // If default location for composit plugin metadata is not found,
965 // we look in the plugin cache directory, which will normally contain
966 // he last "master" catalog downloaded
967 path = ocpn::lookup_metadata();
968 if (path != "") {
969 return path;
970 }
971
972 // And if that does not work, use the empty metadata file found in the
973 // distribution "data" directory
974 path = g_BasePlatform->GetSharedDataDir();
975 path += SEP;
976 path += "ocpn-plugins.xml";
977 if (!ocpn::exists(path)) {
978 MESSAGE_LOG << "Non-existing plugins file: " << path;
979 }
980 return path;
981}
982
983static void parseMetadata(const std::string path, CatalogCtx& ctx) {
984 using namespace std;
985
986 MESSAGE_LOG << "PluginHandler: using metadata path: " << path;
987 ctx.depth = 0;
988 if (!ocpn::exists(path)) {
989 MESSAGE_LOG << "Non-existing plugins metadata file: " << path;
990 return;
991 }
992 ifstream ifpath(path);
993 std::string xml((istreambuf_iterator<char>(ifpath)),
994 istreambuf_iterator<char>());
995 ParseCatalog(xml, &ctx);
996}
997
998bool PluginHandler::InstallPlugin(const std::string& path,
999 std::string& filelist,
1000 const std::string metadata_path,
1001 bool only_metadata) {
1002 if (!ExtractTarball(path, filelist, metadata_path, only_metadata)) {
1003 std::ostringstream os;
1004 os << "Cannot unpack plugin tarball at : " << path;
1005 MESSAGE_LOG << os.str();
1006 if (filelist != "") Cleanup(filelist, "unknown_name");
1007 last_error_msg = os.str();
1008 return false;
1009 }
1010 if (only_metadata) {
1011 return true;
1012 }
1013 struct CatalogCtx ctx;
1014 std::ifstream istream(metadata_path);
1015 std::stringstream buff;
1016 buff << istream.rdbuf();
1017
1018 auto xml = std::string("<plugins>") + buff.str() + "</plugins>";
1019 ParseCatalog(xml, &ctx);
1020 auto name = ctx.plugins[0].name;
1021 auto version = ctx.plugins[0].version;
1022 saveFilelist(filelist, name);
1023 saveDirlist(name);
1024 saveVersion(name, version);
1025
1026 return true;
1027}
1028
1030 if (metadataPath.size() > 0) {
1031 return metadataPath;
1032 }
1033 metadataPath = computeMetadataPath();
1034 DEBUG_LOG << "Using metadata path: " << metadataPath;
1035 return metadataPath;
1036}
1037
1039 std::string path = g_BasePlatform->GetPrivateDataDir().ToStdString();
1040 path += SEP;
1041 return path + "ocpn-plugins.xml";
1042}
1043
1044const std::map<std::string, int> PluginHandler::GetCountByTarget() {
1045 auto plugins = GetInstalled();
1046 auto a = GetAvailable();
1047 plugins.insert(plugins.end(), a.begin(), a.end());
1048 std::map<std::string, int> count_by_target;
1049 for (const auto& p : plugins) {
1050 if (p.target == "") {
1051 continue; // Built-in plugins like dashboard et. al.
1052 }
1053 auto key = p.target + ":" + p.target_version;
1054 if (count_by_target.find(key) == count_by_target.end()) {
1055 count_by_target[key] = 1;
1056 } else {
1057 count_by_target[key] += 1;
1058 }
1059 }
1060 return count_by_target;
1061}
1062
1063std::vector<std::string> PluginHandler::GetImportPaths() {
1064 return glob_dir(importsDir(), "*.xml");
1065}
1066
1067void PluginHandler::CleanupFiles(const std::string& manifestFile,
1068 const std::string& plugname) {
1069 std::ifstream diskfiles(manifestFile);
1070 if (diskfiles.is_open()) {
1071 std::stringstream buffer;
1072 buffer << diskfiles.rdbuf();
1073 PluginHandler::Cleanup(buffer.str(), plugname);
1074 }
1075}
1076
1078static void PurgeEmptyDirs(const std::string& root) {
1079 if (!wxFileName::IsDirWritable(root)) return;
1080 if (ocpn::tolower(root).find("opencpn") == std::string::npos) return;
1081 wxDir rootdir(root);
1082 if (!rootdir.IsOpened()) return;
1083 wxString dirname;
1084 bool cont = rootdir.GetFirst(&dirname, "", wxDIR_DIRS);
1085 while (cont) {
1086 PurgeEmptyDirs((rootdir.GetNameWithSep() + dirname).ToStdString());
1087 cont = rootdir.GetNext(&dirname);
1088 }
1089 rootdir.Close();
1090 rootdir.Open(root);
1091 if (!(rootdir.HasFiles() || rootdir.HasSubDirs())) {
1092 wxFileName::Rmdir(rootdir.GetName());
1093 }
1094}
1095
1096void PluginHandler::Cleanup(const std::string& filelist,
1097 const std::string& plugname) {
1098 MESSAGE_LOG << "Cleaning up failed install of " << plugname;
1099
1100 std::vector<std::string> paths = LoadLinesFromFile(filelist);
1101 for (const auto& path : paths) {
1102 if (isRegularFile(path.c_str())) {
1103 int r = remove(path.c_str());
1104 if (r != 0) {
1105 MESSAGE_LOG << "Cannot remove file " << path << ": " << strerror(r);
1106 }
1107 }
1108 }
1109 for (const auto& path : paths) PurgeEmptyDirs(path);
1110
1111 std::string path = PluginHandler::FileListPath(plugname);
1112 if (ocpn::exists(path)) remove(path.c_str());
1113
1114 // Best effort tries, failures are non-critical
1115 remove(dirListPath(plugname).c_str());
1116 remove(PluginHandler::VersionPath(plugname).c_str());
1117}
1118
1123std::vector<PluginMetadata> PluginHandler::getCompatiblePlugins() {
1125 struct metadata_compare {
1126 bool operator()(const PluginMetadata& lhs,
1127 const PluginMetadata& rhs) const {
1128 return lhs.key() < rhs.key();
1129 }
1130 };
1131
1132 std::vector<PluginMetadata> returnArray;
1133
1134 std::set<PluginMetadata, metadata_compare> unique_plugins;
1135 for (const auto& plugin : GetAvailable()) {
1136 unique_plugins.insert(plugin);
1137 }
1138 for (const auto& plugin : unique_plugins) {
1139 if (IsCompatible(plugin)) {
1140 returnArray.push_back(plugin);
1141 }
1142 }
1143 return returnArray;
1144}
1145
1146const std::vector<PluginMetadata> PluginHandler::GetAvailable() {
1147 using namespace std;
1148 CatalogCtx* ctx;
1149
1150 auto catalogHandler = CatalogHandler::GetInstance();
1151
1152 ctx = catalogHandler->GetActiveCatalogContext();
1153 auto status = catalogHandler->GetCatalogStatus();
1154
1155 if (status == CatalogHandler::ServerStatus::OK) {
1156 catalogData.undef = false;
1157 catalogData.version = ctx->version;
1158 catalogData.date = ctx->date;
1159 }
1160 return ctx->plugins;
1161}
1162
1163std::vector<std::string> PluginHandler::GetInstalldataPlugins() {
1164 std::vector<std::string> names;
1165 fs::path dirpath(PluginsInstallDataPath());
1166 for (const auto& entry : fs::directory_iterator(dirpath)) {
1167 const std::string name(entry.path().filename().string());
1168 if (ocpn::endswith(name, ".files"))
1169 names.push_back(ocpn::split(name.c_str(), ".")[0]);
1170 }
1171 return names;
1172}
1173
1174const std::vector<PluginMetadata> PluginHandler::GetInstalled() {
1175 using namespace std;
1176 vector<PluginMetadata> plugins;
1177
1178 auto loader = PluginLoader::GetInstance();
1179 for (unsigned int i = 0; i < loader->GetPlugInArray()->GetCount(); i += 1) {
1180 const PlugInContainer* p = loader->GetPlugInArray()->Item(i);
1181 PluginMetadata plugin;
1182 auto name = string(p->m_common_name);
1183 // std::transform(name.begin(), name.end(), name.begin(), ::tolower);
1184 plugin.name = name;
1185 std::stringstream ss;
1186 ss << p->m_version_major << "." << p->m_version_minor;
1187 plugin.version = ss.str();
1188 plugin.readonly = !IsPluginWritable(plugin.name);
1189 string path = PluginHandler::VersionPath(plugin.name);
1190 if (path != "" && wxFileName::IsFileReadable(path)) {
1191 std::ifstream stream;
1192 stream.open(path, ifstream::in);
1193 stream >> plugin.version;
1194 }
1195 plugins.push_back(plugin);
1196 }
1197 return plugins;
1198}
1199
1201 auto loader = PluginLoader::GetInstance();
1202 ssize_t ix = PlugInIxByName(pm.name, loader->GetPlugInArray());
1203 if (ix == -1) return; // no such plugin
1204
1205 auto plugins = *loader->GetPlugInArray();
1206 plugins[ix]->m_managed_metadata = pm;
1207}
1208
1209bool PluginHandler::InstallPlugin(PluginMetadata plugin, std::string path) {
1210 std::string filelist;
1211 if (!ExtractTarball(path, filelist)) {
1212 std::ostringstream os;
1213 os << "Cannot unpack plugin: " << plugin.name << " at " << path;
1214 MESSAGE_LOG << os.str();
1215 last_error_msg = os.str();
1216 PluginHandler::Cleanup(filelist, plugin.name);
1217 return false;
1218 }
1219 saveFilelist(filelist, plugin.name);
1220 saveDirlist(plugin.name);
1221 saveVersion(plugin.name, plugin.version);
1222 return true;
1223}
1224
1226 std::string path = tmpfile_path();
1227 if (path.empty()) {
1228 MESSAGE_LOG << "Cannot create temporary file";
1229 path = "";
1230 return false;
1231 }
1232 std::ofstream stream;
1233 stream.open(path.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
1234 DEBUG_LOG << "Downloading: " << plugin.name << std::endl;
1235 auto downloader = Downloader(plugin.tarball_url);
1236 downloader.download(&stream);
1237
1238 return InstallPlugin(plugin, path);
1239}
1240
1241bool PluginHandler::InstallPlugin(const std::string& path) {
1242 PluginMetadata metadata;
1243 if (!ExtractMetadata(path, metadata)) {
1244 MESSAGE_LOG << "Cannot extract metadata from tarball";
1245 return false;
1246 }
1247 return InstallPlugin(metadata, path);
1248}
1249
1250bool PluginHandler::ExtractMetadata(const std::string& path,
1251 PluginMetadata& metadata) {
1252 std::string filelist;
1253 std::string temp_path = tmpfile_path();
1254 if (!ExtractTarball(path, filelist, temp_path, true)) {
1255 std::ostringstream os;
1256 os << "Cannot unpack plugin " << metadata.name << " tarball at: " << path;
1257 MESSAGE_LOG << os.str();
1258 if (filelist != "") Cleanup(filelist, "unknown_name");
1259 last_error_msg = os.str();
1260 return false;
1261 }
1262 if (!isRegularFile(temp_path.c_str())) {
1263 // This could happen if the tarball does not contain the metadata.xml file
1264 // or the metadata.xml file could not be extracted.
1265 return false;
1266 }
1267
1268 struct CatalogCtx ctx;
1269 std::ifstream istream(temp_path);
1270 std::stringstream buff;
1271 buff << istream.rdbuf();
1272 int r = remove(temp_path.c_str());
1273 if (r != 0) {
1274 MESSAGE_LOG << "Cannot remove file " << temp_path << ":" << strerror(r);
1275 }
1276 auto xml = std::string("<plugins>") + buff.str() + "</plugins>";
1277 ParseCatalog(xml, &ctx);
1278 metadata = ctx.plugins[0];
1279 if (metadata.name.empty()) {
1280 MESSAGE_LOG << "Plugin metadata is empty";
1281 }
1282 return !metadata.name.empty();
1283}
1284
1285bool PluginHandler::ClearInstallData(const std::string plugin_name) {
1286 auto ix = PlugInIxByName(plugin_name,
1287 PluginLoader::GetInstance()->GetPlugInArray());
1288 if (ix != -1) {
1289 MESSAGE_LOG << "Attempt to remove installation data for loaded plugin";
1290 return false;
1291 }
1292 return DoClearInstallData(plugin_name);
1293}
1294
1295bool PluginHandler::DoClearInstallData(const std::string plugin_name) {
1296 std::string path = PluginHandler::FileListPath(plugin_name);
1297 if (!ocpn::exists(path)) {
1298 MESSAGE_LOG << "Cannot find installation data for " << plugin_name << " ("
1299 << path << ")";
1300 return false;
1301 }
1302 std::vector<std::string> plug_paths = LoadLinesFromFile(path);
1303 for (const auto& p : plug_paths) {
1304 if (isRegularFile(p.c_str())) {
1305 int r = remove(p.c_str());
1306 if (r != 0) {
1307 MESSAGE_LOG << "Cannot remove file " << p << ": " << strerror(r);
1308 }
1309 }
1310 }
1311 for (const auto& p : plug_paths) PurgeEmptyDirs(p);
1312 int r = remove(path.c_str());
1313 if (r != 0) {
1314 MESSAGE_LOG << "Cannot remove file " << path << ": " << strerror(r);
1315 }
1316 // Best effort tries, failures are OK.
1317 remove(dirListPath(plugin_name).c_str());
1318 remove(PluginHandler::VersionPath(plugin_name).c_str());
1319 remove(PluginHandler::ImportedMetadataPath(plugin_name).c_str());
1320 return true;
1321}
1322
1323bool PluginHandler::Uninstall(const std::string plugin) {
1324 using namespace std;
1325
1326 auto loader = PluginLoader::GetInstance();
1327 auto ix = PlugInIxByName(plugin, loader->GetPlugInArray());
1328 if (ix < 0) {
1329 MESSAGE_LOG << "trying to Uninstall non-existing plugin " << plugin;
1330 return false;
1331 }
1332 auto pic = loader->GetPlugInArray()->Item(ix);
1333
1334 // Capture library file name before pic dies.
1335 string libfile = pic->m_plugin_file.ToStdString();
1336 loader->UnLoadPlugIn(ix);
1337
1338 bool ok = DoClearInstallData(plugin);
1339
1340 // If this is an orphan plugin, there may be no installation record
1341 // So make sure that the library file (.so/.dylib/.dll) is removed
1342 // as a minimum best effort requirement
1343 if (isRegularFile(libfile.c_str())) {
1344 remove(libfile.c_str());
1345 }
1346 loader->MarkAsLoadable(libfile);
1347
1348 return ok;
1349}
1350
1351using PluginMap = std::unordered_map<std::string, std::vector<std::string>>;
1352
1358static std::string FindMatchingDataDir(std::regex name_re) {
1359 using namespace std;
1360 wxString data_dirs(g_BasePlatform->GetPluginDataPath());
1361 wxStringTokenizer tokens(data_dirs, ";");
1362 while (tokens.HasMoreTokens()) {
1363 auto token = tokens.GetNextToken();
1364 wxFileName path(token);
1365 wxDir dir(path.GetFullPath());
1366 if (dir.IsOpened()) {
1367 wxString filename;
1368 bool cont = dir.GetFirst(&filename, "", wxDIR_DIRS);
1369 while (cont) {
1370 smatch sm;
1371 string s(filename);
1372 if (regex_search(s, sm, name_re)) {
1373 stringstream ss;
1374 for (auto c : sm) ss << c;
1375 return ss.str();
1376 }
1377 cont = dir.GetNext(&filename);
1378 }
1379 }
1380 }
1381 return "";
1382}
1383
1388static std::string FindMatchingLibFile(std::regex name_re) {
1389 using namespace std;
1390 for (const auto& lib : PluginPaths::GetInstance()->Libdirs()) {
1391 wxDir dir(lib);
1392 wxString filename;
1393 bool cont = dir.GetFirst(&filename, "", wxDIR_FILES);
1394 while (cont) {
1395 smatch sm;
1396 string s(filename);
1397 if (regex_search(s, sm, name_re)) {
1398 stringstream ss;
1399 for (auto c : sm) ss << c;
1400 return ss.str();
1401 }
1402 cont = dir.GetNext(&filename);
1403 }
1404 }
1405 return "";
1406}
1407
1409static std::string PluginNameCase(const std::string& name) {
1410 using namespace std;
1411 const string lc_name = ocpn::tolower(name);
1412 regex name_re(lc_name, regex_constants::icase | regex_constants::ECMAScript);
1413
1414 // Look for matching plugin in list of installed and available.
1415 // This often fails since the lists are not yet available when
1416 // plugins are loaded, but is otherwise a safe bet.
1417 for (const auto& plugin : PluginHandler::GetInstance()->GetInstalled()) {
1418 if (ocpn::tolower(plugin.name) == lc_name) return plugin.name;
1419 }
1420 for (const auto& plugin : PluginHandler::GetInstance()->GetAvailable()) {
1421 if (ocpn::tolower(plugin.name) == lc_name) return plugin.name;
1422 }
1423
1424 string match = FindMatchingDataDir(name_re);
1425 if (match != "") return match;
1426
1427 match = FindMatchingLibFile(name_re);
1428 return match != "" ? match : name;
1429}
1430
1432static void LoadPluginMapFile(PluginMap& map, const std::string& path) {
1433 std::ifstream f;
1434 f.open(path);
1435 if (f.fail()) {
1436 MESSAGE_LOG << "Cannot open " << path << ": " << strerror(errno);
1437 return;
1438 }
1439 std::stringstream buf;
1440 buf << f.rdbuf();
1441 auto filelist = ocpn::split(buf.str().c_str(), "\n");
1442 for (auto& file : filelist) {
1443 file = wxFileName(file).GetFullName().ToStdString();
1444 }
1445
1446 // key is basename with removed .files suffix and correct case.
1447 auto key = wxFileName(path).GetFullName().ToStdString();
1448 key = ocpn::split(key.c_str(), ".")[0];
1449 key = PluginNameCase(key);
1450 map[key] = filelist;
1451}
1452
1454static void LoadPluginMap(PluginMap& map) {
1455 map.clear();
1457 if (!root.IsOpened()) return;
1458 wxString filename;
1459 bool cont = root.GetFirst(&filename, "*.files", wxDIR_FILES);
1460 while (cont) {
1461 auto path = root.GetNameWithSep() + filename;
1462 LoadPluginMapFile(map, path.ToStdString());
1463 cont = root.GetNext(&filename);
1464 }
1465}
1466
1467std::string PluginHandler::GetPluginByLibrary(const std::string& filename) {
1468 auto basename = wxFileName(filename).GetFullName().ToStdString();
1469 if (FilesByPlugin.size() == 0) LoadPluginMap(FilesByPlugin);
1470 for (const auto& it : FilesByPlugin) {
1471 auto found = std::find(it.second.begin(), it.second.end(), basename);
1472 if (found != it.second.end()) return it.first;
1473 }
1474 return "";
1475}
1476
1478 // Look for the desired file
1479 wxURI uri(wxString(plugin.tarball_url.c_str()));
1480 wxFileName fn(uri.GetPath());
1481 wxString tarballFile = fn.GetFullName();
1482 std::string cacheFile = ocpn::lookup_tarball(tarballFile);
1483
1484#ifdef __WXOSX__
1485 // Depending on the browser settings, MacOS will sometimes automatically
1486 // de-compress the tar.gz file, leaving a simple ".tar" file in its expected
1487 // place. Check for this case, and "do the right thing"
1488 if (cacheFile == "") {
1489 fn.ClearExt();
1490 wxFileName fn1(fn.GetFullName());
1491 if (fn1.GetExt().IsSameAs("tar")) {
1492 tarballFile = fn.GetFullName();
1493 cacheFile = ocpn::lookup_tarball(tarballFile);
1494 }
1495 }
1496#endif
1497
1498 if (cacheFile != "") {
1499 MESSAGE_LOG << "Installing " << tarballFile << " from local cache";
1500 bool bOK = InstallPlugin(plugin, cacheFile);
1501 if (!bOK) {
1502 evt_download_failed.Notify(cacheFile);
1503 return false;
1504 }
1505 evt_download_ok.Notify(plugin.name + " " + plugin.version);
1506 return true;
1507 }
1508 return false;
1509}
BasePlatform * g_BasePlatform
points to g_platform, handles brain-dead MS linker.
Basic platform specific support utilities without GUI deps.
Plugin catalog management: Build the runtime catalog, handling downloads as required.
Datatypes and methods to parse ocpn-plugins.xml XML data, either complete catalog or a single plugin.
wxString & DefaultPrivateDataDir()
Return dir path for opencpn.log, etc., does not respect -c option.
wxString GetPluginDataPath()
Return ';'-separated list of base directories for plugin data.
wxString & GetPrivateDataDir()
Return dir path for opencpn.log, etc., respecting -c cli option.
Internal helper wrapping host OS and version.
Default downloader, usable in a CLI context.
Definition downloader.h:35
void Notify() override
Notify all listeners, no data supplied.
Host ABI encapsulation and plugin compatibility checks.
Data for a loaded plugin, including dl-loaded library.
wxString m_common_name
A common name string for the plugin.
Handle plugin install from remote repositories and local operations to Uninstall and list plugins.
static std::vector< std::string > GetImportPaths()
List of paths for imported plugins metadata.
bool Uninstall(const std::string plugin)
Uninstall an installed and loaded plugin.
const std::vector< PluginMetadata > GetInstalled()
Return list of all installed and loaded plugins.
const std::map< std::string, int > GetCountByTarget()
Map of available plugin targets -> number of occurences.
static void Cleanup(const std::string &filelist, const std::string &plugname)
Cleanup failed installation attempt using filelist for plugin.
bool IsPluginWritable(std::string name)
Check if given plugin can be installed/updated.
std::vector< PluginMetadata > getCompatiblePlugins()
Return list of available, unique and compatible plugins from configured XML catalog.
static std::string ImportedMetadataPath(std::string name)
Return path to imported metadata for given plugin.
static std::string VersionPath(std::string name)
Return path to file containing version for given plugin.
std::string GetMetadataPath()
Return path to metadata XML file.
std::vector< std::string > GetInstalldataPlugins()
Return list of installed plugins lower case names, not necessarily loaded.
bool InstallPlugin(PluginMetadata plugin)
Download and install a new, not installed plugin.
const std::vector< PluginMetadata > GetAvailable()
Update catalog and return list of available, not installed plugins.
bool ClearInstallData(const std::string plugin_name)
Remove installation data for not loaded plugin.
static bool IsCompatible(const PluginMetadata &metadata, const char *os=PKG_TARGET, const char *os_version=PKG_TARGET_VERSION)
Return true if given plugin is loadable on given os/version.
std::string GetUserMetadataPath()
Return path to user, writable metadata XML file.
static std::string FileListPath(std::string name)
Return path to installation manifest for given plugin.
EventVar evt_download_failed
Notified with plugin name after failed download attempt.
void SetInstalledMetadata(const PluginMetadata &pm)
Set metadata for an installed plugin.
bool ExtractMetadata(const std::string &path, PluginMetadata &metadata)
Extract metadata in given tarball path.
bool InstallPluginFromCache(PluginMetadata plugin)
Install plugin tarball from local cache.
std::string GetPluginByLibrary(const std::string &filename)
Return plugin containing given filename or "" if not found.
static PluginHandler * GetInstance()
Singleton factory.
static std::string PluginsInstallDataPath()
Return base directory for installation data.
EventVar evt_download_ok
Notified with plugin name + version string after successful download from repository.
static void MarkAsLoadable(const std::string &library_path)
Mark a library file (complete path) as loadable i.
Accessors for various paths to install plugins and their data.
std::string UserLibdir()
The single, user-writable directory for installing .dll files.
std::string Homedir() const
home directory, convenience stuff.
std::string UserDatadir()
The single, user-writable common parent for plugin data directories, typically ending in 'plugins'.
static PluginPaths * GetInstance()
Return the singleton instance.
std::string UserBindir()
The single, user-writable directory for installing helper binaries.
Plugin ABI encapsulation.
Global variables reflecting command line options and arguments.
Global variables stored in configuration file.
Handle downloading of files from remote urls.
Enhanced logging interface on top of wx/log.h.
std::string lookup_tarball(const char *uri)
Get path to tarball in cache for given filename.
std::string lookup_metadata(const char *name)
Get metadata path for a given name defaulting to ocpn-plugins.xml)
bool startswith(const std::string &str, const std::string &prefix)
Return true if s starts with given prefix.
std::string tolower(const std::string &input)
Return copy of s with all characters converted to lower case.
std::vector< std::string > split(const char *token_string, const std::string &delimiter)
Return vector of items in s separated by delimiter.
bool endswith(const std::string &str, const std::string &suffix)
Return true if s ends with given suffix.
bool exists(const std::string &name)
void mkdir(const std::string path)
Miscellaneous utilities, many of which string related.
Downloaded plugins cache.
Plugin remote repositories installation and Uninstall/list operations.
Low level code to load plugins from disk, notably the PluginLoader class.
Plugin installation and data paths support.
std::vector< const PlugInData * > GetInstalled()
Return sorted list of all installed plugins.
The result from parsing the xml catalog i.
Plugin metadata, reflects the xml format directly.
bool readonly
Can plugin be removed?
Versions uses a modified semantic versioning scheme: major.minor.revision.post-tag+build.
static SemanticVersion parse(std::string s)
Parse a version string, sets major == -1 on errors.