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