diff --git a/include/system.hpp b/include/system.hpp index 036d16db..12c30002 100644 --- a/include/system.hpp +++ b/include/system.hpp @@ -2,11 +2,16 @@ #include #include +#include namespace rack { +// In C++17, this will be `std::filesystem` +namespace filesystem = std::experimental::filesystem; + + /** Cross-platform functions for operating systems routines */ namespace system { @@ -64,11 +69,11 @@ The launched process will continue running if the current process is closed. */ void runProcessDetached(const std::string& path); std::string getOperatingSystemInfo(); -/** Unzips a ZIP file to a folder. -The folder must exist. -Returns 0 if successful. +/** Extracts an archive into a folder. +Currently supports the "ustar zstd" format (.tar.zstd) */ -int unzipToFolder(const std::string& zipPath, const std::string& dir); +void unarchiveToFolder(const filesystem::path& archivePath, const filesystem::path& folderPath); +void archiveFolder(const filesystem::path& archivePath, const filesystem::path& folderPath); } // namespace system diff --git a/src/patch.cpp b/src/patch.cpp index 6a0183e2..6eb7192a 100644 --- a/src/patch.cpp +++ b/src/patch.cpp @@ -55,7 +55,7 @@ void PatchManager::save(std::string path) { INFO("Saving patch %s", path.c_str()); saveAutosave(); - // TODO Archive autosave folder + system::archiveFolder(path, asset::autosavePath); } void PatchManager::saveDialog() { @@ -141,8 +141,11 @@ void PatchManager::saveAutosave() { bool PatchManager::load(std::string path) { INFO("Loading patch %s", path.c_str()); - // TODO Extract archive to autosave + filesystem::remove_all(asset::autosavePath); + filesystem::create_directories(asset::autosavePath); + system::unarchiveToFolder(path, asset::autosavePath); + loadAutosave(); return true; } @@ -174,7 +177,7 @@ void PatchManager::loadAutosave() { FILE* file = std::fopen(patchPath.c_str(), "r"); if (!file) { // Exit silently - // TODO Load autosave + // TODO Load template return; } DEFER({ diff --git a/src/plugin.cpp b/src/plugin.cpp index 9147f6e4..b07acd37 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -197,8 +197,11 @@ static void extractPackages(std::string path) { continue; INFO("Extracting package %s", packagePath.c_str()); // Extract package - if (system::unzipToFolder(packagePath, path)) { - WARN("Package %s failed to extract", packagePath.c_str()); + try { + system::unarchiveToFolder(packagePath, path); + } + catch (Exception& e) { + WARN("Package %s failed to extract: %s", packagePath.c_str(), e.what()); message += string::f("Could not extract package %s\n", packagePath.c_str()); continue; } @@ -240,7 +243,7 @@ void init() { std::string fundamentalDir = asset::pluginsPath + "/Fundamental"; if (!settings::devMode && !getPlugin("Fundamental") && system::isFile(fundamentalSrc)) { INFO("Extracting bundled Fundamental package"); - system::unzipToFolder(fundamentalSrc.c_str(), asset::pluginsPath.c_str()); + system::unarchiveToFolder(fundamentalSrc.c_str(), asset::pluginsPath.c_str()); loadPlugin(fundamentalDir); } diff --git a/src/system.cpp b/src/system.cpp index 93c7afdf..88b04655 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -28,6 +28,8 @@ #define ZIP_STATIC #include +#include +#include #include #include @@ -428,5 +430,154 @@ int unzipToFolder(const std::string& zipPath, const std::string& dir) { } +void unarchiveToFolder(const filesystem::path& archivePath, const filesystem::path& folderPath) { + // Based on minitar.c extract() in libarchive examples + int r; + + // Open archive for reading + struct archive* a = archive_read_new(); + DEFER({archive_read_free(a);}); + archive_read_support_filter_zstd(a); + // archive_read_support_filter_all(a); + archive_read_support_format_tar(a); + // archive_read_support_format_all(a); + // TODO Fix unicode filenames on Windows? + r = archive_read_open_filename(a, archivePath.c_str(), 1 << 14); + if (r != ARCHIVE_OK) + throw Exception(string::f("unzipToFolder() could not open archive %s: %s", archivePath.c_str(), archive_error_string(a))); + DEFER({archive_read_close(a);}); + + // Open folder for writing + struct archive* disk = archive_write_disk_new(); + DEFER({archive_write_free(disk);}); + int flags = ARCHIVE_EXTRACT_TIME; + archive_write_disk_set_options(disk, flags); + // archive_write_disk_set_standard_lookup(disk); + DEFER({archive_write_close(disk);}); + + // Iterate archive + for (;;) { + // Get next entry + struct archive_entry* entry; + r = archive_read_next_header(a, &entry); + if (r == ARCHIVE_EOF) + break; + if (r < ARCHIVE_OK) + throw Exception(string::f("unzipToFolder() could not read entry from archive: %s", archive_error_string(a))); + + // Set pathname relative to folderPath + filesystem::path entryPath = archive_entry_pathname(entry); + if (!entryPath.is_relative()) + throw Exception(string::f("unzipToFolder() does not support absolute paths: %s", entryPath.c_str())); + entryPath = filesystem::absolute(entryPath, folderPath); + archive_entry_set_pathname(entry, entryPath.c_str()); + + // Write entry to disk + r = archive_write_header(disk, entry); + if (r < ARCHIVE_OK) + throw Exception(string::f("unzipToFolder() could not write file to folder: %s", archive_error_string(disk))); + + // Copy data to file + for (;;) { + const void* buf; + size_t size; + int64_t offset; + // Read data from archive + r = archive_read_data_block(a, &buf, &size, &offset); + if (r == ARCHIVE_EOF) + break; + if (r < ARCHIVE_OK) + throw Exception(string::f("unzipToFolder() could not read data from archive", archive_error_string(a))); + + // Write data to file + r = archive_write_data_block(disk, buf, size, offset); + if (r < ARCHIVE_OK) + throw Exception(string::f("unzipToFolder() could not write data to file", archive_error_string(disk))); + } + + // Close file + r = archive_write_finish_entry(disk); + if (r < ARCHIVE_OK) + throw Exception(string::f("unzipToFolder() could not close file", archive_error_string(disk))); + } +} + + +/** Behaves like `std::filesystem::relative()`. +Limitation: `p` must be a descendant of `base`. Doesn't support adding `../` to the return path. +*/ +static filesystem::path getRelativePath(filesystem::path p, filesystem::path base = filesystem::current_path()) { + p = filesystem::absolute(p); + base = filesystem::absolute(base); + std::string pStr = p; + std::string baseStr = base; + if (pStr.size() < baseStr.size()) + throw Exception("getRelativePath() error: path is shorter than base"); + if (!std::equal(baseStr.begin(), baseStr.end(), pStr.begin())) + throw Exception("getRelativePath() error: path does not begin with base"); + + // If p == base, this correctly returns "." + return "." + std::string(pStr.begin() + baseStr.size(), pStr.end()); +} + + +void archiveFolder(const filesystem::path& archivePath, const filesystem::path& folderPath){ + // Based on minitar.c create() in libarchive examples + int r; + + // Open archive for writing + struct archive* a = archive_write_new(); + DEFER({archive_write_free(a);}); + archive_write_set_format_ustar(a); + archive_write_add_filter_zstd(a); + r = archive_write_open_filename(a, archivePath.c_str()); + if (r != ARCHIVE_OK) + throw Exception(string::f("archiveFolder() could not open archive %s for writing: %s", archivePath.c_str(), archive_error_string(a))); + DEFER({archive_write_close(a);}); + + // Open folder for reading + struct archive* disk = archive_read_disk_new(); + DEFER({archive_read_free(disk);}); + r = archive_read_disk_open(disk, folderPath.c_str()); + if (r != ARCHIVE_OK) + throw Exception(string::f("archiveFolder() could not open folder %s for reading: %s", folderPath.c_str(), archive_error_string(a))); + DEFER({archive_read_close(a);}); + + // Iterate folder + for (;;) { + struct archive_entry* entry = archive_entry_new(); + DEFER({archive_entry_free(entry);}); + + r = archive_read_next_header2(disk, entry); + if (r == ARCHIVE_EOF) + break; + if (r != ARCHIVE_OK) + throw Exception(string::f("archiveFolder() could not get next entry from archive: %s", archive_error_string(disk))); + + // Recurse dirs + archive_read_disk_descend(disk); + + // Convert absolute path to relative path + filesystem::path entryPath = archive_entry_pathname(entry); + entryPath = getRelativePath(entryPath, folderPath); + archive_entry_set_pathname(entry, entryPath.c_str()); + + // Write file to archive + r = archive_write_header(a, entry); + if (r != ARCHIVE_OK) + throw Exception("archiveFolder() could not write entry to archive"); + + // Manually copy data + FILE* f = std::fopen(archive_entry_sourcepath(entry), "rb"); + DEFER({std::fclose(f);}); + static char buf[1 << 14]; + ssize_t len; + while ((len = std::fread(buf, 1, sizeof(buf), f)) > 0) { + archive_write_data(a, buf, len); + } + } +} + + } // namespace system } // namespace rack