mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-29 20:01:26 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06cd4cbcc2 | ||
|
|
2ae1fdbd01 | ||
|
|
78b504be0e | ||
|
|
3ef782c6e5 | ||
|
|
e68694dbaf | ||
|
|
72cfe71b81 |
@@ -256,10 +256,13 @@ extern bool is_gallery_file(const std::string& path, char const* type);
|
|||||||
extern bool is_shapes_dir(const std::string& dir);
|
extern bool is_shapes_dir(const std::string& dir);
|
||||||
//BBS: add json support
|
//BBS: add json support
|
||||||
extern bool is_json_file(const std::string& path);
|
extern bool is_json_file(const std::string& path);
|
||||||
// True if rel_path is relative, has no ".." component and, joined to root, still resolves inside it.
|
// True if rel_path is relative, has no ".." component or embedded NUL and, joined to root, still resolves inside it.
|
||||||
// Both '/' and '\\' are treated as separators on every platform, so an archive rejected on one OS
|
// Both '/' and '\\' are treated as separators on every platform, so an archive rejected on one OS
|
||||||
// is rejected on all of them.
|
// is rejected on all of them.
|
||||||
extern bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root);
|
extern bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root);
|
||||||
|
// True if a symlink stored at link_rel_path (relative to root) with this target stays inside root: the target
|
||||||
|
// must be relative, and joined to the link's directory it must pass is_path_within_root.
|
||||||
|
extern bool is_symlink_target_within_root(const std::string &link_rel_path, const std::string &target, const boost::filesystem::path &root);
|
||||||
|
|
||||||
// Orca: custom protocal support utils
|
// Orca: custom protocal support utils
|
||||||
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
|
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
#include "miniz_extension.hpp"
|
#include "miniz_extension.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
|
#include <boost/log/trivial.hpp>
|
||||||
|
|
||||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||||
#include "boost/nowide/cstdio.hpp"
|
#include "boost/nowide/cstdio.hpp"
|
||||||
#endif
|
#endif
|
||||||
@@ -115,6 +118,66 @@ std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_
|
|||||||
return decode_zip_unicode_path_extra_field(extra.substr(0, extra_size > 0 ? extra_size - 1 : 0), stat.m_filename);
|
return decode_zip_unicode_path_extra_field(extra.substr(0, extra_size > 0 ? extra_size - 1 : 0), stat.m_filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir)
|
||||||
|
{
|
||||||
|
mz_zip_archive archive;
|
||||||
|
mz_zip_zero_struct(&archive);
|
||||||
|
|
||||||
|
if (!open_zip_reader(&archive, zip_path_utf8)) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "Unable to open zip reader for " << zip_path_utf8;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mz_uint num_entries = mz_zip_reader_get_num_files(&archive);
|
||||||
|
mz_zip_archive_file_stat stat;
|
||||||
|
|
||||||
|
// Validate every entry first so an archive with a single escaping entry leaves no partial output behind.
|
||||||
|
const boost::filesystem::path root(dest_dir);
|
||||||
|
for (mz_uint i = 0; i < num_entries; ++i) {
|
||||||
|
if (mz_zip_reader_file_stat(&archive, i, &stat) && !is_path_within_root(stat.m_filename, root)) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "Unzip: rejecting " << zip_path_utf8 << ", entry " << stat.m_filename << " resolves outside " << dest_dir;
|
||||||
|
close_zip_reader(&archive);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (mz_uint i = 0; i < num_entries; ++i) {
|
||||||
|
if (!mz_zip_reader_file_stat(&archive, i, &stat)) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "Unzip: read file stat failed";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::string dest_file = dest_dir + "/" + stat.m_filename;
|
||||||
|
try {
|
||||||
|
if (stat.m_is_directory) {
|
||||||
|
const boost::filesystem::path dest_path(dest_file);
|
||||||
|
if (!boost::filesystem::exists(dest_path))
|
||||||
|
boost::filesystem::create_directories(dest_path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (stat.m_uncomp_size == 0) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "Unzip: invalid size for file " << stat.m_filename;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Replace a symlink at the destination rather than writing through it.
|
||||||
|
const boost::filesystem::path dest_path(dest_file);
|
||||||
|
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path)))
|
||||||
|
boost::filesystem::remove(dest_path);
|
||||||
|
if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
|
||||||
|
close_zip_reader(&archive);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "Unzip: successfully extract file " << stat.m_file_index << " to " << dest_file;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
close_zip_reader(&archive);
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "Unzip: archive read exception: " << e.what();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close_zip_reader(&archive);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
MZ_Archive::MZ_Archive()
|
MZ_Archive::MZ_Archive()
|
||||||
{
|
{
|
||||||
mz_zip_zero_struct(&arch);
|
mz_zip_zero_struct(&arch);
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname_utf8);
|
|||||||
bool close_zip_reader(mz_zip_archive *zip);
|
bool close_zip_reader(mz_zip_archive *zip);
|
||||||
bool close_zip_writer(mz_zip_archive *zip);
|
bool close_zip_writer(mz_zip_archive *zip);
|
||||||
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
|
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
|
||||||
|
// Extracts every entry of the archive under dest_dir. Nothing is written if any entry would resolve outside dest_dir.
|
||||||
|
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir);
|
||||||
|
|
||||||
class MZ_Archive {
|
class MZ_Archive {
|
||||||
public:
|
public:
|
||||||
|
|||||||
+16
-1
@@ -1093,6 +1093,9 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p
|
|||||||
auto is_separator = [](char c) { return c == '/' || c == '\\'; };
|
auto is_separator = [](char c) { return c == '/' || c == '\\'; };
|
||||||
if (rel_path.empty() || is_separator(rel_path.front()) || (rel_path.size() > 1 && rel_path[1] == ':'))
|
if (rel_path.empty() || is_separator(rel_path.front()) || (rel_path.size() > 1 && rel_path[1] == ':'))
|
||||||
return false;
|
return false;
|
||||||
|
// The filesystem calls stop at a NUL, so they would act on a shorter path than the one checked here.
|
||||||
|
if (rel_path.find('\0') != std::string::npos)
|
||||||
|
return false;
|
||||||
for (size_t start = 0; start <= rel_path.size();) {
|
for (size_t start = 0; start <= rel_path.size();) {
|
||||||
size_t end = start;
|
size_t end = start;
|
||||||
while (end < rel_path.size() && !is_separator(rel_path[end]))
|
while (end < rel_path.size() && !is_separator(rel_path[end]))
|
||||||
@@ -1103,7 +1106,10 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p
|
|||||||
}
|
}
|
||||||
// Resolve against the canonical root so a symlink inside it cannot lead back out.
|
// Resolve against the canonical root so a symlink inside it cannot lead back out.
|
||||||
try {
|
try {
|
||||||
const std::string root_str = boost::filesystem::weakly_canonical(root).string();
|
std::string root_str = boost::filesystem::weakly_canonical(root).string();
|
||||||
|
// A trailing separator on root would otherwise fail the prefix match below for every path.
|
||||||
|
while (!root_str.empty() && (root_str.back() == '/' || root_str.back() == boost::filesystem::path::preferred_separator))
|
||||||
|
root_str.pop_back();
|
||||||
const std::string full_str = boost::filesystem::weakly_canonical(root / rel_path).string();
|
const std::string full_str = boost::filesystem::weakly_canonical(root / rel_path).string();
|
||||||
return full_str.compare(0, root_str.size(), root_str) == 0 &&
|
return full_str.compare(0, root_str.size(), root_str) == 0 &&
|
||||||
(full_str.size() == root_str.size() || full_str[root_str.size()] == boost::filesystem::path::preferred_separator);
|
(full_str.size() == root_str.size() || full_str[root_str.size()] == boost::filesystem::path::preferred_separator);
|
||||||
@@ -1112,6 +1118,15 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool is_symlink_target_within_root(const std::string &link_rel_path, const std::string &target, const boost::filesystem::path &root)
|
||||||
|
{
|
||||||
|
if (target.empty() || target.front() == '/' || target.front() == '\\' || (target.size() > 1 && target[1] == ':'))
|
||||||
|
return false;
|
||||||
|
// A relative target without ".." only descends from the link's directory, so no chain of such links can leave root.
|
||||||
|
const size_t sep = link_rel_path.find_last_of("/\\");
|
||||||
|
return is_path_within_root((sep == std::string::npos ? std::string() : link_rel_path.substr(0, sep + 1)) + target, root);
|
||||||
|
}
|
||||||
|
|
||||||
bool is_img_file(const std::string &path)
|
bool is_img_file(const std::string &path)
|
||||||
{
|
{
|
||||||
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
|
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
|
||||||
|
|||||||
@@ -1019,14 +1019,9 @@ if (UNIX AND NOT APPLE)
|
|||||||
find_package(GTK${SLIC3R_GTK} REQUIRED)
|
find_package(GTK${SLIC3R_GTK} REQUIRED)
|
||||||
pkg_check_modules(LIBSECRET REQUIRED libsecret-1)
|
pkg_check_modules(LIBSECRET REQUIRED libsecret-1)
|
||||||
pkg_check_modules(webkit2gtk REQUIRED webkit2gtk-4.1)
|
pkg_check_modules(webkit2gtk REQUIRED webkit2gtk-4.1)
|
||||||
if (FLATPAK)
|
find_package(X11 REQUIRED)
|
||||||
# I don't know why this is needed, but for whatever reason slic3r isn't
|
|
||||||
# linking to X11 and webkit2gtk. force it.
|
|
||||||
find_package(X11 REQUIRED)
|
|
||||||
target_link_libraries(libslic3r_gui ${X11_LIBRARIES} ${webkit2gtk_LIBRARIES})
|
|
||||||
endif()
|
|
||||||
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GTK${SLIC3R_GTK}_INCLUDE_DIRS} ${LIBSECRET_INCLUDE_DIRS} ${webkit2gtk_INCLUDE_DIRS})
|
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GTK${SLIC3R_GTK}_INCLUDE_DIRS} ${LIBSECRET_INCLUDE_DIRS} ${webkit2gtk_INCLUDE_DIRS})
|
||||||
target_link_libraries(libslic3r_gui ${GTK${SLIC3R_GTK}_LIBRARIES} fontconfig ${LIBSECRET_LIBRARIES})
|
target_link_libraries(libslic3r_gui ${GTK${SLIC3R_GTK}_LIBRARIES} fontconfig ${LIBSECRET_LIBRARIES} ${webkit2gtk_LIBRARIES} ${X11_LIBRARIES})
|
||||||
|
|
||||||
# Propagate GDK backend detection results as compile definitions so that
|
# Propagate GDK backend detection results as compile definitions so that
|
||||||
# LinuxDisplayBackend.cpp can include the right GDK headers.
|
# LinuxDisplayBackend.cpp can include the right GDK headers.
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
#include <boost/algorithm/string/predicate.hpp>
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
#include <boost/any.hpp>
|
#include <boost/any.hpp>
|
||||||
|
|
||||||
|
#include <wx/filename.h>
|
||||||
|
#include <wx/filesys.h>
|
||||||
|
|
||||||
#if __APPLE__
|
#if __APPLE__
|
||||||
#import <IOKit/pwr_mgt/IOPMLib.h>
|
#import <IOKit/pwr_mgt/IOPMLib.h>
|
||||||
#elif _WIN32
|
#elif _WIN32
|
||||||
@@ -531,6 +534,11 @@ boost::filesystem::path into_path(const wxString &str)
|
|||||||
return boost::filesystem::path(str.wx_str());
|
return boost::filesystem::path(str.wx_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wxString file_url_from_path(const boost::filesystem::path &path)
|
||||||
|
{
|
||||||
|
return wxFileSystem::FileNameToURL(wxFileName(from_path(path)));
|
||||||
|
}
|
||||||
|
|
||||||
void about()
|
void about()
|
||||||
{
|
{
|
||||||
AboutDialog dlg;
|
AboutDialog dlg;
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ std::string into_u8(const wxString &str);
|
|||||||
wxString from_path(const boost::filesystem::path &path);
|
wxString from_path(const boost::filesystem::path &path);
|
||||||
// boost path from wxString
|
// boost path from wxString
|
||||||
boost::filesystem::path into_path(const wxString &str);
|
boost::filesystem::path into_path(const wxString &str);
|
||||||
|
// file:// URL of a local path, percent-encoded so characters such as '#', '%' and '?' stay part of the path.
|
||||||
|
// Append any query or fragment to the result.
|
||||||
|
wxString file_url_from_path(const boost::filesystem::path &path);
|
||||||
|
|
||||||
// Display an About dialog
|
// Display an About dialog
|
||||||
extern void about();
|
extern void about();
|
||||||
|
|||||||
@@ -1512,11 +1512,33 @@ int GUI_App::install_plugin(std::string name, std::string package_name, InstallP
|
|||||||
size_t n = mz_zip_reader_get_extra(&archive, stat.m_file_index, extra.data(), extra.size());
|
size_t n = mz_zip_reader_get_extra(&archive, stat.m_file_index, extra.data(), extra.size());
|
||||||
dest_file = decode(extra.substr(0, n), stat.m_filename);
|
dest_file = decode(extra.substr(0, n), stat.m_filename);
|
||||||
}
|
}
|
||||||
|
if (!is_path_within_root(dest_file, plugin_folder)) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "[install_plugin] entry " << dest_file << " resolves outside " << plugin_folder.string();
|
||||||
|
close_zip_reader(&archive);
|
||||||
|
if (pro_fn) { pro_fn(InstallStatusUnzipFailed, 0, cancel); }
|
||||||
|
return InstallStatusUnzipFailed;
|
||||||
|
}
|
||||||
auto dest_path = plugin_folder / dest_file;
|
auto dest_path = plugin_folder / dest_file;
|
||||||
boost::filesystem::create_directories(dest_path.parent_path());
|
|
||||||
std::string dest_zip_file = encode_path(dest_path.string().c_str());
|
std::string dest_zip_file = encode_path(dest_path.string().c_str());
|
||||||
|
#ifndef WIN32
|
||||||
|
// Validate a symlink's target before anything at the destination is replaced.
|
||||||
|
const bool is_link = S_ISLNK(stat.m_external_attr >> 16);
|
||||||
|
std::string link;
|
||||||
|
if (is_link) {
|
||||||
|
link.assign(stat.m_uncomp_size, 0);
|
||||||
|
if (!mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, link.data(), stat.m_uncomp_size, 0) ||
|
||||||
|
!is_symlink_target_within_root(dest_file, link, plugin_folder)) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "[install_plugin] link " << dest_file << " -> " << link << " is unreadable or resolves outside " << plugin_folder.string();
|
||||||
|
close_zip_reader(&archive);
|
||||||
|
if (pro_fn) { pro_fn(InstallStatusUnzipFailed, 0, cancel); }
|
||||||
|
return InstallStatusUnzipFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
try {
|
try {
|
||||||
if (fs::exists(dest_path)) {
|
boost::filesystem::create_directories(dest_path.parent_path());
|
||||||
|
// symlink_status so that an existing symlink, dangling or not, is replaced rather than written through.
|
||||||
|
if (fs::exists(fs::symlink_status(dest_path))) {
|
||||||
boost::system::error_code ec;
|
boost::system::error_code ec;
|
||||||
fs::remove(dest_path, ec);
|
fs::remove(dest_path, ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
@@ -1544,9 +1566,8 @@ int GUI_App::install_plugin(std::string name, std::string package_name, InstallP
|
|||||||
}
|
}
|
||||||
mz_bool res = 0;
|
mz_bool res = 0;
|
||||||
#ifndef WIN32
|
#ifndef WIN32
|
||||||
if (S_ISLNK(stat.m_external_attr >> 16)) {
|
if (is_link) {
|
||||||
std::string link(stat.m_uncomp_size + 1, 0);
|
res = 1;
|
||||||
res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, link.data(), stat.m_uncomp_size, 0);
|
|
||||||
try {
|
try {
|
||||||
boost::filesystem::create_symlink(link, dest_path);
|
boost::filesystem::create_symlink(link, dest_path);
|
||||||
} catch (const std::exception &e) {
|
} catch (const std::exception &e) {
|
||||||
|
|||||||
@@ -98,10 +98,7 @@ void MarkdownTip::LoadStyle()
|
|||||||
ph /= "tooltip/styled.html";
|
ph /= "tooltip/styled.html";
|
||||||
_data_dir = false;
|
_data_dir = false;
|
||||||
}
|
}
|
||||||
auto url = ph.string();
|
_tipView->LoadURL(file_url_from_path(ph));
|
||||||
std::replace(url.begin(), url.end(), '\\', '/');
|
|
||||||
url = "file:///" + url;
|
|
||||||
_tipView->LoadURL(from_u8(url));
|
|
||||||
_lastTip.clear();
|
_lastTip.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3672,7 +3672,7 @@ void Sidebar::update_all_preset_comboboxes()
|
|||||||
wxString url = from_u8(PrintHost::get_print_host_webui(&cfg));
|
wxString url = from_u8(PrintHost::get_print_host_webui(&cfg));
|
||||||
wxString apikey;
|
wxString apikey;
|
||||||
if(url.empty())
|
if(url.empty())
|
||||||
url = wxString::Format("file://%s/web/orca/missing_connection.html", from_u8(resources_dir()));
|
url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/orca/missing_connection.html");
|
||||||
else {
|
else {
|
||||||
const auto host_type = cfg.option<ConfigOptionEnum<PrintHostType>>("host_type")->value;
|
const auto host_type = cfg.option<ConfigOptionEnum<PrintHostType>>("host_type")->value;
|
||||||
if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint))
|
if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint))
|
||||||
|
|||||||
@@ -57,9 +57,7 @@ PrivacyUpdateDialog::PrivacyUpdateDialog(wxWindow* parent, wxWindowID id, const
|
|||||||
|
|
||||||
fs::path ph(resources_dir());
|
fs::path ph(resources_dir());
|
||||||
ph /= "tooltip/privacyupdate.html";
|
ph /= "tooltip/privacyupdate.html";
|
||||||
m_host_url = ph.string();
|
m_host_url = into_u8(file_url_from_path(ph));
|
||||||
std::replace(m_host_url.begin(), m_host_url.end(), '\\', '/');
|
|
||||||
m_host_url = "file:///" + m_host_url;
|
|
||||||
m_vebview_release_note->LoadURL(from_u8(m_host_url));
|
m_vebview_release_note->LoadURL(from_u8(m_host_url));
|
||||||
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
|
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
|
||||||
|
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ const std::vector<std::string> license_list = {
|
|||||||
ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style) : wxPanel(parent, id, pos, size, style)
|
ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style) : wxPanel(parent, id, pos, size, style)
|
||||||
{
|
{
|
||||||
SetBackgroundColour(*wxWHITE);
|
SetBackgroundColour(*wxWHITE);
|
||||||
m_project_home_url = wxString::Format("file://%s/web/model/index.html", from_u8(resources_dir()));
|
m_project_home_url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/model/index.html");
|
||||||
wxString strlang = wxGetApp().current_language_code_safe();
|
wxString strlang = wxGetApp().current_language_code_safe();
|
||||||
if (strlang != "")
|
if (strlang != "")
|
||||||
m_project_home_url = wxString::Format("file://%s/web/model/index.html?lang=%s", from_u8(resources_dir()), strlang);
|
m_project_home_url += "?lang=" + strlang;
|
||||||
|
|
||||||
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
|
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
|
|||||||
@@ -510,7 +510,7 @@ void UpdateVersionDialog::update_version_info(wxString release_note, wxString ve
|
|||||||
out_buf->append(text, size);
|
out_buf->append(text, size);
|
||||||
}, (void*) &html_source, MD_DIALECT_GITHUB | MD_FLAG_STRIKETHROUGH | MD_FLAG_WIKILINKS, 0);
|
}, (void*) &html_source, MD_DIALECT_GITHUB | MD_FLAG_STRIKETHROUGH | MD_FLAG_WIKILINKS, 0);
|
||||||
html_source.append("</body></html>");
|
html_source.append("</body></html>");
|
||||||
m_vebview_release_note->LoadURL("file://" + (boost::filesystem::path (resources_dir()) / "web/guide/0/index.html").string());
|
m_vebview_release_note->LoadURL(file_url_from_path(boost::filesystem::path(resources_dir()) / "web/guide/0/index.html"));
|
||||||
|
|
||||||
SetMinSize(GetSize());
|
SetMinSize(GetSize());
|
||||||
SetMaxSize(GetSize());
|
SetMaxSize(GetSize());
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ DownPluginFrame::DownPluginFrame(GUI_App *pGUI) : wxDialog((wxWindow *) (pGUI->m
|
|||||||
|
|
||||||
// set the frame icon
|
// set the frame icon
|
||||||
wxBoxSizer *topsizer = new wxBoxSizer(wxVERTICAL);
|
wxBoxSizer *topsizer = new wxBoxSizer(wxVERTICAL);
|
||||||
wxString TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/6/index.html").make_preferred().string());
|
wxString TargetUrl = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/guide/6/index.html");
|
||||||
|
|
||||||
TargetUrl = "file://" + TargetUrl;
|
|
||||||
|
|
||||||
// Create the webview
|
// Create the webview
|
||||||
m_browser = WebView::CreateWebView(this, TargetUrl);
|
m_browser = WebView::CreateWebView(this, TargetUrl);
|
||||||
|
|||||||
@@ -218,37 +218,38 @@ wxString GuideFrame::SetStartPage(GuidePage startpage, bool load)
|
|||||||
m_page = startpage;
|
m_page = startpage;
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(" enter, load=%1%, start_page=%2%")%load%int(startpage);
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(" enter, load=%1%, start_page=%2%")%load%int(startpage);
|
||||||
//wxLogMessage("GUIDE: webpage_1 %s", (boost::filesystem::path(resources_dir()) / "web\\guide\\1\\index.html").make_preferred().string().c_str() );
|
//wxLogMessage("GUIDE: webpage_1 %s", (boost::filesystem::path(resources_dir()) / "web\\guide\\1\\index.html").make_preferred().string().c_str() );
|
||||||
wxString TargetUrl = from_u8( (boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=1").make_preferred().string() );
|
const wxString guide_url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/guide/0/index.html");
|
||||||
|
wxString TargetUrl = guide_url + "?target=1";
|
||||||
//wxLogMessage("GUIDE: webpage_2 %s", TargetUrl.mb_str());
|
//wxLogMessage("GUIDE: webpage_2 %s", TargetUrl.mb_str());
|
||||||
|
|
||||||
if (startpage == BBL_WELCOME){
|
if (startpage == BBL_WELCOME){
|
||||||
SetTitle(_L("Setup Wizard"));
|
SetTitle(_L("Setup Wizard"));
|
||||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=1").make_preferred().string());
|
TargetUrl = guide_url + "?target=1";
|
||||||
} else if (startpage == BBL_REGION) {
|
} else if (startpage == BBL_REGION) {
|
||||||
SetTitle(_L("Setup Wizard"));
|
SetTitle(_L("Setup Wizard"));
|
||||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=11").make_preferred().string());
|
TargetUrl = guide_url + "?target=11";
|
||||||
} else if (startpage == BBL_MODELS) {
|
} else if (startpage == BBL_MODELS) {
|
||||||
SetTitle(_L("Setup Wizard"));
|
SetTitle(_L("Setup Wizard"));
|
||||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string());
|
TargetUrl = guide_url + "?target=21";
|
||||||
} else if (startpage == BBL_FILAMENTS) {
|
} else if (startpage == BBL_FILAMENTS) {
|
||||||
SetTitle(_L("Setup Wizard"));
|
SetTitle(_L("Setup Wizard"));
|
||||||
|
|
||||||
int nSize = m_ProfileJson["model"].size();
|
int nSize = m_ProfileJson["model"].size();
|
||||||
|
|
||||||
if (nSize>0)
|
if (nSize>0)
|
||||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=22").make_preferred().string());
|
TargetUrl = guide_url + "?target=22";
|
||||||
else
|
else
|
||||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string());
|
TargetUrl = guide_url + "?target=21";
|
||||||
} else if (startpage == BBL_FILAMENT_ONLY) {
|
} else if (startpage == BBL_FILAMENT_ONLY) {
|
||||||
SetTitle("");
|
SetTitle("");
|
||||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=23").make_preferred().string());
|
TargetUrl = guide_url + "?target=23";
|
||||||
} else if (startpage == BBL_MODELS_ONLY) {
|
} else if (startpage == BBL_MODELS_ONLY) {
|
||||||
SetTitle("");
|
SetTitle("");
|
||||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=24").make_preferred().string());
|
TargetUrl = guide_url + "?target=24";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
SetTitle(_L("Setup Wizard"));
|
SetTitle(_L("Setup Wizard"));
|
||||||
TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string());
|
TargetUrl = guide_url + "?target=21";
|
||||||
}
|
}
|
||||||
|
|
||||||
wxString strlang = wxGetApp().current_language_code_safe();
|
wxString strlang = wxGetApp().current_language_code_safe();
|
||||||
@@ -256,7 +257,6 @@ wxString GuideFrame::SetStartPage(GuidePage startpage, bool load)
|
|||||||
if (strlang != "")
|
if (strlang != "")
|
||||||
TargetUrl = wxString::Format("%s&lang=%s", w2s(TargetUrl), strlang);
|
TargetUrl = wxString::Format("%s&lang=%s", w2s(TargetUrl), strlang);
|
||||||
|
|
||||||
TargetUrl = "file://" + TargetUrl;
|
|
||||||
if (load)
|
if (load)
|
||||||
load_url(TargetUrl);
|
load_url(TargetUrl);
|
||||||
|
|
||||||
@@ -1284,7 +1284,7 @@ bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_r
|
|||||||
entry["vendor"] = vp.id;
|
entry["vendor"] = vp.id;
|
||||||
entry["nozzle_diameter"] = nozzle_str;
|
entry["nozzle_diameter"] = nozzle_str;
|
||||||
entry["materials"] = materials_str;
|
entry["materials"] = materials_str;
|
||||||
entry["cover"] = cover_path.string();
|
entry["cover"] = into_u8(file_url_from_path(cover_path));
|
||||||
entry["nozzle_selected"] = "";
|
entry["nozzle_selected"] = "";
|
||||||
entry["sub_path"] = "";
|
entry["sub_path"] = "";
|
||||||
m_ProfileJson["model"].push_back(entry);
|
m_ProfileJson["model"].push_back(entry);
|
||||||
@@ -1732,7 +1732,7 @@ int GuideFrame::LoadProfileFamily(std::string strVendor, std::string strFilePath
|
|||||||
cover_path = (boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") / cover_file)
|
cover_path = (boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") / cover_file)
|
||||||
.make_preferred();
|
.make_preferred();
|
||||||
}
|
}
|
||||||
OneModel["cover"] = cover_path.string();
|
OneModel["cover"] = into_u8(file_url_from_path(cover_path));
|
||||||
|
|
||||||
OneModel["nozzle_selected"] = "";
|
OneModel["nozzle_selected"] = "";
|
||||||
|
|
||||||
|
|||||||
@@ -599,7 +599,7 @@ void ZUserLogin::OnScriptResponseMessage(wxCommandEvent &WXUNUSED(evt))
|
|||||||
|
|
||||||
bool ZUserLogin::ShowErrorPage()
|
bool ZUserLogin::ShowErrorPage()
|
||||||
{
|
{
|
||||||
wxString ErrortUrl = from_u8((boost::filesystem::path(resources_dir()) / "web\\login\\error.html").make_preferred().string());
|
wxString ErrortUrl = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/login/error.html");
|
||||||
load_url(ErrortUrl);
|
load_url(ErrortUrl);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ namespace GUI {
|
|||||||
WebViewPanel::WebViewPanel(wxWindow *parent)
|
WebViewPanel::WebViewPanel(wxWindow *parent)
|
||||||
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
|
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
|
||||||
{
|
{
|
||||||
wxString url = wxString::Format("file://%s/web/homepage/index.html", from_u8(resources_dir()));
|
wxString url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/homepage/index.html");
|
||||||
wxString strlang = wxGetApp().current_language_code_safe();
|
wxString strlang = wxGetApp().current_language_code_safe();
|
||||||
if (strlang != "")
|
if (strlang != "")
|
||||||
url = wxString::Format("file://%s/web/homepage/index.html?lang=%s", from_u8(resources_dir()), strlang);
|
url += "?lang=" + strlang;
|
||||||
|
|
||||||
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
|
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "MultiNozzleSync.hpp"
|
#include "MultiNozzleSync.hpp"
|
||||||
|
|
||||||
|
#include "../GUI.hpp"
|
||||||
#include "../GUI_App.hpp"
|
#include "../GUI_App.hpp"
|
||||||
#include "../I18N.hpp"
|
#include "../I18N.hpp"
|
||||||
#include "../Plater.hpp"
|
#include "../Plater.hpp"
|
||||||
@@ -21,8 +22,6 @@
|
|||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
#include <wx/choice.h>
|
#include <wx/choice.h>
|
||||||
#include <wx/filename.h>
|
|
||||||
#include <wx/filesys.h>
|
|
||||||
#include <wx/sizer.h>
|
#include <wx/sizer.h>
|
||||||
#include <wx/stattext.h>
|
#include <wx/stattext.h>
|
||||||
|
|
||||||
@@ -641,8 +640,7 @@ NozzleListTable::NozzleListTable(wxWindow* parent) : wxPanel(parent,wxID_ANY,wxD
|
|||||||
m_web_view->AddScriptMessageHandler("nozzleListTable");
|
m_web_view->AddScriptMessageHandler("nozzleListTable");
|
||||||
m_web_view->EnableContextMenu(false);
|
m_web_view->EnableContextMenu(false);
|
||||||
fs::path filepath = fs::path(resources_dir()) / "web/flush/NozzleListTable.html";
|
fs::path filepath = fs::path(resources_dir()) / "web/flush/NozzleListTable.html";
|
||||||
wxFileName fn(wxString::FromUTF8(filepath.string()));
|
wxString url = file_url_from_path(filepath);
|
||||||
wxString url = wxFileSystem::FileNameToURL(fn);
|
|
||||||
m_web_view->LoadURL(url);
|
m_web_view->LoadURL(url);
|
||||||
|
|
||||||
auto sizer = new wxBoxSizer(wxVERTICAL);
|
auto sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|||||||
@@ -48,20 +48,19 @@ constexpr char ORCA_BRIDGE_JS[] = R"JS(
|
|||||||
|
|
||||||
wxString bootstrap_url()
|
wxString bootstrap_url()
|
||||||
{
|
{
|
||||||
return wxString("file://") + from_u8((boost::filesystem::path(resources_dir()) / BOOTSTRAP_PAGE).make_preferred().string());
|
return file_url_from_path(boost::filesystem::path(resources_dir()) / BOOTSTRAP_PAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
wxString content_base_url()
|
wxString content_base_url()
|
||||||
{
|
{
|
||||||
const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
|
return file_url_from_path(boost::filesystem::path(resources_dir()) / "web") + "/";
|
||||||
return wxString("file://") + from_u8(dir) + "/";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool is_content_url(const wxString& url)
|
bool is_content_url(const wxString& url)
|
||||||
{
|
{
|
||||||
// The web view reports the URL it parsed, which escapes anything the resources path holds
|
// The web view reports the URL it parsed, which may escape the resources path differently
|
||||||
// (a space, a non-ASCII character), while content_base_url() is the raw path.
|
// from content_base_url().
|
||||||
return wxURI::Unescape(url.BeforeFirst('#')) == content_base_url();
|
return wxURI::Unescape(url.BeforeFirst('#')) == wxURI::Unescape(content_base_url());
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* orca_bridge_script() { return ORCA_BRIDGE_JS; }
|
const char* orca_bridge_script() { return ORCA_BRIDGE_JS; }
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ bool WebViewHostDialog::create_webview(const std::string& resource_path,
|
|||||||
|
|
||||||
wxString WebViewHostDialog::build_resource_url(const std::string& resource_path) const
|
wxString WebViewHostDialog::build_resource_url(const std::string& resource_path) const
|
||||||
{
|
{
|
||||||
wxString target_url = from_u8((boost::filesystem::path(resources_dir()) / resource_path).make_preferred().string());
|
wxString target_url = file_url_from_path(boost::filesystem::path(resources_dir()) / resource_path);
|
||||||
|
|
||||||
if (append_language_to_url()) {
|
if (append_language_to_url()) {
|
||||||
const wxString lang = wxGetApp().current_language_code_safe();
|
const wxString lang = wxGetApp().current_language_code_safe();
|
||||||
@@ -202,7 +202,7 @@ wxString WebViewHostDialog::build_resource_url(const std::string& resource_path)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return wxString("file://") + target_url;
|
return target_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebViewHostDialog::load_url(const wxString& url)
|
void WebViewHostDialog::load_url(const wxString& url)
|
||||||
|
|||||||
@@ -464,7 +464,7 @@ WipingDialog::WipingDialog(wxWindow* parent, const int max_flush_volume) :
|
|||||||
wxString filepath_str = from_path(filepath);
|
wxString filepath_str = from_path(filepath);
|
||||||
wxFileName fn(filepath_str);
|
wxFileName fn(filepath_str);
|
||||||
if(fn.FileExists()) {
|
if(fn.FileExists()) {
|
||||||
wxString url = wxFileSystem::FileNameToURL(fn);
|
wxString url = file_url_from_path(filepath);
|
||||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__<< "File exists and load url " << url.ToStdString();
|
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__<< "File exists and load url " << url.ToStdString();
|
||||||
m_webview->LoadURL(url);
|
m_webview->LoadURL(url);
|
||||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__<< "Successfully loaded url: " << url.ToStdString();
|
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__<< "Successfully loaded url: " << url.ToStdString();
|
||||||
|
|||||||
@@ -340,9 +340,7 @@ namespace Slic3r {
|
|||||||
if (classify_printer_model(config->opt_string("printer_model")) != ElegooPrinterType::CC2)
|
if (classify_printer_model(config->opt_string("printer_model")) != ElegooPrinterType::CC2)
|
||||||
return fallback_webui;
|
return fallback_webui;
|
||||||
|
|
||||||
std::string web_path = resources_dir() + "/web/elegoolink/lan_service_web/index.html";
|
std::string web_path = GUI::into_u8(GUI::file_url_from_path(boost::filesystem::path(resources_dir()) / "web/elegoolink/lan_service_web/index.html"));
|
||||||
std::replace(web_path.begin(), web_path.end(), '\\', '/');
|
|
||||||
web_path = "file://" + web_path;
|
|
||||||
const std::string token = get_cc2_token(config->opt_string("printhost_apikey"));
|
const std::string token = get_cc2_token(config->opt_string("printhost_apikey"));
|
||||||
const std::string host_ip = Http::get_host_header_value(host);
|
const std::string host_ip = Http::get_host_header_value(host);
|
||||||
|
|
||||||
|
|||||||
@@ -339,62 +339,8 @@ bool PresetUpdater::priv::get_file(const std::string &url, const fs::path &targe
|
|||||||
//BBS: refine preset update logic
|
//BBS: refine preset update logic
|
||||||
bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::path &dest_path)
|
bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::path &dest_path)
|
||||||
{
|
{
|
||||||
bool res = true;
|
const std::string parent_path = (!dest_path.empty() ? dest_path : source_path.parent_path()).string();
|
||||||
std::string file_path = source_path.string();
|
return extract_archive_confined(source_path.string(), parent_path);
|
||||||
std::string parent_path = (!dest_path.empty() ? dest_path : source_path.parent_path()).string();
|
|
||||||
mz_zip_archive archive;
|
|
||||||
mz_zip_zero_struct(&archive);
|
|
||||||
|
|
||||||
if (!open_zip_reader(&archive, file_path))
|
|
||||||
{
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Unable to open zip reader for "<<file_path;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mz_uint num_entries = mz_zip_reader_get_num_files(&archive);
|
|
||||||
|
|
||||||
mz_zip_archive_file_stat stat;
|
|
||||||
// we first loop the entries to read from the archive the .amf file only, in order to extract the version from it
|
|
||||||
for (mz_uint i = 0; i < num_entries; ++i)
|
|
||||||
{
|
|
||||||
if (mz_zip_reader_file_stat(&archive, i, &stat))
|
|
||||||
{
|
|
||||||
std::string dest_file = parent_path+"/"+stat.m_filename;
|
|
||||||
if (stat.m_is_directory) {
|
|
||||||
fs::path dest_path(dest_file);
|
|
||||||
if (!fs::exists(dest_path))
|
|
||||||
fs::create_directories(dest_path);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else if (stat.m_uncomp_size == 0) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]Unzip: invalid size for file "<<stat.m_filename;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
res = mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0);
|
|
||||||
if (!res) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]extract file "<<stat.m_filename<<" to dest "<<dest_file<<" failed";
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]successfully extract file " << stat.m_file_index << " to "<<dest_file;
|
|
||||||
}
|
|
||||||
catch (const std::exception& e)
|
|
||||||
{
|
|
||||||
// ensure the zip archive is closed and rethrow the exception
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]Archive read exception:"<<e.what();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]Unzip: read file stat failed";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a leftover partial archive for the vendor about to be synchronized.
|
// Remove a leftover partial archive for the vendor about to be synchronized.
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_lay_on_face.cpp
|
test_lay_on_face.cpp
|
||||||
test_model.cpp
|
test_model.cpp
|
||||||
test_utils.cpp
|
test_utils.cpp
|
||||||
|
test_miniz_extension.cpp
|
||||||
test_timeutils.cpp
|
test_timeutils.cpp
|
||||||
test_voronoi.cpp
|
test_voronoi.cpp
|
||||||
test_wipe_tower_estimate.cpp
|
test_wipe_tower_estimate.cpp
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include "libslic3r/miniz_extension.hpp"
|
||||||
|
|
||||||
|
#include "test_utils.hpp"
|
||||||
|
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
namespace fs = boost::filesystem;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void write_zip(const fs::path &zip_file, const std::vector<std::pair<std::string, std::string>> &entries)
|
||||||
|
{
|
||||||
|
mz_zip_archive zip;
|
||||||
|
mz_zip_zero_struct(&zip);
|
||||||
|
REQUIRE(open_zip_writer(&zip, zip_file.string()));
|
||||||
|
for (const auto &[name, content] : entries)
|
||||||
|
REQUIRE(mz_zip_writer_add_mem(&zip, name.c_str(), content.data(), content.size(), MZ_DEFAULT_COMPRESSION));
|
||||||
|
REQUIRE(mz_zip_writer_finalize_archive(&zip));
|
||||||
|
REQUIRE(close_zip_writer(&zip));
|
||||||
|
}
|
||||||
|
|
||||||
|
// miniz refuses to write a name starting with '/', so write a placeholder of the same length and patch it in place.
|
||||||
|
void rename_entry(const fs::path &zip_file, const std::string &from, const std::string &to)
|
||||||
|
{
|
||||||
|
REQUIRE(from.size() == to.size());
|
||||||
|
std::string bytes;
|
||||||
|
{
|
||||||
|
std::ifstream in(zip_file.string(), std::ios::binary);
|
||||||
|
bytes.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
size_t count = 0;
|
||||||
|
for (size_t pos = bytes.find(from); pos != std::string::npos; pos = bytes.find(from, pos + to.size()), ++count)
|
||||||
|
bytes.replace(pos, from.size(), to);
|
||||||
|
// Once in the local header and once in the central directory.
|
||||||
|
REQUIRE(count == 2);
|
||||||
|
std::ofstream out(zip_file.string(), std::ios::binary | std::ios::trunc);
|
||||||
|
out << bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> list_dir(const fs::path &dir)
|
||||||
|
{
|
||||||
|
std::vector<std::string> names;
|
||||||
|
for (const fs::directory_entry &entry : fs::directory_iterator(dir))
|
||||||
|
names.push_back(entry.path().filename().string());
|
||||||
|
std::sort(names.begin(), names.end());
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string read_file(const fs::path &file)
|
||||||
|
{
|
||||||
|
std::ifstream in(file.string(), std::ios::binary);
|
||||||
|
return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("Confined extraction writes a well-formed archive under the target directory", "[MinizExtension]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir tmp;
|
||||||
|
const fs::path zip_file = tmp.path() / "bundle.zip";
|
||||||
|
const fs::path target = tmp.path() / "cache";
|
||||||
|
fs::create_directories(target);
|
||||||
|
write_zip(zip_file, {{"vendor/", ""}, {"vendor/machine/", ""}, {"vendor.json", "{\"a\":1}"}, {"vendor/machine/printer.json", "{\"b\":2}"}});
|
||||||
|
|
||||||
|
REQUIRE(extract_archive_confined(zip_file.string(), target.string()));
|
||||||
|
CHECK(fs::is_directory(target / "vendor"));
|
||||||
|
CHECK(read_file(target / "vendor.json") == "{\"a\":1}");
|
||||||
|
CHECK(read_file(target / "vendor" / "machine" / "printer.json") == "{\"b\":2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Confined extraction rejects an archive with an entry outside the target directory", "[MinizExtension]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir tmp;
|
||||||
|
const fs::path zip_file = tmp.path() / "bundle.zip";
|
||||||
|
const fs::path target = tmp.path() / "cache";
|
||||||
|
fs::create_directories(target);
|
||||||
|
|
||||||
|
const std::string escaping_entry = GENERATE(std::string("../escape.txt"), std::string("..\\escape.txt"),
|
||||||
|
std::string("sub/../../escape.txt"), std::string("C:/escape.txt"),
|
||||||
|
std::string("C:escape.txt"), std::string("\\escape.txt"));
|
||||||
|
// The normal entry comes first so a per-entry check would already have written it.
|
||||||
|
write_zip(zip_file, {{"normal.json", "{}"}, {escaping_entry, "escaped"}});
|
||||||
|
|
||||||
|
CAPTURE(escaping_entry);
|
||||||
|
CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string()));
|
||||||
|
CHECK_FALSE(fs::exists(tmp.path() / "escape.txt"));
|
||||||
|
CHECK(fs::is_empty(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Confined extraction rejects an archive with an absolute entry name", "[MinizExtension]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir tmp;
|
||||||
|
const fs::path zip_file = tmp.path() / "bundle.zip";
|
||||||
|
const fs::path target = tmp.path() / "cache";
|
||||||
|
fs::create_directories(target);
|
||||||
|
|
||||||
|
const std::string absolute = (tmp.path() / "escape.txt").generic_string();
|
||||||
|
const std::string placeholder = "#" + absolute.substr(1);
|
||||||
|
write_zip(zip_file, {{"normal.json", "{}"}, {placeholder, "escaped"}});
|
||||||
|
rename_entry(zip_file, placeholder, absolute);
|
||||||
|
|
||||||
|
CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string()));
|
||||||
|
CHECK_FALSE(fs::exists(tmp.path() / "escape.txt"));
|
||||||
|
CHECK(fs::is_empty(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Confined extraction rejects a directory entry outside the target directory", "[MinizExtension]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir tmp;
|
||||||
|
const fs::path zip_file = tmp.path() / "bundle.zip";
|
||||||
|
const fs::path target = tmp.path() / "cache";
|
||||||
|
fs::create_directories(target);
|
||||||
|
write_zip(zip_file, {{"vendor/", ""}, {"../outside/", ""}});
|
||||||
|
|
||||||
|
CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string()));
|
||||||
|
CHECK_FALSE(fs::exists(tmp.path() / "outside"));
|
||||||
|
CHECK(fs::is_empty(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Confined extraction validates zero-size entries like any other", "[MinizExtension]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir tmp;
|
||||||
|
const fs::path zip_file = tmp.path() / "bundle.zip";
|
||||||
|
const fs::path target = tmp.path() / "cache";
|
||||||
|
fs::create_directories(target);
|
||||||
|
|
||||||
|
SECTION("an empty file inside the target does not fail the archive") {
|
||||||
|
write_zip(zip_file, {{"empty.json", ""}, {"vendor.json", "{}"}});
|
||||||
|
CHECK(extract_archive_confined(zip_file.string(), target.string()));
|
||||||
|
CHECK(read_file(target / "vendor.json") == "{}");
|
||||||
|
}
|
||||||
|
SECTION("an empty file outside the target rejects the archive") {
|
||||||
|
write_zip(zip_file, {{"vendor.json", "{}"}, {"../escape.txt", ""}});
|
||||||
|
CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string()));
|
||||||
|
CHECK_FALSE(fs::exists(tmp.path() / "escape.txt"));
|
||||||
|
CHECK(fs::is_empty(target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Confined extraction writes nothing outside the target for Windows-specific name forms", "[MinizExtension]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir tmp;
|
||||||
|
const fs::path zip_file = tmp.path() / "bundle.zip";
|
||||||
|
const fs::path target = tmp.path() / "cache";
|
||||||
|
fs::create_directories(target);
|
||||||
|
|
||||||
|
// Windows strips trailing dots and spaces and maps device names; whether these extract depends on the
|
||||||
|
// platform, but none of them may land beside the target.
|
||||||
|
const std::string name = GENERATE(std::string("name."), std::string("name "), std::string("..."), std::string(".. "),
|
||||||
|
std::string(".. /escape.txt"), std::string(".../escape.txt"), std::string("CON"),
|
||||||
|
std::string("sub/NUL.txt"), std::string("C:escape.txt"));
|
||||||
|
write_zip(zip_file, {{name, "payload"}});
|
||||||
|
|
||||||
|
CAPTURE(name);
|
||||||
|
extract_archive_confined(zip_file.string(), target.string());
|
||||||
|
CHECK(list_dir(tmp.path()) == std::vector<std::string>{"bundle.zip", "cache"});
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
TEST_CASE("Confined extraction replaces a symlink at the destination instead of writing through it", "[MinizExtension]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir tmp;
|
||||||
|
const fs::path zip_file = tmp.path() / "bundle.zip";
|
||||||
|
const fs::path target = tmp.path() / "cache";
|
||||||
|
const fs::path outside = tmp.path() / "outside";
|
||||||
|
fs::create_directories(target);
|
||||||
|
fs::create_directories(outside);
|
||||||
|
write_zip(zip_file, {{"vendor.json", "{\"a\":1}"}});
|
||||||
|
|
||||||
|
SECTION("a dangling symlink") {
|
||||||
|
fs::create_symlink(outside / "vendor.json", target / "vendor.json");
|
||||||
|
CHECK(extract_archive_confined(zip_file.string(), target.string()));
|
||||||
|
CHECK_FALSE(fs::exists(outside / "vendor.json"));
|
||||||
|
CHECK_FALSE(fs::is_symlink(fs::symlink_status(target / "vendor.json")));
|
||||||
|
CHECK(read_file(target / "vendor.json") == "{\"a\":1}");
|
||||||
|
}
|
||||||
|
SECTION("a symlink to an existing file") {
|
||||||
|
{ std::ofstream((outside / "vendor.json").string()) << "original"; }
|
||||||
|
fs::create_symlink(outside / "vendor.json", target / "vendor.json");
|
||||||
|
extract_archive_confined(zip_file.string(), target.string());
|
||||||
|
CHECK(read_file(outside / "vendor.json") == "original");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -243,3 +243,82 @@ TEST_CASE("find_unused_filename gives up after 999 versions", "[Utils]") {
|
|||||||
REQUIRE_FALSE(find_unused_filename(dir.path(), "model.3mf", {}, name));
|
REQUIRE_FALSE(find_unused_filename(dir.path(), "model.3mf", {}, name));
|
||||||
CHECK(name == "model(999).3mf");
|
CHECK(name == "model(999).3mf");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("is_path_within_root accepts a root given with a trailing separator", "[utils]") {
|
||||||
|
ScopedTemporaryDir tmp;
|
||||||
|
const std::string root = tmp.path().string();
|
||||||
|
const std::string with_separator = GENERATE_COPY(root + "/", root + std::string(1, static_cast<char>(boost::filesystem::path::preferred_separator)));
|
||||||
|
|
||||||
|
CAPTURE(with_separator);
|
||||||
|
CHECK(is_path_within_root("vendor.json", with_separator));
|
||||||
|
CHECK(is_path_within_root("vendor/machine/printer.json", with_separator));
|
||||||
|
CHECK_FALSE(is_path_within_root("../vendor.json", with_separator));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("is_path_within_root treats Windows-specific name forms the same on every platform", "[utils]") {
|
||||||
|
ScopedTemporaryDir tmp;
|
||||||
|
|
||||||
|
SECTION("names ending in dots or spaces stay inside the root") {
|
||||||
|
const std::string name = GENERATE(std::string("name."), std::string("name "), std::string("dir./file.json"), std::string("dir /file.json"));
|
||||||
|
CAPTURE(name);
|
||||||
|
CHECK(is_path_within_root(name, tmp.path()));
|
||||||
|
}
|
||||||
|
SECTION("drive-relative names are rejected") {
|
||||||
|
const std::string name = GENERATE(std::string("C:x"), std::string("c:x/y.json"), std::string("C:"));
|
||||||
|
CAPTURE(name);
|
||||||
|
CHECK_FALSE(is_path_within_root(name, tmp.path()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("is_path_within_root rejects a name with an embedded NUL", "[utils]") {
|
||||||
|
ScopedTemporaryDir tmp;
|
||||||
|
// The filesystem calls stop at the NUL, so they would act on a different path than the one checked.
|
||||||
|
const std::string name = GENERATE(std::string("..\0", 3), std::string("..\0x/file.json", 14), std::string("sub/..\0x", 8),
|
||||||
|
std::string("file.json\0", 10), std::string("\0file.json", 10));
|
||||||
|
CAPTURE(name.size());
|
||||||
|
CHECK_FALSE(is_path_within_root(name, tmp.path()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("is_symlink_target_within_root accepts relative targets that stay inside the root", "[utils]") {
|
||||||
|
ScopedTemporaryDir tmp;
|
||||||
|
const auto [link, target] = GENERATE(std::make_pair(std::string("Versions/Current"), std::string("A")),
|
||||||
|
std::make_pair(std::string("Foo.framework/Foo"), std::string("Versions/Current/Foo")),
|
||||||
|
std::make_pair(std::string("libfoo.so"), std::string("libfoo.so.1")),
|
||||||
|
std::make_pair(std::string("a/b/link"), std::string("c/d")));
|
||||||
|
CAPTURE(link, target);
|
||||||
|
CHECK(is_symlink_target_within_root(link, target, tmp.path()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("is_symlink_target_within_root rejects absolute targets and targets that climb out", "[utils]") {
|
||||||
|
ScopedTemporaryDir tmp;
|
||||||
|
const std::string outside = (tmp.path().parent_path() / "outside").generic_string();
|
||||||
|
const auto [link, target] = GENERATE_COPY(std::make_pair(std::string("sub/link"), outside),
|
||||||
|
std::make_pair(std::string("sub/link"), std::string("/etc/passwd")),
|
||||||
|
std::make_pair(std::string("sub/link"), std::string("\\outside")),
|
||||||
|
std::make_pair(std::string("sub/link"), std::string("C:/outside")),
|
||||||
|
std::make_pair(std::string("sub/link"), std::string("C:outside")),
|
||||||
|
std::make_pair(std::string("sub/link"), std::string("")),
|
||||||
|
std::make_pair(std::string("link"), std::string("..")),
|
||||||
|
std::make_pair(std::string("link"), std::string("../outside")),
|
||||||
|
std::make_pair(std::string("sub/link"), std::string("../../outside")),
|
||||||
|
std::make_pair(std::string("sub/link"), std::string("x/../../../outside")),
|
||||||
|
std::make_pair(std::string("sub/link"), std::string("..\\..\\outside")),
|
||||||
|
// symlink() stops at the NUL, so this target would be created as "..".
|
||||||
|
std::make_pair(std::string("link"), std::string("..\0", 3)));
|
||||||
|
CAPTURE(link, target);
|
||||||
|
CHECK_FALSE(is_symlink_target_within_root(link, target, tmp.path()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
TEST_CASE("is_symlink_target_within_root rejects a target that passes through a symlink leading out", "[utils]") {
|
||||||
|
ScopedTemporaryDir tmp;
|
||||||
|
const boost::filesystem::path root = tmp.path() / "root";
|
||||||
|
const boost::filesystem::path outside = tmp.path() / "outside";
|
||||||
|
boost::filesystem::create_directories(root);
|
||||||
|
boost::filesystem::create_directories(outside);
|
||||||
|
boost::filesystem::create_symlink(outside, root / "out");
|
||||||
|
|
||||||
|
CHECK_FALSE(is_symlink_target_within_root("link", "out/lib.so", root));
|
||||||
|
CHECK(is_symlink_target_within_root("link", "in/lib.so", root));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_plugin_cloud_metadata.cpp
|
test_plugin_cloud_metadata.cpp
|
||||||
test_plugin_audit.cpp
|
test_plugin_audit.cpp
|
||||||
test_shortcuts.cpp
|
test_shortcuts.cpp
|
||||||
|
test_file_url.cpp
|
||||||
../fff_print/test_helpers.cpp
|
../fff_print/test_helpers.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <catch2/generators/catch_generators.hpp>
|
||||||
|
|
||||||
|
#include "slic3r/GUI/GUI.hpp"
|
||||||
|
|
||||||
|
#include <boost/filesystem/path.hpp>
|
||||||
|
|
||||||
|
#include <wx/uri.h>
|
||||||
|
|
||||||
|
using namespace Slic3r::GUI;
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
TEST_CASE("A file URL keeps characters special to URLs in its path", "[FileUrl]")
|
||||||
|
{
|
||||||
|
const std::string path = GENERATE(as<std::string>{},
|
||||||
|
"/opt/test#dir/resources/web/homepage/index.html",
|
||||||
|
"/opt/test%20x/resources/web/homepage/index.html",
|
||||||
|
"/opt/Orca Slicer/resources/web/homepage/index.html",
|
||||||
|
"/opt/what?/resources/web/homepage/index.html",
|
||||||
|
"/home/Jos\xC3\xA9/\xE8\xB5\x84\xE6\xBA\x90/resources/web/homepage/index.html");
|
||||||
|
|
||||||
|
const wxString url = file_url_from_path(boost::filesystem::path(path));
|
||||||
|
CAPTURE(path, url.utf8_string());
|
||||||
|
|
||||||
|
// WebView::CreateWebView() and WebView::LoadUrl() re-parse the URL before loading it.
|
||||||
|
const wxURI uri(wxURI(url).BuildURI());
|
||||||
|
CHECK(uri.GetScheme() == "file");
|
||||||
|
CHECK(!uri.HasQuery());
|
||||||
|
CHECK(!uri.HasFragment());
|
||||||
|
CHECK(wxURI::Unescape(uri.GetPath()).utf8_string() == path);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A file URL of a plain path is the path behind file://", "[FileUrl]")
|
||||||
|
{
|
||||||
|
const std::string path = "/opt/OrcaSlicer/resources/web/homepage/index.html";
|
||||||
|
CHECK(file_url_from_path(boost::filesystem::path(path)) == "file://" + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A query appended to a file URL stays separate from its path", "[FileUrl]")
|
||||||
|
{
|
||||||
|
const std::string path = "/opt/test#dir%20x/resources/web/guide/0/index.html";
|
||||||
|
const wxURI uri(file_url_from_path(boost::filesystem::path(path)) + "?target=21&lang=de");
|
||||||
|
CHECK(wxURI::Unescape(uri.GetPath()).utf8_string() == path);
|
||||||
|
CHECK(uri.GetQuery() == "target=21&lang=de");
|
||||||
|
CHECK(!uri.HasFragment());
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
TEST_CASE("A file URL of a Windows path has a drive letter and forward slashes", "[FileUrl]")
|
||||||
|
{
|
||||||
|
const auto [path, url] = GENERATE(table<std::wstring, std::string>({
|
||||||
|
{ L"C:\\Program Files\\OrcaSlicer\\resources\\web\\homepage\\index.html",
|
||||||
|
"file:///C:/Program%20Files/OrcaSlicer/resources/web/homepage/index.html" },
|
||||||
|
{ L"D:\\#OneDrive\\OrcaSlicer\\resources\\web\\guide\\0\\index.html",
|
||||||
|
"file:///D:/%23OneDrive/OrcaSlicer/resources/web/guide/0/index.html" },
|
||||||
|
{ L"D:\\100%\\OrcaSlicer\\resources\\web\\homepage\\index.html",
|
||||||
|
"file:///D:/100%25/OrcaSlicer/resources/web/homepage/index.html" },
|
||||||
|
// Callers join the resources directory with a forward-slash relative path.
|
||||||
|
{ L"D:\\#OneDrive\\OrcaSlicer\\resources/web/homepage/index.html",
|
||||||
|
"file:///D:/%23OneDrive/OrcaSlicer/resources/web/homepage/index.html" },
|
||||||
|
{ L"\\\\server\\share\\OrcaSlicer\\resources\\web\\homepage\\index.html",
|
||||||
|
"file://server/share/OrcaSlicer/resources/web/homepage/index.html" },
|
||||||
|
}));
|
||||||
|
CHECK(file_url_from_path(boost::filesystem::path(path)).utf8_string() == url);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A query appended to a Windows file URL stays separate from its path", "[FileUrl]")
|
||||||
|
{
|
||||||
|
const wxURI uri(file_url_from_path(boost::filesystem::path(L"D:\\#OneDrive\\OrcaSlicer\\resources\\web\\guide\\0\\index.html")) +
|
||||||
|
"?target=21&lang=de");
|
||||||
|
CHECK(wxURI::Unescape(uri.GetPath()) == "/D:/#OneDrive/OrcaSlicer/resources/web/guide/0/index.html");
|
||||||
|
CHECK(uri.GetQuery() == "target=21&lang=de");
|
||||||
|
CHECK(!uri.HasFragment());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include <wx/uri.h>
|
||||||
|
|
||||||
namespace py = pybind11;
|
namespace py = pybind11;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -210,23 +212,33 @@ TEST_CASE("A reloaded plugin page is recognised by its base URL, fragment aside"
|
|||||||
{
|
{
|
||||||
using namespace Slic3r::GUI::web_hosting;
|
using namespace Slic3r::GUI::web_hosting;
|
||||||
|
|
||||||
// A resources path holding a space, which the web view reports escaped.
|
// A resources path holding a space, which the web view may report escaped differently.
|
||||||
const Slic3r::ScopedResourcesDir resources("web content check");
|
const Slic3r::ScopedResourcesDir resources("web content check");
|
||||||
|
|
||||||
// The swapped-in page, then after an in-page anchor and a reload.
|
// The swapped-in page, then after an in-page anchor and a reload.
|
||||||
CHECK(is_content_url(content_base_url()));
|
CHECK(is_content_url(content_base_url()));
|
||||||
CHECK(is_content_url(content_base_url() + "#tab2"));
|
CHECK(is_content_url(content_base_url() + "#tab2"));
|
||||||
wxString escaped = content_base_url();
|
const wxString unescaped = wxURI::Unescape(content_base_url());
|
||||||
escaped.Replace(" ", "%20");
|
REQUIRE(unescaped != content_base_url());
|
||||||
REQUIRE(escaped != content_base_url());
|
CHECK(is_content_url(unescaped));
|
||||||
CHECK(is_content_url(escaped));
|
CHECK(is_content_url(unescaped + "#tab2"));
|
||||||
CHECK(is_content_url(escaped + "#tab2"));
|
|
||||||
// A page the plugin linked to keeps its own URL and must be left alone.
|
// A page the plugin linked to keeps its own URL and must be left alone.
|
||||||
CHECK_FALSE(is_content_url(content_base_url() + "guide.html"));
|
CHECK_FALSE(is_content_url(content_base_url() + "guide.html"));
|
||||||
CHECK_FALSE(is_content_url("https://example.com/"));
|
CHECK_FALSE(is_content_url("https://example.com/"));
|
||||||
CHECK_FALSE(is_content_url(""));
|
CHECK_FALSE(is_content_url(""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A reloaded plugin page is recognised when the resources path holds a '#'", "[PluginHost]")
|
||||||
|
{
|
||||||
|
using namespace Slic3r::GUI::web_hosting;
|
||||||
|
|
||||||
|
const Slic3r::ScopedResourcesDir resources("web#content check");
|
||||||
|
|
||||||
|
CHECK(is_content_url(content_base_url()));
|
||||||
|
CHECK(is_content_url(content_base_url() + "#tab2"));
|
||||||
|
CHECK_FALSE(is_content_url(content_base_url() + "guide.html"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]")
|
TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]")
|
||||||
{
|
{
|
||||||
using Catch::Matchers::WithinAbs;
|
using Catch::Matchers::WithinAbs;
|
||||||
|
|||||||
Reference in New Issue
Block a user