diff --git a/meson.build b/meson.build index fa8aedd..0b2df58 100644 --- a/meson.build +++ b/meson.build @@ -4,6 +4,8 @@ project('xwim', 'cpp', 'warning_level=3', 'b_ndebug=if-release']) +add_global_arguments('-DVERSION='+meson.version(), language: 'cpp') + subdir('src') subdir('doc') subdir('test') diff --git a/src/archive.cpp b/src/archive.cpp deleted file mode 100644 index 831c5e9..0000000 --- a/src/archive.cpp +++ /dev/null @@ -1,128 +0,0 @@ -#include -#include - -namespace logger = spdlog; - -#include -#include - -#include -#include -#include -#include - -#include "archive_sys.hpp" -#include "archive.hpp" -#include "spec.hpp" -#include "fileformats.hpp" - -namespace xwim { - -static void _spec_is_root_filename(ArchiveSpec* spec, - ArchiveEntryView entry, - std::filesystem::path* filepath) { - auto entry_path = entry.path(); - auto norm_stem = filepath->filename(); - norm_stem = xwim::stem(norm_stem); - - if (*entry_path.begin() != norm_stem) { - logger::debug("Archive root does not match archive name"); - spec->is_root_filename = false; - } else { - logger::debug("Archive root matches archive name"); - spec->is_root_filename = true; - } - logger::debug("\t-> Archive root: {}", entry_path.begin()->string()); - logger::debug("\t-> Archive stem: {}", norm_stem.string()); -} - -static void _spec_is_root_dir(ArchiveSpec* spec, ArchiveEntryView entry) { - if (entry.is_directory()) { - logger::debug("Archive root is directory"); - spec->is_root_dir = true; - } else { - logger::debug("Archive root is not a directory"); - spec->is_root_dir = false; - } - logger::debug("\t-> Archive mode_t: {0:o}", entry.file_type()); -} - -static void _spec_has_single_root(ArchiveSpec* spec, - ArchiveEntryView first_entry, - ArchiveReaderSys& archive_reader) { - std::filesystem::path first_entry_root = *(first_entry.path().begin()); - logger::trace("Testing roots"); - - spec->has_single_root = true; - - while (archive_reader.advance()) { - ArchiveEntryView entry = archive_reader.cur(); - - auto next_entry = entry.path(); - logger::trace("Path: {}, Root: {}", next_entry.string(), - next_entry.begin()->string()); - - if (first_entry_root != *next_entry.begin()) { - logger::debug("Archive has multiple roots"); - logger::debug("\t-> Archive root I: {}", - first_entry_root.begin()->string()); - logger::debug("\t-> Archive root II: {}", next_entry.begin()->string()); - - spec->has_single_root = false; - break; - } - } - - if (spec->has_single_root) - logger::debug("Archive has single root: {}", first_entry_root.string()); -} - -Archive::Archive(std::filesystem::path path) : path{path} {} - -ArchiveSpec Archive::check() { - logger::trace("Creating archive spec for {}", this->path.string()); - - ArchiveReaderSys archive_reader {this->path}; - - ArchiveSpec archive_spec; - - if (!archive_reader.advance()) { // can't advance even once, archive is empty - logger::debug("Archive is empty"); - return {false, false, false}; - } - - ArchiveEntryView first_entry = archive_reader.cur(); - - logger::trace("Found archive entry {}", first_entry.path_name()); - - _spec_is_root_filename(&archive_spec, first_entry, &this->path); - _spec_is_root_dir(&archive_spec, first_entry); - _spec_has_single_root(&archive_spec, first_entry, archive_reader); - - return archive_spec; -} - -void Archive::extract(ExtractSpec extract_spec) { - std::filesystem::path abs_path = std::filesystem::absolute(this->path); - - std::unique_ptr extractor; - - if(extract_spec.make_dir) { - logger::trace("Creating extract directory {}", extract_spec.dirname.string()); - extractor = std::unique_ptr(new ArchiveExtractorSys{extract_spec.dirname}); - } else { - extractor = std::unique_ptr(new ArchiveExtractorSys{}); - } - - ArchiveReaderSys reader{abs_path}; - extractor->extract_all(reader); -} - -void Archive::compress(CompressSpec compress_spec) { - std::filesystem::path abs_path = std::filesystem::absolute(this->path); - - ArchiveCompressorSys compressor{abs_path, compress_spec}; - compressor.compress(); -} - -} // namespace xwim diff --git a/src/archive.hpp b/src/archive.hpp deleted file mode 100644 index fccd250..0000000 --- a/src/archive.hpp +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include - -#include "spec.hpp" - -namespace xwim { - -/** Class for interacting with archives */ -class Archive { - private: - std::filesystem::path path; - - public: - explicit Archive(std::filesystem::path path); - - /** Generate an ArchiveSpec by analysing the archive at `path` - * - * @returns ArchiveSpec for the archive - */ - ArchiveSpec check(); - - /** Extract the archive at `path` according to given ExtractSpec */ - void extract(ExtractSpec extract_spec); - - /** Compress the archive at `path` according to given CompressSpec */ - void compress(CompressSpec compress_spec); -}; - -class ArchiveException : public std::exception { - private: - std::string _what; - - public: - ArchiveException(std::string what, archive* archive) { - if (archive_error_string(archive)) { - _what = fmt::format("{}: {}", what, archive_error_string(archive)); - } else { - _what = fmt::format("{}", what); - } - } - - virtual const char* what() const noexcept - { return this->_what.c_str(); } -}; - -} // namespace xwim diff --git a/src/archive_sys.cpp b/src/archive_sys.cpp deleted file mode 100644 index e46b1bc..0000000 --- a/src/archive_sys.cpp +++ /dev/null @@ -1,297 +0,0 @@ -#include -#include -#include - -#include "archive.hpp" -#include "fileformats.hpp" -#include "spec.hpp" -namespace logger = spdlog; - -#include - -#include -#include - -#include "archive_sys.hpp" - -namespace fs = std::filesystem; - -bool xwim::ArchiveEntryView::is_empty() { return (this->ae == nullptr); } - -std::string xwim::ArchiveEntryView::path_name() { - if (!this->ae) throw ArchiveSysException{"Access to invalid archive entry"}; - - return archive_entry_pathname(this->ae); -} - -fs::path xwim::ArchiveEntryView::path() { - if (!this->ae) throw ArchiveSysException{"Access to invalid archive entry"}; - return fs::path{this->path_name()}; -} - -mode_t xwim::ArchiveEntryView::file_type() { - if (!this->ae) throw ArchiveSysException{"Access to invalid archive entry"}; - return archive_entry_filetype(this->ae); -} - -bool xwim::ArchiveEntryView::is_directory() { - return S_ISDIR(this->file_type()); -} - -xwim::ArchiveReaderSys::ArchiveReaderSys(fs::path &path) { - int r; // libarchive error handling - - logger::trace("Setting up archive reader"); - this->ar = archive_read_new(); - archive_read_support_filter_all(this->ar); - archive_read_support_format_all(this->ar); - - logger::trace("Reading archive at {}", path.c_str()); - r = archive_read_open_filename(this->ar, path.c_str(), 10240); - if (r != ARCHIVE_OK) - throw ArchiveSysException{"Could not open archive file", this->ar}; - - logger::trace("Archive read succesfully"); -} - -xwim::ArchiveReaderSys::~ArchiveReaderSys() { - logger::trace("Destructing ArchiveReaderSys"); - - if (this->ar) archive_read_free(this->ar); -} - -bool xwim::ArchiveReaderSys::advance() { - int r; // libarchive error handling - logger::trace("Advancing reader to next archive entry"); - - r = archive_read_next_header(this->ar, &this->ae); - if (r == ARCHIVE_EOF) { - this->ae = nullptr; - return false; - } - if (r != ARCHIVE_OK) - throw(ArchiveSysException{"Could not list archive", this->ar}); - - logger::trace("Got entry {}", archive_entry_pathname(ae)); - return true; -} - -const xwim::ArchiveEntryView xwim::ArchiveReaderSys::cur() { - return ArchiveEntryView{this->ae}; -} - -xwim::ArchiveExtractorSys::ArchiveExtractorSys(fs::path &root) { - logger::trace("Constructing ArchiveExtractorSys with path {}", root.string()); - - fs::create_directories(root); - fs::current_path(root); - - this->writer = archive_write_disk_new(); - archive_write_disk_set_standard_lookup(this->writer); - - logger::trace("Constructed ArchiveExtractorSys at {:p}", - (void *)this->writer); -} - -xwim::ArchiveExtractorSys::ArchiveExtractorSys() { - logger::trace("Construction ArchiveExtractorSys without root"); - - this->writer = archive_write_disk_new(); - archive_write_disk_set_standard_lookup(this->writer); - logger::trace("Constructed ArchiveExtractorSys at {:p}", - (void *)this->writer); -} - -void xwim::ArchiveExtractorSys::extract_all(xwim::ArchiveReaderSys &reader) { - while (reader.advance()) { - this->extract_entry(reader); - } -} - -// forward declared -static int copy_data(struct archive *ar, struct archive *aw); - -void xwim::ArchiveExtractorSys::extract_entry(xwim::ArchiveReaderSys &reader) { - int r; - r = archive_write_header(this->writer, reader.ae); - if (r != ARCHIVE_OK) { - throw(ArchiveSysException("Could not extract entry", reader.ar)); - } - - r = copy_data(reader.ar, this->writer); - if (r != ARCHIVE_OK) { - throw(ArchiveSysException("Could not extract entry", reader.ar)); - } -} - -xwim::ArchiveExtractorSys::~ArchiveExtractorSys() { - logger::trace("Destructing ArchiveExtractorSys at {:p}", - (void *)this->writer); - if (this->writer) { - archive_write_close(this->writer); - archive_write_free(this->writer); - } -} - -xwim::ArchiveCompressorSys::ArchiveCompressorSys( - fs::path &root, xwim::CompressSpec compress_spec) - : root{root}, compress_spec{compress_spec} { - this->new_archive = archive_write_new(); - - for (xwim::archive_filter filter : this->compress_spec.filters) { - archive_write_add_filter(this->new_archive, filter); - } - - archive_write_set_format(this->new_archive, this->compress_spec.format); -} - -// forward declared -static fs::path archive_path_norm(const fs::path &root, - const xwim::CompressSpec &compress_spec); - -void xwim::ArchiveCompressorSys::compress() { - fs::path archive_path = archive_path_norm(this->root, this->compress_spec); - - logger::debug("Writing archive at: {}", archive_path.filename().c_str()); - - archive_write_open_filename(this->new_archive, - archive_path.filename().c_str()); - - archive *disk = archive_read_disk_new(); - archive_read_disk_set_standard_lookup(disk); - - int r; - - r = archive_read_disk_open(disk, fs::relative(this->root).c_str()); - if (r != ARCHIVE_OK) { - throw ArchiveSysException("Could not open path for archiving", disk); - } - - archive_entry *entry; - char buff[16384]; - - for (;;) { - entry = archive_entry_new(); - r = archive_read_next_header2(disk, entry); - if (r == ARCHIVE_EOF) break; - if (r != ARCHIVE_OK) { - throw ArchiveSysException("Could not read next archive entry", disk); - } - - archive_read_disk_descend(disk); - - const char* ae_path = archive_entry_pathname(entry); - fs::path ae_rel_path = fs::relative(fs::path(ae_path), this->root.parent_path()); - archive_entry_set_pathname(entry, ae_rel_path.c_str()); - logger::trace("Processing entry {}", archive_entry_pathname(entry)); - - r = archive_write_header(this->new_archive, entry); - if (r < ARCHIVE_OK) { - throw ArchiveSysException("Could not write header for archive entry", - this->new_archive); - } - - if (r > ARCHIVE_FAILED) { - int fd = open(archive_entry_sourcepath(entry), O_RDONLY); - ssize_t len = read(fd, buff, sizeof(buff)); - while (len > 0) { - archive_write_data(this->new_archive, buff, len); - len = read(fd, buff, sizeof(buff)); - } - close(fd); - } - logger::trace("Entry written {}", archive_entry_pathname(entry)); - archive_entry_free(entry); - } -} - -xwim::ArchiveCompressorSys::~ArchiveCompressorSys() { - logger::trace("Destructing ArchiveExtractorSys at {:p}", - (void *)this->new_archive); - if (this->new_archive) { - archive_write_close(this->new_archive); - archive_write_free(this->new_archive); - } -} - -/** Creates an archive path from the path to compress and normalizes it - * - * Note that currently only single arguments are allowed for `xwim` to - * minimize ambiguity. - * - * The archive path is determined from the argument file/directory by: - * 1. If file: - * 1.1. Stem the filename - * 1.2. Append an extension appropriate for the archive format (from the - * spec) - * 2. If directory: - * 2.1. Remove any trailing '/' - * 2.2. Append an extension appropriate for the archive format (from the - * spec) - */ -static fs::path archive_path_norm(const fs::path &root, - const xwim::CompressSpec &compress_spec) { - fs::path archive_path{root}; - fs::file_status archive_path_stat = fs::status(archive_path); - - std::set known_types = {fs::file_type::directory, fs::file_type::regular}; - fs::perms flag_mask = - fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read; - - if (!fs::exists(archive_path)) { - logger::error("Non-existing path: {}", archive_path.string()); - throw xwim::ArchiveSysException{"Path does not exists"}; - } - - if (!known_types.count(archive_path_stat.type())) { - logger::error("Unknown path type: {}", archive_path_stat.type()); - throw xwim::ArchiveSysException{"Unknown path type"}; - } - - if ((archive_path_stat.permissions() & flag_mask) == fs::perms::none) { - logger::error("Cannot read path with permissions: {}", - archive_path_stat.permissions()); - throw xwim::ArchiveSysException{"Unreadable path"}; - } - - if (archive_path_stat.type() == fs::file_type::regular) { - while (archive_path.has_extension()) { - archive_path.replace_extension(); - } - } - - if (archive_path_stat.type() == fs::file_type::directory) { - if (archive_path.string().back() == '/') { - logger::trace("Found trailing / in path"); - std::string ps = archive_path.string(); - ps.erase(ps.size() - 1, 1); - - archive_path = fs::path{ps}; - logger::trace("Normalized path to {}", archive_path.string()); - } - } - - archive_path.concat(compress_spec.extension); - return archive_path; -} - -static int copy_data(struct archive *ar, struct archive *aw) { - int r; - const void *buff; - size_t size; - int64_t offset; - - for (;;) { - r = archive_read_data_block(ar, &buff, &size, &offset); - if (r == ARCHIVE_EOF) { - return (ARCHIVE_OK); - } - if (r != ARCHIVE_OK) { - return (r); - } - r = archive_write_data_block(aw, buff, size, offset); - if (r != ARCHIVE_OK) { - return (r); - } - } -} diff --git a/src/archive_sys.hpp b/src/archive_sys.hpp deleted file mode 100644 index 2818d17..0000000 --- a/src/archive_sys.hpp +++ /dev/null @@ -1,119 +0,0 @@ -#pragma once - -#include - -#include -#include -#include "spec.hpp" -#include - -namespace xwim { - -/** A view into an archive entry - * - * The view is non-owning and the caller must guarantee - * that the parent archive entry is valid when the view - * is accessed. - */ -class ArchiveEntryView { - private: - archive_entry* ae; - - public: - ArchiveEntryView() = default; - ArchiveEntryView(archive_entry* entry) : ae{entry} {} - - bool is_empty(); - std::string path_name(); - std::filesystem::path path(); - mode_t file_type(); - bool is_directory(); -}; - -/** A reader for archive files - * - * Shim for `libarchive`. Iterates through - * entries of an archive with `next()` - */ -class ArchiveReaderSys { - private: - archive* ar; - archive_entry* ae; - friend class ArchiveExtractorSys; - - public: - ArchiveReaderSys(std::filesystem::path& path); - ~ArchiveReaderSys(); - - /** Advances the internal entry pointer - * - * @return true if the pointer advanced to the next entry - * false if the end of the archive was reached - */ - bool advance(); - - /** Returns a non-owning view of the current entry - * - * ArchiveEntryView is a non-owning view of the currently - * active entry in this reader. A retrieved archive entry - * may not be used after another call to advance in the - * same reader. - * - * @return a view to the archive entry this reader currently - * points to - */ - const ArchiveEntryView cur(); -}; - -/** A extractor for archive files - * - * Shim for `libarchive`. - */ -class ArchiveExtractorSys { - private: - archive* writer; - - public: - ArchiveExtractorSys(std::filesystem::path& root); - ArchiveExtractorSys(); - ~ArchiveExtractorSys(); - - void extract_all(ArchiveReaderSys& reader); - void extract_entry(ArchiveReaderSys& reader); -}; - -/** A compressor for archive files - * - * Shim for `libarchive` - */ -class ArchiveCompressorSys { -private: - archive* new_archive; - std::filesystem::path root; - xwim::CompressSpec compress_spec; - -public: - ArchiveCompressorSys(std::filesystem::path& root, xwim::CompressSpec compress_spec); - ~ArchiveCompressorSys(); - - void compress(); -}; - -class ArchiveSysException : public std::exception { - private: - std::string _what; - - public: - ArchiveSysException(std::string what, archive* archive) { - if (archive_error_string(archive)) { - _what = fmt::format("{}: {}", what, archive_error_string(archive)); - } else { - _what = fmt::format("{}", what); - } - } - ArchiveSysException(std::string what) { _what = fmt::format("{}", what); } - - virtual const char* what() const noexcept { return this->_what.c_str(); } -}; - -} // namespace xwim diff --git a/src/argparse.cpp b/src/argparse.cpp new file mode 100644 index 0000000..3f78e08 --- /dev/null +++ b/src/argparse.cpp @@ -0,0 +1,84 @@ +#include "argparse.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "archivinfo.hpp" +#include "fileformats.hpp" + +using namespace TCLAP; +using namespace xwim; +namespace fs = std::filesystem; + +template <> +struct TCLAP::ArgTraits { + typedef ValueLike ValueCategory; +}; + +ArgParse::ArgParse() + : cmd{"xwim - Do What I Mean Extractor", ' ', "0.3.0"}, + arg_compress{"c", "compress", "Compress ", false}, + arg_extract{"x", "extract", "Extract ", false}, + arg_outfile{"o", "out", "Out ", + false, fs::path{}, "A path on the filesystem"}, + arg_infiles{"Files", "Archive to extract or files to compress", true, + "A path on the filesystem"} { + cmd.xorAdd(arg_compress, arg_extract); + cmd.add(arg_outfile); + cmd.add(arg_infiles); +}; + +void ArgParse::parse(int argc, char** argv) { + try { + cmd.parse(argc, argv); + } catch (ArgException& e) { + throw new xwim::ArgParseException(e.error()); + } + + this->extract = parse_extract(); + this->outfile = arg_outfile.getValue(); + this->infiles = arg_infiles.getValue(); +} + +bool ArgParse::parse_extract() { + // extract/compress explicitly given; xor ensured in `cmd` + if (this->arg_compress.getValue()) { + return false; + } else if (this->arg_extract.getValue()) { + return true; + } + + // Not explicitly given, check if we can guess from input + + // An outfile is given + if (this->arg_outfile.isSet()) { + // outfile looks like an archive + if (xwim::archivinfo::has_known_extension(this->arg_outfile.getValue())) { + return false; + } + + // outfile is not a known archive, assume it meant as folder for extraction + else { + return true; + } + } + + // one infile which is an archive, so intention is probably to extract this + if (this->arg_infiles.getValue().size() == 1 && + xwim::archivinfo::is_archive(this->arg_infiles.getValue().at(0))) { + return true; + } + + // all other cases, in particular multiple infiles, assume we want to compress + return false; +} +bool ArgParse::compressp() { return !this->extract; } +bool ArgParse::extractp() { return this->extract; } diff --git a/src/argparse.hpp b/src/argparse.hpp new file mode 100644 index 0000000..dd0a372 --- /dev/null +++ b/src/argparse.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace xwim { +class ArgParse { + private: + bool extract; + std::filesystem::path outfile; + std::vector infiles; + + TCLAP::CmdLine cmd; + TCLAP::SwitchArg arg_compress; + TCLAP::SwitchArg arg_extract; + TCLAP::ValueArg arg_outfile; + TCLAP::UnlabeledMultiArg arg_infiles; + + protected: + bool parse_extract(); + + public: + ArgParse(); + void parse(int argc, char** argv); + bool compressp(); + bool extractp(); +}; + +class ArgParseException : public std::exception { + private: + std::string _what; + + public: + ArgParseException(std::string what) : _what{what} {}; + template + ArgParseException(std::string fmt_string, Args&&... args) : _what{fmt::format(fmt_string, args...)} {}; + virtual const char* what() const noexcept { return this->_what.c_str(); } +}; +} // namespace xwim diff --git a/src/fileformats.hpp b/src/fileformats.hpp deleted file mode 100644 index 32bf011..0000000 --- a/src/fileformats.hpp +++ /dev/null @@ -1,149 +0,0 @@ -/** @file fileformats.hpp - * @brief Handle archive extensions - */ -#pragma once - -#include -#include -namespace logger = spdlog; - -#include -#include -#include - -namespace xwim { - -/** Common archive formats understood by xwim - * - * The underlying libarchive backend retrieves format information by a process - * called `bidding`. Hence, this information is mainly used to strip extensions. - * - * Stripping extensions via `std::filesystem::path` does not work reliably since - * it gets easily confused by dots in the regular file name. - */ -const std::set fileformats{".7z", ".7zip", ".jar", ".tgz", - ".bz2", ".bzip2", ".gz", ".gzip", - ".rar", ".tar", ".xz", ".zip"}; - -/** Archive filters - * - * Archive filters are essentially either data compression algorithms or data - * encodings. Filters are used on archives after an archiving program created - * the archive out of files and folders. Multiple filters can be applied to an - * archive. The order is significant. - * - * The simplest way to understand the distinction between filters and formats is - * to visualize the traditional `tar.gz` format. Tar creates the archive (an - * archive format). Gzip compresses the archive (an archive filter). In theory - * one could create a `tar.gz.lz.uu` tarball. That is, a `tar` archive - * filter-compressed with `gzip`, filter-compressed with `lzip`, filter-encoded - * with `uuencode`. - * - * Note that while this abstraction works in many cases it is not perfect. For - * example `.zip` files are traditionally archives where every entry is - * compressed separately and then bundled them together into an archive. In - * those cases the archive format is ZIP with no (external) filters. - */ -enum archive_filter { - NONE = 0, - GZIP = 1, - BZIP2 = 2, - COMPRESS = 3, - PROGRAM = 4, - LZMA = 5, - XZ = 6, - UU = 7, - RPM = 8, - LZIP = 9, - LRZIP = 10, - LZOP = 11, - GRZIP = 12, - LZ4 = 13, - ZSTD = 14 -}; - -/** Archive formats - * - * Archive formats are the specifications for bundling together multiple files - * and folders (including metadata) into a single file (the archive). See also - * `archive_filter` for more details on the difference between archive formats - * and archive filters. - */ -enum archive_format { - BASE_MASK = 0xff0000, - CPIO = 0x10000, - CPIO_POSIX = (CPIO | 1), - CPIO_BIN_LE = (CPIO | 2), - CPIO_BIN_BE = (CPIO | 3), - CPIO_SVR4_NOCRC = (CPIO | 4), - CPIO_SVR4_CRC = (CPIO | 5), - CPIO_AFIO_LARGE = (CPIO | 6), - SHAR = 0x20000, - SHAR_BASE = (SHAR | 1), - SHAR_DUMP = (SHAR | 2), - TAR = 0x30000, - TAR_USTAR = (TAR | 1), - TAR_PAX_INTERCHANGE = (TAR | 2), - TAR_PAX_RESTRICTED = (TAR | 3), - TAR_GNUTAR = (TAR | 4), - ISO9660 = 0x40000, - ISO9660_ROCKRIDGE = (ISO9660 | 1), - ZIP = 0x50000, - EMPTY = 0x60000, - AR = 0x70000, - AR_GNU = (AR | 1), - AR_BSD = (AR | 2), - MTREE = 0x80000, - RAW = 0x90000, - XAR = 0xA0000, - LHA = 0xB0000, - CAB = 0xC0000, - RAR = 0xD0000, - SEVENZIP = 0xE0000, - WARC = 0xF0000, - RAR_V5 = 0x100000 -}; - -/** Strip archive extensions from a path - * - * @returns Base filename without archive extensions - */ -inline std::filesystem::path -stem(const std::filesystem::path& path) { - std::filesystem::path p_stem{path}; - logger::trace("Stemming {}", p_stem.string()); - - p_stem = p_stem.filename(); - - while (fileformats.find(p_stem.extension().string()) != fileformats.end()) { - p_stem = p_stem.stem(); - logger::trace("Stemmed to {}", p_stem.string()); - } - - logger::trace("Finished stemming {}", p_stem.string()); - return p_stem; -} - -/** Get the archive extension of a path. - * - * The archive extension may be a combination of supported fileformats in which - * case all of them are returned. - * - * @returns Archive extension of the archive or path() if no (known) extension - * exists. - */ -inline std::filesystem::path ext(const std::filesystem::path& path) { - std::filesystem::path p_ext{path}; - logger::trace("Extracting extension of {}", p_ext.string()); - - std::filesystem::path p_ext_collector; - while (fileformats.find(p_ext.extension().string()) != fileformats.end()) { - // path extension() const - p_ext_collector = p_ext.extension().concat(p_ext_collector.string()); - p_ext.replace_extension(); - } - - return p_ext_collector; -} - -} // namespace xwim diff --git a/src/main.cpp b/src/main.cpp index fddf13a..5c970cf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,77 +1,6 @@ #include #include -#include -#include -#include -#include - -#include "util/log.hpp" -#include "util/argparse.hpp" -#include "archive.hpp" -#include "spec.hpp" -#include "fileformats.hpp" - -namespace logger = spdlog; -using namespace xwim::argparse; - -void extract(const XwimPath& xwim_path) { - try { - xwim::Archive archive{xwim_path.path()}; - xwim::ArchiveSpec archive_spec = archive.check(); - - logger::info("{}", archive_spec); - - xwim::ExtractSpec extract_spec{}; - - if (!archive_spec.has_single_root || !archive_spec.is_root_filename) { - extract_spec.make_dir = true; - - std::filesystem::path stem = xwim::stem(xwim_path.path()); - - extract_spec.dirname = stem; - } - - if (archive_spec.has_subarchive) { - extract_spec.extract_subarchive = true; - } - - logger::info("{}", extract_spec); - - archive.extract(extract_spec); - - } catch (xwim::ArchiveException& ae) { - logger::error("{}", ae.what()); - } -} - -void compress(const XwimPath& xwim_path) { - try { - xwim::Archive archive{xwim_path.path()}; - xwim::CompressSpec compress_spec{}; - - archive.compress(compress_spec); - - } catch (xwim::ArchiveException& ae) { - logger::error("{}", ae.what()); - } -} - -XwimPath parse_args(int argc, char** argv) { - try { - return parse(argc, argv); - } catch (ArgParseException& ex) { - logger::error("{}\n", ex.what()); - std::cout << usage(); - std::exit(1); - } -} - int main(int argc, char** argv) { - xwim::log::init(); - XwimPath xwim_path = parse_args(argc, argv); - - if(xwim_path.is_archive()) { extract(xwim_path); } - else { compress(xwim_path); } } diff --git a/src/meson.build b/src/meson.build index ab44dda..69ad6da 100644 --- a/src/meson.build +++ b/src/meson.build @@ -5,6 +5,7 @@ xwim_src = ['main.cpp', xwim_libs = [dependency('libarchive', required: true), dependency('fmt', required: true), - dependency('spdlog', required: true)] + dependency('spdlog', required: true), + dependency('tclap', required: true)] executable('xwim', xwim_src, dependencies: xwim_libs) diff --git a/src/spec.hpp b/src/spec.hpp deleted file mode 100644 index da164ca..0000000 --- a/src/spec.hpp +++ /dev/null @@ -1,132 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -#include "fileformats.hpp" - -namespace xwim { - -/** Properties of an archive - * - * These properties can be retrieved by analyzing the - * archive. There is no outside-knowledge. All information - * is in the archive. - */ -struct ArchiveSpec { - bool has_single_root = false; /** There is only a single file xor a single - folder at the archive's root */ - bool is_root_filename = false; /** the name of the (single) root is the same - as the stemmed archive file name. Cannot be - true if `has_single_root` is false */ - bool is_root_dir = false; /** The (single) root is a folder. Cannot be true if - `has_single_root` is false */ - bool has_subarchive = false; /** Whether the archive contains sub-archives */ -}; - -/** Properties influencing the extraction process - * - * These properties can be set to influence the extraction - * process accordingly. - */ -struct ExtractSpec { - bool make_dir = false; /** Create a new directory for extraction at `dirname` */ - std::filesystem::path dirname{}; /** The path to a directory for extraction */ - bool extract_subarchive = false; /** Recursively extract sub-archives */ -}; - - -/** Compile time definitions for platform-dependent files and filters */ -#if defined(unix) || defined(__unix__) || defined(__unix) -#define XWIM_COMPRESS_FORMAT xwim::archive_format::TAR_USTAR -#define XWIM_COMPRESS_FILTER { xwim::archive_filter::GZIP } -#define XWIM_COMPRESS_EXTENSION ".tar.gz" -#elif defined(_win32) || defined(__win32__) || defined(__windows__) -#define XWIM_COMPRESS_FORMAT xwim::archive_format::ZIP -#define XWIM_COMPRESS_FILTER {} -#define XWIM_COMPRESS_EXTENSION ".zip" -#else -#define XWIM_COMPRESS_FORMAT xwim::fileformats::archive_format::ZIP -#define XWIM_COMPRESS_FILTER {} -#define XWIM_COMPRESS_EXTENSION ".zip" -#endif - -/** Properties influencing the compression process - * - * These properties can be set to influence the compression process - * accordingly. - * - * Per default, the compress spec is platform dependent to accommodate for the - * expected format on that platform. On Windows this is zip, on Unix this is - * tar.gz - */ -struct CompressSpec { - xwim::archive_format format = - XWIM_COMPRESS_FORMAT; /** The archiving format, e.g. tar */ - std::vector filters = - XWIM_COMPRESS_FILTER; /** Filters applied to the archive, - e.g. gzip */ - std::string extension = - XWIM_COMPRESS_EXTENSION; /** Archive extension, e.g. .tar.gz */ -}; - -} // namespace xwim - -#if FMT_VERSION < 50300 -typedef fmt::basic_parse_context format_parse_context; -#endif - -template <> -struct fmt::formatter { - constexpr auto parse(format_parse_context & ctx) { - return ctx.begin(); - } - - template - auto format(const xwim::ArchiveSpec& spec, FormatContext& ctx) { - return format_to(ctx.out(), - "Archive[" - " .has_single_root={}," - " .is_root_filename={}" - " .is_root_dir={}" - " .has_subarchive={}" - " ]", - spec.has_single_root, spec.is_root_filename, - spec.is_root_dir, spec.has_subarchive); - } -}; - -template <> -struct fmt::formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - template - auto format(const xwim::ExtractSpec& spec, FormatContext& ctx) { - return format_to(ctx.out(), - "Extract[" - " .make_dir={}," - " .dirname={}" - " .extract_subarchive={}" - " ]", - spec.make_dir, spec.dirname.string(), - spec.extract_subarchive); - } -}; - -template <> -struct fmt::formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - template - auto format(const xwim::CompressSpec& spec, FormatContext& ctx) { - return format_to(ctx.out(), - "Compress[" - " .format={}," - " .filters={}" - " ]", - spec.format, spec.filters); - } -}; diff --git a/src/util/argparse.cpp b/src/util/argparse.cpp deleted file mode 100644 index 30966a4..0000000 --- a/src/util/argparse.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "argparse.hpp" - -namespace xwim { -namespace argparse { - XwimPath parse(int argc, char** argv) { - return XwimPath{argc, argv}; - } - - // contructs XwimPath{} first so that destructurs may running - // http://www.vishalchovatiya.com/7-best-practices-for-exception-handling-in-cpp-with-example/ - XwimPath::XwimPath(int argc, char** argv) : XwimPath{} { - if (argc < 2) throw ArgParseException{"No argument provided"}; - if (argc > 2) throw ArgParseException{"Too many arguments provided"}; - - this->_path = std::filesystem::path{argv[1]}; - } - - bool XwimPath::is_archive() { - return !xwim::ext(_path).empty(); - } - - std::filesystem::path XwimPath::path() const { - return std::filesystem::path{_path}; - } -} -} diff --git a/src/util/argparse.hpp b/src/util/argparse.hpp deleted file mode 100644 index 740761e..0000000 --- a/src/util/argparse.hpp +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "../fileformats.hpp" - -namespace xwim { -/** - * xwim allows for - * 1. an archive - * 2. a file or folder - * - * In case of (1) the archive will be extracted according to the xwim - * do-what-i-mean rules. - * - * In case of (2) the file or folder will be compressed into a "platform native" - * format, i.e. what appears to be the most widely used format on that platform. - * In case of unix this is tar.gz. In case of windows this is zip. The archive - * gets the same name as the file or folder and a proper extension. - * - * A list of files or folders is unsupported as it would be too ambigious to - * choose a name for the archive. A list of archives is unsupported for - * consistency reasons. Any mixture is unsupported as it would be too ambigious - * what the user wants. This is subject to change in the future. - */ -namespace argparse { - -class XwimPath { - private: - std::filesystem::path _path; - - public: - XwimPath() : _path{} {}; - XwimPath(int argc, char** argv); - - bool is_archive(); - std::filesystem::path path() const; -}; - -class ArgParseException : public std::exception { - private: - std::string _what; - - public: - ArgParseException(std::string what) : _what{what} {}; - - virtual const char* what() const noexcept { return this->_what.c_str(); } -}; - -XwimPath parse(int argc, char** argv); - -inline std::string usage() { - std::stringstream s; - s << "USAGE:" - << "\t xwim \n" - << "\n" - - << "PARAMS:" << std::left << std::setfill('.') << std::setw(10) - << "\t path " - << " Archive\n" - << "\n" - - << "FORMATS:\n" - << "\t .7z, .7zip .jar, .tgz, .bz2, .bzip2\n" - << "\t .gz, .gzip, .rar, .tar, .xz, .zip\n" - << "\n" - - << "EXAMPLES:\n" - << "\t Extract archive archive.tar.gz:\n" - << "\t xwim archive.tar.gz\n" - << std::endl; - - return s.str(); -} - -} // namespace argparse -} // namespace xwim diff --git a/src/util/log.hpp b/src/util/log.hpp deleted file mode 100644 index fbb3abe..0000000 --- a/src/util/log.hpp +++ /dev/null @@ -1,77 +0,0 @@ -#pragma once - -#include -#include -#include - -#ifdef NDEBUG -#define XWIM_LOGLEVEL SPDLOG_LEVEL_ERROR -#else -#define XWIM_LOGLEVEL SPDLOG_LEVEL_DEBUG -#endif - -namespace xwim::log { - -/** - * Get log level from XWIM_LOGLEVEL environment variable. - * For valid values see SPDLOG_LEVEL_NAMES in spdlog/common.h - * - * @returns spdlog::level::level_enum::off if no valid XWIM_LOGLEVEL defined - */ -spdlog::level::level_enum _init_from_env() { - char* env_lvl = std::getenv("XWIM_LOGLEVEL"); - if (!env_lvl) { - return spdlog::level::level_enum::off; - } - - spdlog::level::level_enum lvl = spdlog::level::from_str(env_lvl); - - //`::from_str` returns `off` if no match found - if (spdlog::level::level_enum::off == lvl) { - spdlog::debug("No environment definition for log level"); // uses default - // logger/level - } - - return lvl; -} - -/** - * Get log level from compile time definition. - * - * @return spdlog::level::level_enum::error for release builds (-DNDEBUG) - * spdlog::level::level_enum::debug for debug builds - */ -spdlog::level::level_enum _init_from_compile() { - return static_cast(XWIM_LOGLEVEL); -} - -/** - * Determine the log level from various sources at runtime. - * - * The log level is determined from sources in the following order (first - * wins): - * 1. The `level` argument - * 2. The XWIM_LOGLEVEL environment variable - * 3. The default for the build type (-DNDEBUG) - * -> ERROR for release builds - * -> DEBUG for debug builds - * - * The determined level is then set for the default logger via - * `spdlog::set_level`. - */ -void init(spdlog::level::level_enum level = spdlog::level::level_enum::off) { - if (spdlog::level::level_enum::off != level) { - spdlog::set_level(level); - return; - } - - level = _init_from_env(); - if (spdlog::level::level_enum::off != level) { - spdlog::set_level(level); - return; - } - - spdlog::set_level(_init_from_compile()); -} - -} // namespace xwim::log diff --git a/test/archive_test.cpp b/test/archive_test.cpp deleted file mode 100644 index f548415..0000000 --- a/test/archive_test.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include - -#include -#include -#include - -TEST(ArchiveTest, ArchiveSpecDetectsSingleRoot) { - xwim::Archive archive("test/archives/root.tar.gz"); - - xwim::ArchiveSpec spec = archive.check(); - ASSERT_TRUE(spec.has_single_root); -} diff --git a/test/fileformats_test.cpp b/test/fileformats_test.cpp deleted file mode 100644 index acd6e1d..0000000 --- a/test/fileformats_test.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include - -#include -#include - -TEST(FileformatsTest, StemStripsSingleKnownExtension) { - std::filesystem::path archive_path {"/some/path/to/file.rar"}; - - ASSERT_EQ(xwim::stem(archive_path), std::filesystem::path{"file"}); -} - -TEST(FileformatsTest, StemStripsMultipleKnownExtensions) { - std::filesystem::path archive_path{"/some/path/to/file.tar.rar.gz.7z.rar"}; - - ASSERT_EQ(xwim::stem(archive_path), std::filesystem::path{"file"}); -} - -TEST(FileformatsTest, StemStripsOnlyKnownExtension) { - std::filesystem::path archive_path{"/some/path/to/file.ukn.rar"}; - - ASSERT_EQ(xwim::stem(archive_path), std::filesystem::path{"file.ukn"}); -} - -TEST(FileformatsTest, StemStripsNothingWithoutKnownExtension) { - std::filesystem::path archive_path{"/some/path/to/file.ukn"}; - - ASSERT_EQ(xwim::stem(archive_path), std::filesystem::path{"file.ukn"}); -} - -TEST(FileformatsTest, StemStripsNothingWithoutExtension) { - std::filesystem::path archive_path{"/some/path/to/filerar"}; - - ASSERT_EQ(xwim::stem(archive_path), std::filesystem::path{"filerar"}); -} - -TEST(FileExtTest, ExtGetsKnownExtension) { - std::filesystem::path archive_path{"/some/path/to/file.rar"}; - - ASSERT_EQ(xwim::ext(archive_path), std::filesystem::path{".rar"}); -} - -TEST(FileExtTest, CombinedExtensionGetsAll) { - std::filesystem::path archive_path{"/some/path/to/file.tar.gz"}; - - ASSERT_EQ(xwim::ext(archive_path), std::filesystem::path{".tar.gz"}); -} - -TEST(FileExtTest, ExtEmptyForUnknownExtension) { - std::filesystem::path archive_path{"/some/path/to/file.ukn"}; - - ASSERT_TRUE(xwim::ext(archive_path).empty()); -} - -TEST(FileExtTest, CombinedExtensionGetsKnown) { - std::filesystem::path archive_path{"/some/path/to/file.ukn.tar.gz"}; - - ASSERT_EQ(xwim::ext(archive_path), std::filesystem::path{".tar.gz"}); -} - -TEST(FileExtTest, CombinedExtensionLastUnknownEmpty) { - std::filesystem::path archive_path{"/some/path/to/file.tar.gz.ukn"}; - - ASSERT_TRUE(xwim::ext(archive_path).empty()); -} diff --git a/tests/ahaahm.tar.gz b/tests/ahaahm.tar.gz new file mode 100644 index 0000000..c35f252 Binary files /dev/null and b/tests/ahaahm.tar.gz differ diff --git a/tests/ahaahm/äh🦔~öhm/ähä~öhm.txt b/tests/ahaahm/äh🦔~öhm/ähä~öhm.txt new file mode 100644 index 0000000..eac8994 --- /dev/null +++ b/tests/ahaahm/äh🦔~öhm/ähä~öhm.txt @@ -0,0 +1 @@ +äahääm diff --git a/tests/ahaahm/äh🦔~öhm/ähä~öhm/ähä~öhm.txt b/tests/ahaahm/äh🦔~öhm/ähä~öhm/ähä~öhm.txt new file mode 100644 index 0000000..eac8994 --- /dev/null +++ b/tests/ahaahm/äh🦔~öhm/ähä~öhm/ähä~öhm.txt @@ -0,0 +1 @@ +äahääm diff --git a/tests/root.tar.gz b/tests/root.tar.gz new file mode 100644 index 0000000..79ced2b Binary files /dev/null and b/tests/root.tar.gz differ diff --git a/tests/runtests/run.py b/tests/runtests/run.py new file mode 100755 index 0000000..19bae79 --- /dev/null +++ b/tests/runtests/run.py @@ -0,0 +1,17 @@ +#!/bin/python3 + +import os; +import sys; +import subprocess; +from fnmatch import fnmatch; + +for root, dirs, files in os.walk('../../'): + for f in files: + if len(sys.argv) > 1 and not fnmatch(f, sys.argv[1]): + continue; + + print(f"Running {f}") + print(f"{os.path.join(root,f)}") + r = subprocess.run(["../../../target/src/xwim", os.path.join(root, f)], capture_output=True, encoding='utf-8') + print(f"{r.stdout}") + print(f"{r.stderr}", file=sys.stderr) diff --git a/tests/test-1.23.7z b/tests/test-1.23.7z new file mode 100644 index 0000000..b3e1ea7 Binary files /dev/null and b/tests/test-1.23.7z differ diff --git a/tests/test-1.23.arj b/tests/test-1.23.arj new file mode 100644 index 0000000..3b01a22 Binary files /dev/null and b/tests/test-1.23.arj differ diff --git a/tests/test-1.23.cpio b/tests/test-1.23.cpio new file mode 100644 index 0000000..0777739 Binary files /dev/null and b/tests/test-1.23.cpio differ diff --git a/tests/test-1.23.lzh b/tests/test-1.23.lzh new file mode 100644 index 0000000..942e2cb Binary files /dev/null and b/tests/test-1.23.lzh differ diff --git a/tests/test-1.23.rar b/tests/test-1.23.rar new file mode 100644 index 0000000..b12fa1e Binary files /dev/null and b/tests/test-1.23.rar differ diff --git a/tests/test-1.23.tar b/tests/test-1.23.tar new file mode 100644 index 0000000..7374619 Binary files /dev/null and b/tests/test-1.23.tar differ diff --git a/tests/test-1.23.tar.bz2 b/tests/test-1.23.tar.bz2 new file mode 100644 index 0000000..2d511b8 Binary files /dev/null and b/tests/test-1.23.tar.bz2 differ diff --git a/tests/test-1.23.tar.gz b/tests/test-1.23.tar.gz new file mode 100644 index 0000000..b32be49 Binary files /dev/null and b/tests/test-1.23.tar.gz differ diff --git a/tests/test-1.23.tar.lrz b/tests/test-1.23.tar.lrz new file mode 100644 index 0000000..e24711b Binary files /dev/null and b/tests/test-1.23.tar.lrz differ diff --git a/tests/test-1.23.tar.lzma b/tests/test-1.23.tar.lzma new file mode 100644 index 0000000..27d783c Binary files /dev/null and b/tests/test-1.23.tar.lzma differ diff --git a/tests/test-1.23.zip b/tests/test-1.23.zip new file mode 100644 index 0000000..aa0ff55 Binary files /dev/null and b/tests/test-1.23.zip differ diff --git a/tests/test-1.23_all.deb b/tests/test-1.23_all.deb new file mode 100644 index 0000000..2deb1b8 Binary files /dev/null and b/tests/test-1.23_all.deb differ diff --git a/tests/test-2_all.deb b/tests/test-2_all.deb new file mode 100644 index 0000000..2ed2886 Binary files /dev/null and b/tests/test-2_all.deb differ diff --git a/tests/test-deep-recursion.tar b/tests/test-deep-recursion.tar new file mode 100644 index 0000000..aa62164 Binary files /dev/null and b/tests/test-deep-recursion.tar differ diff --git a/tests/test-dot-first-bomb.tar.gz b/tests/test-dot-first-bomb.tar.gz new file mode 100644 index 0000000..3f81d5c Binary files /dev/null and b/tests/test-dot-first-bomb.tar.gz differ diff --git a/tests/test-dot-first-onedir.tar.gz b/tests/test-dot-first-onedir.tar.gz new file mode 100644 index 0000000..bbb406d Binary files /dev/null and b/tests/test-dot-first-onedir.tar.gz differ diff --git a/tests/test-empty.tar.bz2 b/tests/test-empty.tar.bz2 new file mode 100644 index 0000000..a704637 Binary files /dev/null and b/tests/test-empty.tar.bz2 differ diff --git a/tests/test-one-archive.tar.gz b/tests/test-one-archive.tar.gz new file mode 100644 index 0000000..5799800 Binary files /dev/null and b/tests/test-one-archive.tar.gz differ diff --git a/tests/test-onedir.tar.bz2 b/tests/test-onedir.tar.bz2 new file mode 100644 index 0000000..7e734c1 Binary files /dev/null and b/tests/test-onedir.tar.bz2 differ diff --git a/tests/test-onedir.tar.gz b/tests/test-onedir.tar.gz new file mode 100644 index 0000000..a7b3ee1 Binary files /dev/null and b/tests/test-onedir.tar.gz differ diff --git a/tests/test-onefile.tar.gz b/tests/test-onefile.tar.gz new file mode 100644 index 0000000..e4fed68 Binary files /dev/null and b/tests/test-onefile.tar.gz differ diff --git a/tests/test-recursive-badperms.tar.bz2 b/tests/test-recursive-badperms.tar.bz2 new file mode 100644 index 0000000..7c720bf Binary files /dev/null and b/tests/test-recursive-badperms.tar.bz2 differ diff --git a/tests/test-recursive-no-prompt.tar.bz2 b/tests/test-recursive-no-prompt.tar.bz2 new file mode 100644 index 0000000..ff439e5 Binary files /dev/null and b/tests/test-recursive-no-prompt.tar.bz2 differ diff --git a/tests/test-tar-with-node.tar.gz b/tests/test-tar-with-node.tar.gz new file mode 100644 index 0000000..f12ba42 Binary files /dev/null and b/tests/test-tar-with-node.tar.gz differ diff --git a/tests/test-text.bz2 b/tests/test-text.bz2 new file mode 100644 index 0000000..92bfd31 Binary files /dev/null and b/tests/test-text.bz2 differ diff --git a/tests/test-text.gz b/tests/test-text.gz new file mode 100644 index 0000000..e427562 Binary files /dev/null and b/tests/test-text.gz differ diff --git a/tests/test-text.lrz b/tests/test-text.lrz new file mode 100644 index 0000000..92b32d5 Binary files /dev/null and b/tests/test-text.lrz differ diff --git a/tests/test-text.lz b/tests/test-text.lz new file mode 100644 index 0000000..b1d0f2f Binary files /dev/null and b/tests/test-text.lz differ diff --git a/tests/test-text.xz b/tests/test-text.xz new file mode 100644 index 0000000..75ef861 Binary files /dev/null and b/tests/test-text.xz differ diff --git a/tests/xwim-0.5.0-x86_64-linux.tar.gz b/tests/xwim-0.5.0-x86_64-linux.tar.gz new file mode 100644 index 0000000..4fa6b6f Binary files /dev/null and b/tests/xwim-0.5.0-x86_64-linux.tar.gz differ