archive.cpp (8188B)
1 /* ReaPack: Package manager for REAPER 2 * Copyright (C) 2015-2025 Christian Fillion 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU Lesser General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU Lesser General Public License for more details. 13 * 14 * You should have received a copy of the GNU Lesser General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 #include "archive.hpp" 19 20 #include "config.hpp" 21 #include "errors.hpp" 22 #include "filesystem.hpp" 23 #include "index.hpp" 24 #include "path.hpp" 25 #include "reapack.hpp" 26 #include "transaction.hpp" 27 #include "win32.hpp" 28 29 #include <fstream> 30 #include <iomanip> 31 #include <sstream> 32 33 #include <zip.h> 34 #include <unzip.h> 35 #include <ioapi.h> 36 37 static const Path ARCHIVE_TOC("toc"); 38 static const size_t BUFFER_SIZE = 4096; 39 40 #ifdef _WIN32 41 static void *wide_fopen(voidpf, const void *filename, int mode) 42 { 43 const wchar_t *fopen_mode = nullptr; 44 45 if((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ) 46 fopen_mode = L"rb"; 47 else if(mode & ZLIB_FILEFUNC_MODE_EXISTING) 48 fopen_mode = L"r+b"; 49 else if(mode & ZLIB_FILEFUNC_MODE_CREATE) 50 fopen_mode = L"wb"; 51 52 FILE *file = nullptr; 53 54 if(filename && fopen_mode) { 55 const auto &&wideFilename = Win32::widen(static_cast<const char *>(filename)); 56 _wfopen_s(&file, wideFilename.c_str(), fopen_mode); 57 } 58 59 return file; 60 } 61 #endif 62 63 struct ImportArchive { 64 void importRemote(const std::string &); 65 void importPackage(const std::string &); 66 67 ArchiveReaderPtr m_reader; 68 RemoteList *m_remotes; 69 Transaction *m_tx; 70 IndexPtr m_lastIndex; 71 }; 72 73 void Archive::import(const std::string &path) 74 { 75 ImportArchive state{std::make_shared<ArchiveReader>(path), 76 &g_reapack->config()->remotes}; 77 78 std::stringstream toc; 79 if(const int err = state.m_reader->extractFile(ARCHIVE_TOC, toc)) 80 throw reapack_error(String::format( 81 "Cannot locate the table of contents (%d)", err)); 82 83 // starting import, do not abort process (eg. by throwing) at this point 84 if(!(state.m_tx = g_reapack->setupTransaction())) 85 return; 86 87 std::string line; 88 while(std::getline(toc, line)) { 89 if(line.size() <= 5) // 5 is the length of the line type prefix 90 continue; 91 92 const std::string &data = line.substr(5); 93 94 try { 95 switch(line[0]) { 96 case 'R': 97 state.importRemote(data); 98 break; 99 case 'P': 100 state.importPackage(data); 101 break; 102 default: 103 throw reapack_error(String::format("Unknown token '%s' (skipping)", 104 line.substr(0, 4).c_str())); 105 } 106 } 107 catch(const reapack_error &e) { 108 state.m_tx->receipt()->addError({e.what(), path}); 109 } 110 } 111 112 g_reapack->config()->write(); 113 state.m_tx->runTasks(); 114 } 115 116 void ImportArchive::importRemote(const std::string &data) 117 { 118 m_lastIndex = nullptr; // clear the previous repository 119 Remote remote = Remote::fromString(data); 120 121 if(const int err = m_reader->extractFile(Index::pathFor(remote.name()))) { 122 throw reapack_error(String::format("Failed to extract index of %s (%d)", 123 remote.name().c_str(), err)); 124 } 125 126 const Remote &original = m_remotes->get(remote.name()); 127 if(original.isProtected()) { 128 remote.setUrl(original.url()); 129 remote.protect(); 130 } 131 132 m_remotes->add(remote); 133 m_lastIndex = Index::load(remote.name()); 134 } 135 136 void ImportArchive::importPackage(const std::string &data) 137 { 138 // don't report an error if the index isn't loaded assuming we already 139 // did when failing to import the repository above 140 if(!m_lastIndex) 141 return; 142 143 std::string categoryName, packageName, versionName; 144 int flags; 145 146 std::istringstream stream(data); 147 stream 148 >> quoted(categoryName) >> quoted(packageName) >> quoted(versionName) 149 >> flags; 150 151 const Package *pkg = m_lastIndex->find(categoryName, packageName); 152 const Version *ver = pkg ? pkg->findVersion(versionName) : nullptr; 153 154 if(!ver) { 155 throw reapack_error(String::format( 156 "%s/%s/%s v%s cannot be found or is incompatible with your operating system.", 157 m_lastIndex->name().c_str(), categoryName.c_str(), 158 packageName.c_str(), versionName.c_str())); 159 } 160 161 m_tx->install(ver, flags, m_reader); 162 } 163 164 ArchiveReader::ArchiveReader(const Path &path) 165 { 166 zlib_filefunc64_def filefunc; 167 fill_fopen64_filefunc(&filefunc); 168 #ifdef _WIN32 169 filefunc.zopen64_file = wide_fopen; 170 #endif 171 172 m_zip = unzOpen2_64(path.join().c_str(), &filefunc); 173 174 if(!m_zip) 175 throw reapack_error(FS::lastError()); 176 } 177 178 ArchiveReader::~ArchiveReader() 179 { 180 unzClose(m_zip); 181 } 182 183 int ArchiveReader::extractFile(const Path &path) 184 { 185 std::ofstream stream; 186 187 if(FS::open(stream, path)) 188 return extractFile(path, stream); 189 else { 190 throw reapack_error(String::format("%s: %s", 191 path.join().c_str(), FS::lastError())); 192 } 193 } 194 195 int ArchiveReader::extractFile(const Path &path, std::ostream &stream) noexcept 196 { 197 int status = unzLocateFile(m_zip, path.join(false).c_str(), false); 198 if(status != UNZ_OK) 199 return status; 200 201 status = unzOpenCurrentFile(m_zip); 202 if(status != UNZ_OK) 203 return status; 204 205 std::string buffer(BUFFER_SIZE, 0); 206 207 const auto readChunk = [&] { 208 return unzReadCurrentFile(m_zip, &buffer[0], static_cast<int>(buffer.size())); 209 }; 210 211 while(const int len = readChunk()) { 212 if(len < 0) 213 return len; // read error 214 215 stream.write(&buffer[0], len); 216 } 217 218 return unzCloseCurrentFile(m_zip); 219 } 220 221 FileExtractor::FileExtractor(const Path &target, const ArchiveReaderPtr &reader) 222 : m_path(target), m_reader(reader) 223 { 224 setSummary({ "Extracting", target.join() }); 225 } 226 227 bool FileExtractor::run() 228 { 229 std::ofstream stream; 230 if(!FS::open(stream, m_path.temp())) { 231 setError({FS::lastError(), m_path.temp().join()}); 232 return false; 233 } 234 235 const int error = m_reader->extractFile(m_path.target(), stream); 236 stream.close(); 237 238 if(error) { 239 setError({String::format("Failed to extract file (%d)", error), 240 m_path.target().join()}); 241 return false; 242 } 243 244 return true; 245 } 246 247 ArchiveWriter::ArchiveWriter(const Path &path) 248 { 249 zlib_filefunc64_def filefunc; 250 fill_fopen64_filefunc(&filefunc); 251 #ifdef _WIN32 252 filefunc.zopen64_file = wide_fopen; 253 #endif 254 255 m_zip = zipOpen2_64(path.join().c_str(), APPEND_STATUS_CREATE, nullptr, &filefunc); 256 257 if(!m_zip) 258 throw reapack_error(FS::lastError()); 259 } 260 261 ArchiveWriter::~ArchiveWriter() 262 { 263 zipClose(m_zip, nullptr); 264 } 265 266 int ArchiveWriter::addFile(const Path &path) 267 { 268 std::ifstream stream; 269 270 if(FS::open(stream, path)) 271 return addFile(path, stream); 272 else { 273 throw reapack_error(String::format("%s: %s", 274 path.join().c_str(), FS::lastError())); 275 } 276 } 277 278 int ArchiveWriter::addFile(const Path &path, std::istream &stream) noexcept 279 { 280 const int status = zipOpenNewFileInZip(m_zip, path.join(false).c_str(), nullptr, 281 nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, Z_DEFAULT_COMPRESSION); 282 283 if(status != ZIP_OK) 284 return status; 285 286 std::string buffer(BUFFER_SIZE, 0); 287 288 const auto readChunk = [&] { 289 stream.read(&buffer[0], buffer.size()); 290 return static_cast<int>(stream.gcount()); 291 }; 292 293 while(const int len = readChunk()) { 294 if(len < 0) 295 return len; // write error 296 297 zipWriteInFileInZip(m_zip, &buffer[0], len); 298 } 299 300 return zipCloseFileInZip(m_zip); 301 } 302 303 FileCompressor::FileCompressor(const Path &target, const ArchiveWriterPtr &writer) 304 : m_path(target), m_writer(writer) 305 { 306 setSummary({ "Compressing", target.join() }); 307 } 308 309 bool FileCompressor::run() 310 { 311 std::ifstream stream; 312 if(!FS::open(stream, m_path)) { 313 setError({ 314 String::format("Could not open file for export (%s)", FS::lastError()), 315 m_path.join()}); 316 return false; 317 } 318 319 const int error = m_writer->addFile(m_path, stream); 320 stream.close(); 321 322 if(error) { 323 setError({String::format("Failed to compress file (%d)", error), m_path.join()}); 324 return false; 325 } 326 327 return true; 328 }