mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-17 04:41:21 +00:00
# Description <!-- > Please provide a summary of the changes made in this PR. Include details such as: > * What issue does this PR address or fix? > * What new features or enhancements does this PR introduce? > * Are there any breaking changes or dependencies that need to be considered? --> Windows traditionally limits many file operations to `MAX_PATH` (260 characters). Deeply nested plugin packages can exceed this limit during `.whl` extraction, leaving files missing and causing the plugin to fail to load. This change adds a shared extended_path helper that prepares paths for Windows file operations using the extended-length prefixes `\\?\` or `\\?\UNC\`. The helper is used for ZIP archive I/O and extraction destinations, including plugin wheel extraction. This avoids relying on the Windows long-path setting, which changes machine-wide settings. Paths remain unprefixed outside file operations. Code that bypasses the shared ZIP helpers may still require separate long-path handling. See [Microsoft's documentation on path length limitation](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry) # Screenshots/Recordings/Graphs <!-- > Please attach relevant screenshots to showcase the UI changes. > Please attach images that can help explain the changes. --> ## Tests <!-- > Please describe the tests that you have conducted to verify the changes made in this PR. --> Tested on Windows 11 by using a portable installation of OrcaSlicer and placing it in some really long filepath length e.g. `C:/somename/<240 characters long folder path>/OrcaSlicer` and then running and trying to install any plugin locally or on OrcaCloud. <!-- > A guide for users on how to download the artifacts from this PR. --> [How to Download Pull Requests Artifacts for Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
315 lines
11 KiB
C++
315 lines
11 KiB
C++
#include <cstddef>
|
|
#include <cstdio>
|
|
#include <boost/filesystem/path.hpp>
|
|
#include <boost/filesystem/operations.hpp>
|
|
#include <boost/filesystem/file_status.hpp>
|
|
#include <exception>
|
|
#include <cstdint>
|
|
|
|
#include "miniz_extension.hpp"
|
|
#include "Utils.hpp"
|
|
|
|
#include <boost/filesystem.hpp>
|
|
#include <boost/log/trivial.hpp>
|
|
#include <string>
|
|
#include <miniz.h>
|
|
#include <stdio.h>
|
|
|
|
#if defined(_MSC_VER) || defined(__MINGW64__)
|
|
#include "boost/nowide/cstdio.hpp"
|
|
#include "boost/nowide/convert.hpp"
|
|
#endif
|
|
|
|
#include "I18N.hpp"
|
|
|
|
//! macro used to mark string used at localization,
|
|
//! return same string
|
|
#define L(s) Slic3r::I18N::translate(s)
|
|
|
|
namespace Slic3r {
|
|
|
|
namespace {
|
|
std::string decode_zip_unicode_path_extra_field(const std::string& extra, const std::string& path)
|
|
{
|
|
size_t offset = 0;
|
|
const mz_uint32 path_crc = mz_crc32(0, reinterpret_cast<const unsigned char*>(path.data()), path.size());
|
|
|
|
while (offset + 4 <= extra.size()) {
|
|
const unsigned char* field = reinterpret_cast<const unsigned char*>(extra.data() + offset);
|
|
const std::uint16_t len = field[2] | (static_cast<std::uint16_t>(field[3]) << 8);
|
|
if (offset + 4 + len > extra.size())
|
|
break;
|
|
|
|
if (field[0] == 0x75 && field[1] == 0x70 && len >= 5 && field[4] == 0x01) {
|
|
const mz_uint32 stored_crc =
|
|
static_cast<mz_uint32>(field[5]) |
|
|
(static_cast<mz_uint32>(field[6]) << 8) |
|
|
(static_cast<mz_uint32>(field[7]) << 16) |
|
|
(static_cast<mz_uint32>(field[8]) << 24);
|
|
if (stored_crc == path_crc)
|
|
return std::string(extra.data() + offset + 9, extra.data() + offset + 4 + len);
|
|
}
|
|
|
|
offset += 4 + len;
|
|
}
|
|
|
|
return Slic3r::decode_path(path.c_str());
|
|
}
|
|
|
|
bool open_zip(mz_zip_archive *zip, const char *fname, bool isread)
|
|
{
|
|
if (!zip) return false;
|
|
const char *mode = isread ? "rb" : "wb";
|
|
|
|
FILE *f = nullptr;
|
|
#if defined(_MSC_VER) || defined(__MINGW64__)
|
|
// fname is UTF-8 (the nowide contract). Route it through the shared extended-path helper so an
|
|
// archive stored past MAX_PATH opens even with LongPathsEnabled off.
|
|
const std::wstring wide_path = boost::nowide::widen(fname);
|
|
const std::string extended = boost::nowide::narrow(Slic3r::extended_path(boost::filesystem::path(wide_path)).wstring());
|
|
f = boost::nowide::fopen(extended.c_str(), mode);
|
|
#elif defined(__GNUC__) && defined(_LARGEFILE64_SOURCE)
|
|
f = fopen64(fname, mode);
|
|
#else
|
|
f = fopen(fname, mode);
|
|
#endif
|
|
|
|
if (!f) {
|
|
zip->m_last_error = MZ_ZIP_FILE_OPEN_FAILED;
|
|
return false;
|
|
}
|
|
|
|
bool res = false;
|
|
if (isread)
|
|
{
|
|
res = mz_zip_reader_init_cfile(zip, f, 0, 0);
|
|
if (!res)
|
|
// if we get here it means we tried to open a non-zip file
|
|
// we need to close the file here because the call to mz_zip_get_cfile() made into close_zip() returns a null pointer
|
|
// see: https://github.com/prusa3d/PrusaSlicer/issues/3536
|
|
fclose(f);
|
|
}
|
|
else
|
|
res = mz_zip_writer_init_cfile(zip, f, 0);
|
|
|
|
return res;
|
|
}
|
|
|
|
bool close_zip(mz_zip_archive *zip, bool isread)
|
|
{
|
|
bool ret = false;
|
|
if (zip) {
|
|
FILE *f = mz_zip_get_cfile(zip);
|
|
ret = bool(isread ? mz_zip_reader_end(zip)
|
|
: mz_zip_writer_end(zip));
|
|
if (f) fclose(f);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
#ifdef WIN32
|
|
// mz_file_write_func writing an extracted entry into an already-open FILE*.
|
|
size_t mz_write_to_cfile(void *opaque, mz_uint64 /*file_ofs*/, const void *buffer, size_t size)
|
|
{
|
|
return std::fwrite(buffer, 1, size, static_cast<FILE *>(opaque));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool open_zip_reader(mz_zip_archive *zip, const std::string &fname)
|
|
{
|
|
return open_zip(zip, fname.c_str(), true);
|
|
}
|
|
|
|
bool open_zip_writer(mz_zip_archive *zip, const std::string &fname)
|
|
{
|
|
return open_zip(zip, fname.c_str(), false);
|
|
}
|
|
|
|
bool close_zip_reader(mz_zip_archive *zip) { return close_zip(zip, true); }
|
|
bool close_zip_writer(mz_zip_archive *zip) { return close_zip(zip, false); }
|
|
|
|
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path)
|
|
{
|
|
if (!zip)
|
|
return false;
|
|
#ifdef WIN32
|
|
// Open the destination ourselves with a Windows extended-length path: miniz's narrow
|
|
// extract_to_file cannot be relied on past MAX_PATH with LongPathsEnabled off, and its wide
|
|
// variant takes a plain (unprefixed) path and a text-mode stream. This keeps the write binary.
|
|
FILE *file = ::_wfopen(Slic3r::extended_path(dest_path).wstring().c_str(), L"wb");
|
|
if (!file) {
|
|
zip->m_last_error = MZ_ZIP_FILE_OPEN_FAILED;
|
|
return false;
|
|
}
|
|
|
|
const bool written = mz_zip_reader_extract_to_callback(zip, file_index, mz_write_to_cfile, file, 0) != 0;
|
|
const bool closed = std::fclose(file) == 0;
|
|
if (!written || !closed) {
|
|
if (written && !closed)
|
|
zip->m_last_error = MZ_ZIP_FILE_CLOSE_FAILED;
|
|
// Drop the partial entry rather than leave a truncated file behind.
|
|
boost::system::error_code ec;
|
|
boost::filesystem::remove(Slic3r::extended_path(dest_path), ec);
|
|
return false;
|
|
}
|
|
return true;
|
|
#else
|
|
return mz_zip_reader_extract_to_file(zip, file_index, dest_path.string().c_str(), 0) != 0;
|
|
#endif
|
|
}
|
|
|
|
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat)
|
|
{
|
|
if (stat.m_is_utf8)
|
|
return stat.m_filename;
|
|
|
|
std::string extra(1024, 0);
|
|
const size_t extra_size = mz_zip_reader_get_extra(zip, stat.m_file_index, extra.data(), extra.size());
|
|
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 {
|
|
// Use the extended-length form for every filesystem call so entries under a destination
|
|
// past MAX_PATH are created even with LongPathsEnabled off.
|
|
const boost::filesystem::path dest_path(dest_file);
|
|
const boost::filesystem::path io_dest = extended_path(dest_path);
|
|
if (stat.m_is_directory) {
|
|
if (!boost::filesystem::exists(io_dest))
|
|
boost::filesystem::create_directories(io_dest);
|
|
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.
|
|
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(io_dest)))
|
|
boost::filesystem::remove(io_dest);
|
|
if (!extract_zip_entry_to_file(&archive, stat.m_file_index, dest_path)) {
|
|
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_zip_zero_struct(&arch);
|
|
}
|
|
|
|
std::string MZ_Archive::get_errorstr(mz_zip_error mz_err)
|
|
{
|
|
switch (mz_err)
|
|
{
|
|
case MZ_ZIP_NO_ERROR:
|
|
return "no error";
|
|
case MZ_ZIP_UNDEFINED_ERROR:
|
|
return L("undefined error");
|
|
case MZ_ZIP_TOO_MANY_FILES:
|
|
return L("too many files");
|
|
case MZ_ZIP_FILE_TOO_LARGE:
|
|
return L("File too large");
|
|
case MZ_ZIP_UNSUPPORTED_METHOD:
|
|
return L("unsupported method");
|
|
case MZ_ZIP_UNSUPPORTED_ENCRYPTION:
|
|
return L("unsupported encryption");
|
|
case MZ_ZIP_UNSUPPORTED_FEATURE:
|
|
return L("unsupported feature");
|
|
case MZ_ZIP_FAILED_FINDING_CENTRAL_DIR:
|
|
return L("failed finding central directory");
|
|
case MZ_ZIP_NOT_AN_ARCHIVE:
|
|
return L("not a ZIP archive");
|
|
case MZ_ZIP_INVALID_HEADER_OR_CORRUPTED:
|
|
return L("invalid header or corrupted");
|
|
case MZ_ZIP_UNSUPPORTED_MULTIDISK:
|
|
return L("Saving to RAID is not supported.");
|
|
case MZ_ZIP_DECOMPRESSION_FAILED:
|
|
return L("decompression failed");
|
|
case MZ_ZIP_COMPRESSION_FAILED:
|
|
return L("compression failed");
|
|
case MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE:
|
|
return L("unexpected decompressed size");
|
|
case MZ_ZIP_CRC_CHECK_FAILED:
|
|
return L("CRC check failed");
|
|
case MZ_ZIP_UNSUPPORTED_CDIR_SIZE:
|
|
return L("unsupported central directory size");
|
|
case MZ_ZIP_ALLOC_FAILED:
|
|
return L("allocation failed");
|
|
case MZ_ZIP_FILE_OPEN_FAILED:
|
|
return L("file open failed");
|
|
case MZ_ZIP_FILE_CREATE_FAILED:
|
|
return L("file create failed");
|
|
case MZ_ZIP_FILE_WRITE_FAILED:
|
|
return L("file write failed");
|
|
case MZ_ZIP_FILE_READ_FAILED:
|
|
return L("file read failed");
|
|
case MZ_ZIP_FILE_CLOSE_FAILED:
|
|
return L("file close failed");
|
|
case MZ_ZIP_FILE_SEEK_FAILED:
|
|
return L("file seek failed");
|
|
case MZ_ZIP_FILE_STAT_FAILED:
|
|
return L("file stat failed");
|
|
case MZ_ZIP_INVALID_PARAMETER:
|
|
return L("invalid parameter");
|
|
case MZ_ZIP_INVALID_FILENAME:
|
|
return L("invalid filename");
|
|
case MZ_ZIP_BUF_TOO_SMALL:
|
|
return L("Buffer too small");
|
|
case MZ_ZIP_INTERNAL_ERROR:
|
|
return L("internal error");
|
|
case MZ_ZIP_FILE_NOT_FOUND:
|
|
return L("file not found");
|
|
case MZ_ZIP_ARCHIVE_TOO_LARGE:
|
|
return L("Archive too large");
|
|
case MZ_ZIP_VALIDATION_FAILED:
|
|
return L("validation failed");
|
|
case MZ_ZIP_WRITE_CALLBACK_FAILED:
|
|
return L("write callback failed");
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return L("unknown error");
|
|
}
|
|
|
|
} // namespace Slic3r
|