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, 21);
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 loader->MarkAsLoadable(libfile);
1354
1355 return ok;
1356}
1357
1358using PluginMap = std::unordered_map<std::string, std::vector<std::string>>;
1359
1365static std::string FindMatchingDataDir(std::regex name_re) {
1366 using namespace std;
1367 wxString data_dirs(g_BasePlatform->GetPluginDataPath());
1368 wxStringTokenizer tokens(data_dirs, ";");
1369 while (tokens.HasMoreTokens()) {
1370 auto token = tokens.GetNextToken();
1371 wxFileName path(token);
1372 wxDir dir(path.GetFullPath());
1373 if (dir.IsOpened()) {
1374 wxString filename;
1375 bool cont = dir.GetFirst(&filename, "", wxDIR_DIRS);
1376 while (cont) {
1377 smatch sm;
1378 string s(filename);
1379 if (regex_search(s, sm, name_re)) {
1380 stringstream ss;
1381 for (auto c : sm) ss << c;
1382 return ss.str();
1383 }
1384 cont = dir.GetNext(&filename);
1385 }
1386 }
1387 }
1388 return "";
1389}
1390
1395static std::string FindMatchingLibFile(std::regex name_re) {
1396 using namespace std;
1397 for (const auto& lib : PluginPaths::GetInstance()->Libdirs()) {
1398 wxDir dir(lib);
1399 wxString filename;
1400 bool cont = dir.GetFirst(&filename, "", wxDIR_FILES);
1401 while (cont) {
1402 smatch sm;
1403 string s(filename);
1404 if (regex_search(s, sm, name_re)) {
1405 stringstream ss;
1406 for (auto c : sm) ss << c;
1407 return ss.str();
1408 }
1409 cont = dir.GetNext(&filename);
1410 }
1411 }
1412 return "";
1413}
1414
1416static std::string PluginNameCase(const std::string& name) {
1417 using namespace std;
1418 const string lc_name = ocpn::tolower(name);
1419 regex name_re(lc_name, regex_constants::icase | regex_constants::ECMAScript);
1420
1421 // Look for matching plugin in list of installed and available.
1422 // This often fails since the lists are not yet available when
1423 // plugins are loaded, but is otherwise a safe bet.
1424 for (const auto& plugin : PluginHandler::GetInstance()->GetInstalled()) {
1425 if (ocpn::tolower(plugin.name) == lc_name) return plugin.name;
1426 }
1427 for (const auto& plugin : PluginHandler::GetInstance()->GetAvailable()) {
1428 if (ocpn::tolower(plugin.name) == lc_name) return plugin.name;
1429 }
1430
1431 string match = FindMatchingDataDir(name_re);
1432 if (match != "") return match;
1433
1434 match = FindMatchingLibFile(name_re);
1435 return match != "" ? match : name;
1436}
1437
1439static void LoadPluginMapFile(PluginMap& map, const std::string& path) {
1440 std::ifstream f;
1441 f.open(path);
1442 if (f.fail()) {
1443 MESSAGE_LOG << "Cannot open " << path << ": " << strerror(errno);
1444 return;
1445 }
1446 std::stringstream buf;
1447 buf << f.rdbuf();
1448 auto filelist = ocpn::split(buf.str().c_str(), "\n");
1449 for (auto& file : filelist) {
1450 file = wxFileName(file).GetFullName().ToStdString();
1451 }
1452
1453 // key is basename with removed .files suffix and correct case.
1454 auto key = wxFileName(path).GetFullName().ToStdString();
1455 key = ocpn::split(key.c_str(), ".")[0];
1456 key = PluginNameCase(key);
1457 map[key] = filelist;
1458}
1459
1461static void LoadPluginMap(PluginMap& map) {
1462 map.clear();
1464 if (!root.IsOpened()) return;
1465 wxString filename;
1466 bool cont = root.GetFirst(&filename, "*.files", wxDIR_FILES);
1467 while (cont) {
1468 auto path = root.GetNameWithSep() + filename;
1469 LoadPluginMapFile(map, path.ToStdString());
1470 cont = root.GetNext(&filename);
1471 }
1472}
1473
1474std::string PluginHandler::GetPluginByLibrary(const std::string& filename) {
1475 auto basename = wxFileName(filename).GetFullName().ToStdString();
1476 if (FilesByPlugin.size() == 0) LoadPluginMap(FilesByPlugin);
1477 for (const auto& it : FilesByPlugin) {
1478 auto found = std::find(it.second.begin(), it.second.end(), basename);
1479 if (found != it.second.end()) return it.first;
1480 }
1481 return "";
1482}
1483
1485 // Look for the desired file
1486 wxURI uri(wxString(plugin.tarball_url.c_str()));
1487 wxFileName fn(uri.GetPath());
1488 wxString tarballFile = fn.GetFullName();
1489 std::string cacheFile = ocpn::lookup_tarball(tarballFile);
1490
1491#ifdef __WXOSX__
1492 // Depending on the browser settings, MacOS will sometimes automatically
1493 // de-compress the tar.gz file, leaving a simple ".tar" file in its expected
1494 // place. Check for this case, and "do the right thing"
1495 if (cacheFile == "") {
1496 fn.ClearExt();
1497 wxFileName fn1(fn.GetFullName());
1498 if (fn1.GetExt().IsSameAs("tar")) {
1499 tarballFile = fn.GetFullName();
1500 cacheFile = ocpn::lookup_tarball(tarballFile);
1501 }
1502 }
1503#endif
1504
1505 if (cacheFile != "") {
1506 MESSAGE_LOG << "Installing " << tarballFile << " from local cache";
1507 bool bOK = InstallPlugin(plugin, cacheFile);
1508 if (!bOK) {
1509 evt_download_failed.Notify(cacheFile);
1510 return false;
1511 }
1512 evt_download_ok.Notify(plugin.name + " " + plugin.version);
1513 return true;
1514 }
1515 return false;
1516}
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:38
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.
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.
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.