Compare commits

...

13 commits
0.4 ... master

Author SHA1 Message Date
1523f83a6b
Restructure ExtractIntent
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-02 16:27:21 +01:00
92ac6586de
Restructure CompressSingleIntent 2024-03-02 16:27:21 +01:00
c8e6f51e1b
Reformat UserIntent
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-28 18:55:09 +01:00
1aea5cd924
Cleanup UserIntent
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-28 01:06:20 +01:00
293ee6e1cc
Get interactive mode from correct argument
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-27 22:44:00 +01:00
5d9856c658
Control log level via -v option
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is running
Allow the user to control the log level via the conventional `-v`
command line option. Multiplying this option-as per convention-increases
the verbosity.

This is an additional way to set the log level, in addition to the
existing:
- environment variable
- build type (debug vs release)

Given it is the most intentional and direct way to set the log level it
has precedence over all other options.
2024-02-27 21:48:07 +01:00
65053bbcb9
Cleanup some unused includes 2024-02-27 21:47:34 +01:00
c1506c4547
Add more editors/environments to .gitignore 2024-02-27 21:46:11 +01:00
c6e0e92db2
Add more formats, delete leftovers
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is passing
continuous-integration/drone Build is passing
2022-06-25 21:36:08 +02:00
e6a6e9268e
Refactor to new structure
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2022-06-25 19:57:19 +02:00
7141e67e14
Install gtest in drone
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-09 18:36:02 +02:00
70ae623a1d
Restructure to util add split Intent and Options
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-09 18:23:18 +02:00
89dd5186f5
Move Install section up
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-25 21:26:59 +01:00
20 changed files with 884 additions and 358 deletions

View file

@ -6,6 +6,7 @@ steps:
- name: build-shared - name: build-shared
image: arminfriedl/xwim-build:shared image: arminfriedl/xwim-build:shared
commands: commands:
- meson wrap install gtest || true
- meson target/shared - meson target/shared
- ninja -C target/shared - ninja -C target/shared
- mv target/shared/src/xwim xwim-x86_64-glibc-linux-shared - mv target/shared/src/xwim xwim-x86_64-glibc-linux-shared
@ -13,6 +14,7 @@ steps:
- name: build-static - name: build-static
image: arminfriedl/xwim-build:static image: arminfriedl/xwim-build:static
commands: commands:
- meson wrap install gtest || true
- meson --default-library=static target/static - meson --default-library=static target/static
- ninja -C target/static - ninja -C target/static
- mv target/static/src/xwim xwim-x86_64-musl-linux-static - mv target/static/src/xwim xwim-x86_64-musl-linux-static
@ -48,6 +50,7 @@ steps:
- name: build-shared - name: build-shared
image: arminfriedl/xwim-build:shared image: arminfriedl/xwim-build:shared
commands: commands:
- meson wrap install gtest || true
- meson --buildtype=release target/shared - meson --buildtype=release target/shared
- ninja -C target/shared - ninja -C target/shared
- strip target/shared/src/xwim - strip target/shared/src/xwim
@ -57,6 +60,7 @@ steps:
- name: build-static - name: build-static
image: arminfriedl/xwim-build:static image: arminfriedl/xwim-build:static
commands: commands:
- meson wrap install gtest || true
- meson --buildtype=release --default-library=static target/static - meson --buildtype=release --default-library=static target/static
- ninja -C target/static - ninja -C target/static
- strip target/static/src/xwim - strip target/static/src/xwim

247
.gitignore vendored
View file

@ -2,11 +2,11 @@
build/ build/
target/ target/
compile_commands.json compile_commands.json
.vscode
.ccls-cache .ccls-cache
.idea/codeStyles/**
# Created by https://www.gitignore.io/api/vim,c++,emacs,ninja # Created by https://www.toptal.com/developers/gitignore/api/c++,vim,emacs,linux,macos,ninja,windows,jetbrains+all,clion+all,visualstudiocode
# Edit at https://www.gitignore.io/?templates=vim,c++,emacs,ninja # Edit at https://www.toptal.com/developers/gitignore?templates=c++,vim,emacs,linux,macos,ninja,windows,jetbrains+all,clion+all,visualstudiocode
### C++ ### ### C++ ###
# Prerequisites # Prerequisites
@ -42,6 +42,94 @@ compile_commands.json
*.out *.out
*.app *.app
### CLion+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### CLion+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### Emacs ### ### Emacs ###
# -*- mode: gitignore; -*- # -*- mode: gitignore; -*-
*~ *~
@ -93,6 +181,108 @@ flycheck_*.el
/network-security.data /network-security.data
### JetBrains+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# SonarLint plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### JetBrains+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
### Linux ###
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Ninja ### ### Ninja ###
.ninja_deps .ninja_deps
.ninja_log .ninja_log
@ -100,6 +290,7 @@ flycheck_*.el
### Vim ### ### Vim ###
# Swap # Swap
[._]*.s[a-v][a-z] [._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p] [._]*.sw[a-p]
[._]s[a-rt-v][a-z] [._]s[a-rt-v][a-z]
[._]ss[a-gi-z] [._]ss[a-gi-z]
@ -111,14 +302,54 @@ Sessionx.vim
# Temporary # Temporary
.netrwhist .netrwhist
# Auto-generated tag files # Auto-generated tag files
tags tags
# Persistent undo # Persistent undo
[._]*.un~ [._]*.un~
# Coc configuration directory ### VisualStudioCode ###
.vim .vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# End of https://www.gitignore.io/api/vim,c++,emacs,ninja # Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/c++,vim,emacs,linux,macos,ninja,windows,jetbrains+all,clion+all,visualstudiocode

View file

@ -22,6 +22,22 @@ archiving binaries (if installed). In contrast `xwim` is a compiled binary based
directly on archiving libraries, which some may appreciate. It can optionally be directly on archiving libraries, which some may appreciate. It can optionally be
statically linked if you want it entirely self-contained. statically linked if you want it entirely self-contained.
# Install
`xwim` currently released for Linux only. There are two flavers: statically
linked and dynamically linked. The releases can be downloaded from
https://git.friedl.net/incubator/xwim/releases and should run on most 64-bit
GNU/Linux distributions.
For the dynamically linked version, the following dependencies have to be
installed:
- [spdlog](https://github.com/gabime/spdlog)
- [fmt](https://github.com/fmtlib/fmt)
- [libarchive](https://github.com/libarchive/libarchive)
Windows support is planned for the first stable release. Packaging for various
distributions is also planned once `xwim` stabilizes. Please reach out if you
can help.
# Usage # Usage
Invoking `xwim` is as simple as: Invoking `xwim` is as simple as:
@ -90,22 +106,6 @@ Take a look `Archiver.hpp` if you want to help and have some time for testing.
Most formats can readily be added if they are supported by libarchive. For other Most formats can readily be added if they are supported by libarchive. For other
formats you have to add an `Archiver` implementation. formats you have to add an `Archiver` implementation.
# Install
`xwim` currently released for Linux only. There are two flavers: statically
linked and dynamically linked. The releases can be downloaded from
https://git.friedl.net/incubator/xwim/releases and should run on most 64-bit
GNU/Linux distributions.
For the dynamically linked version, the following dependencies have to be
installed:
- [spdlog](https://github.com/gabime/spdlog)
- [fmt](https://github.com/fmtlib/fmt)
- [libarchive](https://github.com/libarchive/libarchive)
Windows support is planned for the first stable release. Packaging for various
distributions is also planned once `xwim` stabilizes. Please reach out if you
can help.
# Build # Build
xwim is built with [meson](https://mesonbuild.com/). To compile xwim from source xwim is built with [meson](https://mesonbuild.com/). To compile xwim from source
you need: you need:

View file

@ -5,7 +5,9 @@ project('xwim', 'cpp',
'b_ndebug=if-release']) 'b_ndebug=if-release'])
add_global_arguments('-DVERSION='+meson.version(), language: 'cpp') add_global_arguments('-DVERSION='+meson.version(), language: 'cpp')
add_global_arguments('-DSPDLOG_FMT_EXTERNAL', language: 'cpp')
add_global_arguments('-DFMT_HEADER_ONLY', language: 'cpp')
subdir('src') subdir('src')
subdir('doc') subdir('doc')
# subdir('test') subdir('test')

View file

@ -1,13 +1,21 @@
#include "Archiver.hpp" #include "Archiver.hpp"
#include "Formats.hpp"
#include <spdlog/common.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <filesystem> #include <filesystem>
#include <map> #include <map>
#include <memory> #include <memory>
#include "Common.hpp" #include "util/Common.hpp"
#if defined(unix) || defined(__unix__) || defined(__unix)
std::string default_extension = ".tar.gz";
#elif defined(_win32) || defined(__win32__) || defined(__windows__)
std::string default_extension = ".zip";
#else
std::string default_extension = ".zip";
#endif
namespace xwim { namespace xwim {
using namespace std; using namespace std;
@ -30,9 +38,9 @@ fs::path archive_extension(const fs::path& path) {
while (tmp_path.has_extension()) { while (tmp_path.has_extension()) {
tmp_ext = tmp_path.extension() += tmp_ext; tmp_ext = tmp_path.extension() += tmp_ext;
auto search = extensions_format.find(tmp_ext); Format format = find_extension_format(tmp_ext);
if (search != extensions_format.end()) { if (format != Format::UNKNOWN) {
// (Combined) extension known. Remember as `ext` and keep // (Combined) extension known. Remember as `ext` and keep
// looking for even longer extensions. // looking for even longer extensions.
ext = tmp_ext; ext = tmp_ext;
@ -69,9 +77,9 @@ fs::path strip_archive_extension(const fs::path& path) {
tmp_ext = tmp_path.extension() += tmp_ext; tmp_ext = tmp_path.extension() += tmp_ext;
spdlog::debug("Looking for {} in known extensions", tmp_ext); spdlog::debug("Looking for {} in known extensions", tmp_ext);
auto search = extensions_format.find(tmp_ext); Format format = find_extension_format(tmp_ext);
tmp_longest_ext++; tmp_longest_ext++;
if (search != extensions_format.end()) { if (format != Format::UNKNOWN) {
// (Combined) extension known. Remember as `longest_ext` and keep // (Combined) extension known. Remember as `longest_ext` and keep
// looking for even longer extensions. // looking for even longer extensions.
longest_ext = tmp_longest_ext; longest_ext = tmp_longest_ext;
@ -90,7 +98,14 @@ fs::path strip_archive_extension(const fs::path& path) {
return tmp_path; return tmp_path;
} }
bool can_extract(const fs::path& path) { std::filesystem::path default_archive(const std::filesystem::path& base) {
string base_s = base.string();
string ext_s = default_extension;
return fs::path{fmt::format("{}{}", base_s, ext_s)};
}
bool can_handle_archive(const fs::path& path) {
fs::path ext = archive_extension(path); fs::path ext = archive_extension(path);
if (format_extensions.find(ext.string()) != format_extensions.end()) { if (format_extensions.find(ext.string()) != format_extensions.end()) {
spdlog::debug("Found {} in known formats", ext); spdlog::debug("Found {} in known formats", ext);
@ -105,17 +120,20 @@ Format parse_format(const fs::path& path) {
spdlog::debug("Looking for path {}", path); spdlog::debug("Looking for path {}", path);
fs::path ext = archive_extension(path); fs::path ext = archive_extension(path);
spdlog::debug("Looking for ext {}", ext); spdlog::debug("Looking for ext {}", ext);
auto search = extensions_format.find(ext); Format format = find_extension_format(ext);
if (search == extensions_format.end()) {
if (format == Format::UNKNOWN) {
throw XwimError{"No known archiver for {}", path}; throw XwimError{"No known archiver for {}", path};
} }
return search->second; return format;
} }
unique_ptr<Archiver> make_archiver(const string& archive_name) { unique_ptr<Archiver> make_archiver(const string& archive_name) {
switch (parse_format(archive_name)) { switch (parse_format(archive_name)) {
case Format::TAR_GZ: case Format::TAR_GZIP: case Format::TAR_BZIP2:
case Format::TAR_COMPRESS: case Format::TAR_LZIP:
case Format::TAR_XZ: case Format::TAR_ZSTD:
case Format::ZIP: case Format::ZIP:
return make_unique<LibArchiver>(); return make_unique<LibArchiver>();
default: default:
@ -125,4 +143,5 @@ unique_ptr<Archiver> make_archiver(const string& archive_name) {
archive_name}; archive_name};
}; };
} }
} // namespace xwim } // namespace xwim

View file

@ -7,18 +7,11 @@
#include <memory> #include <memory>
#include <set> #include <set>
#include "Common.hpp" #include "util/Common.hpp"
#include "Formats.hpp"
namespace xwim { namespace xwim {
// Invariant:
// `extensions_format` defines a surjection from `format_extensions`
// to `Formats`
const std::set<std::string> format_extensions{".tar.gz", ".zip"};
enum class Format { TAR_GZ, ZIP };
const std::map<std::string, Format> extensions_format{
{".tar.gz", Format::TAR_GZ}, {".zip", Format::ZIP}};
class Archiver { class Archiver {
public: public:
virtual void compress(std::set<std::filesystem::path> ins, virtual void compress(std::set<std::filesystem::path> ins,
@ -40,8 +33,10 @@ class LibArchiver : public Archiver {
std::filesystem::path archive_extension(const std::filesystem::path& path); std::filesystem::path archive_extension(const std::filesystem::path& path);
std::filesystem::path strip_archive_extension(const std::filesystem::path& path); std::filesystem::path strip_archive_extension(const std::filesystem::path& path);
std::filesystem::path default_archive(const std::filesystem::path& base);
Format parse_format(const std::filesystem::path& path); Format parse_format(const std::filesystem::path& path);
bool can_extract(const std::filesystem::path& path); bool can_handle_archive(const std::filesystem::path& path);
std::unique_ptr<Archiver> make_archiver(const std::string& archive_name); std::unique_ptr<Archiver> make_archiver(const std::string& archive_name);

49
src/Formats.hpp Normal file
View file

@ -0,0 +1,49 @@
#pragma once
namespace xwim {
using namespace std;
// Invariant:
// `extensions_format` defines a surjection from `format_extensions`
// to `Formats`
enum class Format {
UNKNOWN,
TAR_BZIP2, TAR_GZIP, TAR_LZIP, TAR_XZ, TAR_COMPRESS, TAR_ZSTD,
ZIP
};
const set<string> format_extensions{
// tar formats see: https://en.wikipedia.org/wiki/Tar_(computing)#Suffixes_for_compressed_files
/* bzip2 */ ".tar.bz2", ".tb2", ".tbz", ".tbz2", ".tz2",
/* gzip */ ".tar.gz", ".taz", ".tgz",
/* lzip */ ".tar.lz",
/* xz */ ".tar.xz", ".txz",
/* compress */ ".tar.Z", ".tZ", ".taZ",
/* zstd */ ".tar.zst", ".tzst",
/* zip */ ".zip"
};
const map<set<string>, Format> extensions_format{
{{".tar.bz2", ".tb2", ".tbz", ".tbz2", ".tz2"}, Format::TAR_BZIP2},
{{".tar.gz", ".taz", ".tgz"}, Format::TAR_GZIP},
{{".tar.lz"}, Format::TAR_LZIP},
{{".tar.xz", ".txz"}, Format::TAR_XZ},
{{".tar.Z", ".tZ", ".taZ"}, Format::TAR_COMPRESS},
{{".tar.zst", ".tzst"}, Format::TAR_ZSTD},
{{".zip"}, Format::ZIP}
};
inline Format find_extension_format(const string& ext) {
for(auto ef: extensions_format) {
auto f = ef.first.find(ext);
if(f != ef.first.end()) {
return ef.second;
}
}
return Format::UNKNOWN;
}
}

226
src/UserIntent.cpp Normal file
View file

@ -0,0 +1,226 @@
#include "UserIntent.hpp"
#include <spdlog/spdlog.h>
#include <algorithm>
#include <filesystem>
#include "Archiver.hpp"
namespace xwim {
unique_ptr<UserIntent> make_compress_intent(const UserOpt &userOpt) {
if (userOpt.paths.size() == 1) {
return make_unique<CompressSingleIntent>(
CompressSingleIntent{*userOpt.paths.begin(), userOpt.out});
}
if (!userOpt.out.has_value()) {
throw XwimError("Cannot guess output for multiple targets");
}
return make_unique<CompressManyIntent>(
CompressManyIntent{userOpt.paths, userOpt.out.value()});
}
unique_ptr<UserIntent> make_extract_intent(const UserOpt &userOpt) {
for (const path &p : userOpt.paths) {
if (!can_handle_archive(p)) {
throw XwimError("Cannot extract path {}", p);
}
}
return make_unique<ExtractIntent>(ExtractIntent{userOpt.paths, userOpt.out});
}
unique_ptr<UserIntent> try_infer_compress_intent(const UserOpt &userOpt) {
if (!userOpt.out.has_value()) {
spdlog::debug("No <out> provided");
if (userOpt.paths.size() != 1) {
spdlog::debug(
"Not a single-path compression. Cannot guess <out> for many-path "
"compression");
return nullptr;
}
spdlog::debug("Only one <path> provided. Assume single-path compression.");
return make_unique<CompressSingleIntent>(
CompressSingleIntent{*userOpt.paths.begin(), userOpt.out});
}
spdlog::debug("<out> provided: {}", userOpt.out.value());
if (can_handle_archive(userOpt.out.value())) {
spdlog::debug("{} given and a known archive format, assume compression",
userOpt.out.value());
return make_compress_intent(userOpt);
}
spdlog::debug(
"Cannot compress multiple paths without a user-provided output archive");
return nullptr;
}
unique_ptr<UserIntent> try_infer_extract_intent(const UserOpt &userOpt) {
bool can_extract_all =
std::all_of(userOpt.paths.begin(), userOpt.paths.end(),
[](const path &path) { return can_handle_archive(path); });
if (!can_extract_all) {
spdlog::debug(
"Cannot extract all provided <paths>. Assume this is not an "
"extraction.");
for (const path &p : userOpt.paths) {
if (!can_handle_archive(p)) {
spdlog::debug("Cannot handle {}", p);
}
}
return nullptr;
}
if (userOpt.out.has_value() && can_handle_archive(userOpt.out.value())) {
spdlog::debug(
"Could extract all provided <paths>. But also {} looks like an "
"archive. Ambiguous intent. Assume this is not an extraction.",
userOpt.out.value());
return nullptr;
}
spdlog::debug(
"Could extract all provided <paths>. But also <out> looks like an "
"archive. Ambiguous intent. Assume this is not an extraction.");
return make_extract_intent(userOpt);
}
unique_ptr<UserIntent> make_intent(const UserOpt &userOpt) {
if (userOpt.wants_compress() && userOpt.wants_extract()) {
throw XwimError("Cannot compress and extract simultaneously");
}
if (userOpt.paths.empty()) {
throw XwimError("No input given...");
}
// explicitly specified intent
if (userOpt.wants_compress()) return make_compress_intent(userOpt);
if (userOpt.wants_extract()) return make_extract_intent(userOpt);
spdlog::info("Intent not explicitly provided, trying to infer intent");
if (auto intent = try_infer_extract_intent(userOpt)) {
spdlog::info("Extraction intent inferred");
return intent;
}
spdlog::info("Cannot infer extraction intent");
if (auto intent = try_infer_compress_intent(userOpt)) {
spdlog::info("Compression intent inferred");
return intent;
}
spdlog::info("Cannot infer compression intent");
throw XwimError("Cannot guess intent");
}
void ExtractIntent::dwim_reparent(const path &out) {
// move extraction if extraction resulted in only one entry and that entries
// name is already the stripped archive name, i.e. reduce unnecessary nesting
auto dit = std::filesystem::directory_iterator(out);
auto dit_path = dit->path();
if (dit == std::filesystem::directory_iterator()) {
spdlog::debug(
"Cannot flatten extraction folder: extraction folder is empty");
return;
}
if (!is_directory(dit_path)) {
spdlog::debug("Cannot flatten extraction folder: {} is not a directory",
dit_path);
return;
}
if (next(dit) != std::filesystem::directory_iterator()) {
spdlog::debug("Cannot flatten extraction folder: multiple items extracted");
return;
}
if (!std::filesystem::equivalent(dit_path.filename(), out.filename())) {
spdlog::debug(
"Cannot flatten extraction folder: archive entry differs from archive "
"name [extraction folder: {}, archive entry: {}]",
out.filename(), dit_path.filename());
return;
}
spdlog::debug("Output folder [{}] is equivalent to archive entry [{}]", out,
dit_path);
spdlog::info("Flattening extraction folder");
int i = rand_int(0, 100000);
path tmp_out = path{out};
tmp_out.concat(fmt::format(".xwim{}", i));
spdlog::debug("Move {} to {}", dit_path, tmp_out);
std::filesystem::rename(dit_path, tmp_out);
spdlog::debug("Remove parent path {}", out);
std::filesystem::remove(out);
spdlog::debug("Moving {} to {}", tmp_out, out);
std::filesystem::rename(tmp_out, out);
}
path ExtractIntent::out_path(const path &p) {
if (!this->out.has_value()) {
// not out path given, create from archive name
path out = std::filesystem::current_path() / strip_archive_extension(p);
create_directories(out);
return out;
}
if (this->archives.size() == 1) {
// out given and only one archive to extract, just extract into `out`
create_directories(this->out.value());
return this->out.value();
}
// out given and multiple archives to extract, create subfolder
// for each archive
create_directories(this->out.value());
path out = this->out.value() / strip_archive_extension(p);
return out;
}
void ExtractIntent::execute() {
for (const path &p : this->archives) {
std::unique_ptr<Archiver> archiver = make_archiver(p);
path out = this->out_path(p);
archiver->extract(p, out);
this->dwim_reparent(out);
}
}
path CompressSingleIntent::out_path() {
if (this->out.has_value()) {
if (!can_handle_archive(this->out.value())) {
throw XwimError("Unknown archive format {}", this->out.value());
}
return this->out.value();
}
return default_archive(strip_archive_extension(this->in).stem());
}
void CompressSingleIntent::execute() {
path out = this->out_path();
unique_ptr<Archiver> archiver = make_archiver(out);
set<path> ins{this->in};
archiver->compress(ins, out);
};
void CompressManyIntent::execute() {
if (!can_handle_archive(this->out)) {
throw XwimError("Unknown archive format {}", this->out);
}
unique_ptr<Archiver> archiver = make_archiver(this->out);
archiver->compress(this->in_paths, this->out);
}
} // namespace xwim

93
src/UserIntent.hpp Normal file
View file

@ -0,0 +1,93 @@
#pragma once
#include <optional>
#include <set>
#include "util/Common.hpp"
#include "UserOpt.hpp"
namespace xwim {
using namespace std;
using std::filesystem::path;
class UserIntent {
public:
virtual void execute() = 0;
virtual ~UserIntent() = default;
};
/* Factory method to construct a UserIntent which implements `execute()` */
unique_ptr<UserIntent> make_intent(const UserOpt& userOpt);
/**
* Extraction intent
*
* Extracts one or multiple archives. Optionally extracts them to given `out` folder. Otherwise extracts them to the
* current working directory.
*/
class ExtractIntent: public UserIntent {
private:
set<path> archives;
optional<path> out;
void dwim_reparent(const path& out);
path out_path(const path& p);
public:
ExtractIntent(set<path> archives, optional<path> out): archives(archives), out(out) {};
~ExtractIntent() override = default;
void execute() override;
};
/**
* Compress intent for a single file or folder.
*
* Compresses a single path which may be a file or a folder.
*
* No `out` path given:
* - derives the archive name from the input path
* - uses the default archive format for the platform
*
* `out` path given:
* - `out` path must be a path with a valid archive name (including extension)
* - tries to compress the input to the out archive
* - if the `out` base name is different from the input base name, puts the input into a new folder
* with base name inside the archive (archive base name is always the name of the archive content)
*/
class CompressSingleIntent : public UserIntent {
private:
path in;
optional<path> out;
path out_path();
public:
CompressSingleIntent(path in, optional<path> out) : UserIntent(), in(in), out(out) {};
~CompressSingleIntent() override = default;
void execute() override;
};
/**
* Compress intent for multiple files and/or folders.
*
* Compresses multiple files and/or folders to a single archive as given by the `out` path. Since `out` cannot be
* guessed from the input in this case it is mandatory.
*
* A new, single root folder with base name equal to base name of the `out` archive is created inside the archive. All
* input files are put into this root folder.
*/
class CompressManyIntent: public UserIntent {
private:
set<path> in_paths;
path out;
public:
CompressManyIntent(set<path> in_paths, path out): UserIntent(), in_paths(in_paths), out(out) {};
~CompressManyIntent() override = default;
void execute() override;
};
} // namespace xwim

52
src/UserOpt.cpp Normal file
View file

@ -0,0 +1,52 @@
#include "UserOpt.hpp"
#include <tclap/CmdLine.h>
template <>
struct TCLAP::ArgTraits<std::filesystem::path> {
// We use `operator=` here for path construction
// because `operator>>` (`ValueLike`) causes a split at
// whitespace
typedef StringLike ValueCategory;
};
namespace xwim {
UserOpt::UserOpt(int argc, char** argv) {
// clang-format off
TCLAP::CmdLine cmd
{"xwim - Do What I Mean Extractor", ' ', "0.3.0"};
TCLAP::SwitchArg arg_compress
{"c", "compress", "Compress <files>", cmd, false};
TCLAP::SwitchArg arg_extract
{"x", "extract", "Extract <file>", cmd, false};
TCLAP::SwitchArg arg_noninteractive
{"i", "non-interactive", "Non-interactive, fail on ambiguity", cmd, false};
TCLAP::ValueArg<fs::path> arg_outfile
{"o", "out", "Out <file-or-path>", false, fs::path{}, "A path on the filesystem", cmd};
TCLAP::MultiSwitchArg arg_verbose
{"v", "verbose", "Verbosity level", cmd, 0};
TCLAP::UnlabeledMultiArg<fs::path> arg_paths
{"files", "Archive(s) to extract or file(s) to compress", true, "A path on the filesystem", cmd};
// clang-format on
cmd.parse(argc, argv);
if (arg_compress.isSet()) this->compress = arg_compress.getValue();
if (arg_extract.isSet()) this->extract = arg_extract.getValue();
if (arg_outfile.isSet()) this->out = arg_outfile.getValue();
this->verbosity = arg_verbose.getValue();
this->interactive = !arg_noninteractive.getValue();
if (arg_paths.isSet()) {
this->paths =
set<fs::path>{arg_paths.getValue().begin(), arg_paths.getValue().end()};
}
}
} // namespace xwim

31
src/UserOpt.hpp Normal file
View file

@ -0,0 +1,31 @@
#pragma once
#include <optional>
#include <set>
#include "util/Common.hpp"
namespace xwim {
using namespace std;
namespace fs = std::filesystem;
struct UserOpt {
optional<bool> compress;
optional<bool> extract;
bool interactive;
int verbosity;
std::optional<fs::path> out;
std::set<fs::path> paths;
UserOpt(int argc, char** argv);
bool wants_compress() const {
return this->compress.has_value() && this->compress.value();
}
bool wants_extract() const {
return this->extract.has_value() && this->extract.value();
}
};
} // namespace xwim

View file

@ -1,165 +0,0 @@
#include "Xwim.hpp"
#include <spdlog/spdlog.h>
#include <algorithm>
#include <cstdlib>
#include <filesystem>
#include <ios>
#include <iostream>
#include <iterator>
#include <random>
#include <string>
#include "Archiver.hpp"
#include "Common.hpp"
namespace xwim {
using namespace std;
namespace fs = std::filesystem;
#if defined(unix) || defined(__unix__) || defined(__unix)
std::string default_extension = ".tar.gz";
#elif defined(_win32) || defined(__win32__) || defined(__windows__)
std::string default_extension = ".zip";
#else
std::string default_extension = ".zip";
#endif
Xwim::Xwim() : action{Action::UNKNOWN} {}
void Xwim::try_infer() {
infer_action();
infer_output();
if (action == Action::COMPRESS) {
archiver = make_archiver(out.string());
} else if (action == Action::EXTRACT) {
// we can only handle one archive for extraction at a time.
// Checked in `infer_extraction_output`
archiver = make_archiver(ins.begin()->string());
}
}
void Xwim::dwim() {
switch (action) {
case Action::COMPRESS:
this->archiver->compress(ins, out);
break;
case Action::EXTRACT:
this->archiver->extract(*ins.begin(), out);
sanitize_output();
break;
default:
spdlog::error("Unknown action");
}
}
void Xwim::sanitize_output() {
fs::path in_stripped = xwim::strip_archive_extension(*ins.begin());
int count = 0;
fs::directory_entry first_entry;
for(auto& e: fs::directory_iterator(out)) {
count++;
if(first_entry.path().empty()) {
first_entry = e;
}
}
if (count >= 2) {
spdlog::debug("Found multiple entries in extraction directory. Moving {} to {}", out, in_stripped);
fs::rename(out, in_stripped);
} else {
if(first_entry.is_directory()) {
spdlog::debug("Found single directory in extraction directory. Moving {} to {}",
first_entry.path(), in_stripped);
fs::rename(first_entry, in_stripped);
fs::remove(out);
} else {
spdlog::debug(
"Found single file in extraction directory. Moving {} to {}", out, in_stripped);
fs::rename(out, in_stripped);
}
}
}
void Xwim::infer_action() {
if (action != Action::UNKNOWN) return;
if (ins.size() == 1 && can_extract(*ins.begin())) {
action = Action::EXTRACT;
} else {
action = Action::COMPRESS;
}
spdlog::debug("Inferred action: {}", action);
}
void Xwim::infer_output() {
if (!out.empty()) return;
switch (action) {
case Action::COMPRESS:
infer_compression_output();
break;
case Action::EXTRACT:
infer_extraction_output();
break;
default:
throw XwimError{"Cannot infer output, action is unknown"};
}
spdlog::debug("Inferred out: {}", out.string());
}
void Xwim::infer_compression_output() {
if (ins.size() == 1) {
// archive name is just the name of the input with default archive
// extension
fs::path archive_stem = xwim::strip_archive_extension(*ins.begin());
archive_stem += default_extension;
out = archive_stem;
} else {
// We cannot guess the name of the output archive
// TODO use readline/lineoise/editline for path completion
cout << "Archive name: ";
cin >> out;
out = fs::path(out);
}
}
void Xwim::infer_extraction_output() {
if (ins.size() > 1) {
throw XwimError{"Cannot extract more than one archive at a time"};
}
// create a temporary path for extraction
fs::path archive_stem = xwim::strip_archive_extension(*ins.begin());
// note: we use here what is considered an `extensions` by `fs::path` so that
// we can strip it again easily later on
archive_stem += ".";
archive_stem += to_string(rand_int(999, 99999));
archive_stem += ".tmp";
this->out = archive_stem;
}
void Xwim::setCompress() {
this->action = Action::COMPRESS;
spdlog::debug("Set action to {}", this->action);
}
void Xwim::setExtract() {
this->action = Action::EXTRACT;
spdlog::debug("Set action to {}", this->action);
}
void Xwim::setOut(fs::path path) {
this->out = path;
spdlog::debug("Set out to {}", this->out);
}
void Xwim::setIns(vector<fs::path> ins) {
this->ins.insert(ins.begin(), ins.end());
if (this->ins.size() != ins.size()) {
spdlog::warn("Duplicate input files found. Removed {} duplicate(s).",
(ins.size() - this->ins.size()));
}
}
} // namespace xwim

View file

@ -1,63 +0,0 @@
#pragma once
#include <fmt/core.h>
#include <fmt/format.h>
#include <exception>
#include <memory>
#include <set>
#include <stdexcept>
#include "Common.hpp"
#include "Archiver.hpp"
namespace xwim {
using namespace std;
namespace fs = std::filesystem;
enum class Action { UNKNOWN, EXTRACT, COMPRESS };
class Xwim {
private:
Action action;
fs::path out;
set<fs::path> ins;
unique_ptr<Archiver> archiver;
void infer_action();
void infer_output();
void infer_compression_output();
void infer_extraction_output();
void sanitize_output();
public:
Xwim();
void try_infer();
void dwim();
void setCompress();
void setExtract();
void setOut(fs::path);
void setIns(vector<fs::path> ins);
};
} // namespace xwim
template <>
struct fmt::formatter<xwim::Action> {
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
template <typename FormatContext>
auto format(const xwim::Action& action, FormatContext& ctx) {
switch (action) {
case xwim::Action::UNKNOWN:
return format_to(ctx.out(), "UNKNOWN");
case xwim::Action::EXTRACT:
return format_to(ctx.out(), "EXTRACT");
case xwim::Action::COMPRESS:
return format_to(ctx.out(), "COMPRESS");
};
return format_to(ctx.out(), "");
}
};

View file

@ -9,8 +9,8 @@
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include "Archiver.hpp" #include "../Archiver.hpp"
#include "Common.hpp" #include "../util/Common.hpp"
namespace xwim { namespace xwim {
using namespace std; using namespace std;
@ -27,8 +27,9 @@ void LibArchiver::compress(set<fs::path> ins, fs::path archive_out) {
// complete type. `archive` is forward declared only. // complete type. `archive` is forward declared only.
shared_ptr<archive> writer; shared_ptr<archive> writer;
writer = shared_ptr<archive>(archive_write_new(), archive_write_free); writer = shared_ptr<archive>(archive_write_new(), archive_write_free);
archive_write_add_filter_gzip(writer.get()); // archive_write_add_filter_gzip(writer.get());
archive_write_set_format_pax_restricted(writer.get()); // archive_write_set_format_pax_restricted(writer.get());
archive_write_set_format_filter_by_ext(writer.get(), archive_out.c_str());
archive_write_open_filename(writer.get(), archive_out.c_str()); archive_write_open_filename(writer.get(), archive_out.c_str());
shared_ptr<archive> reader; shared_ptr<archive> reader;

View file

@ -1,74 +1,25 @@
#include <spdlog/common.h> #include <spdlog/common.h>
#include <spdlog/logger.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <tclap/ArgException.h>
#include <tclap/CmdLine.h>
#include <tclap/StdOutput.h>
#include <tclap/SwitchArg.h>
#include <tclap/UnlabeledMultiArg.h>
#include <tclap/ValueArg.h>
#include <cstdlib> #include <cstdlib>
#include <filesystem> #include <filesystem>
#include "Common.hpp" #include "UserIntent.hpp"
#include "Log.hpp" #include "UserOpt.hpp"
#include "Xwim.hpp" #include "util/Common.hpp"
#include "util/Log.hpp"
using namespace xwim; using namespace xwim;
using namespace std; using namespace std;
namespace fs = std::filesystem;
template <>
struct TCLAP::ArgTraits<std::filesystem::path> {
typedef ValueLike ValueCategory;
};
int main(int argc, char** argv) { int main(int argc, char** argv) {
log::init(); log::init();
UserOpt user_opt = UserOpt{argc, argv};
TCLAP::CmdLine cmd{"xwim - Do What I Mean Extractor", ' ', "0.3.0"}; log::init(user_opt.verbosity);
TCLAP::SwitchArg arg_compress{"c", "compress", "Compress <files>", cmd,
false};
TCLAP::SwitchArg arg_extract{"x", "extract", "Extract <file>", cmd, false};
TCLAP::ValueArg<fs::path> arg_outfile{
"o", "out", "Out <file-or-path>",
false, fs::path{}, "A path on the filesystem",
cmd};
TCLAP::UnlabeledMultiArg<fs::path> arg_infiles{
"Files", "Archive to extract or files to compress", true,
"A path on the filesystem", cmd};
Xwim xwim;
cmd.parse(argc, argv);
if (arg_extract.isSet() && arg_compress.isSet()) {
// This is a bit ugly but `none-or-xor` only available in
// tclap-1.4 which is not well supported in current
// distributions
auto out = TCLAP::StdOutput{};
TCLAP::ArgException e{
"Cannot compress `-c` and extract `-x` simultaneously"};
try {
out.failure(cmd, e);
} catch (TCLAP::ExitException& e) {
exit(e.getExitStatus());
}
}
// `none-or-xor` ensured already
if (arg_extract.isSet()) xwim.setExtract();
if (arg_compress.isSet()) xwim.setCompress();
if (arg_outfile.isSet()) xwim.setOut(arg_outfile.getValue());
if (arg_infiles.isSet()) xwim.setIns(arg_infiles.getValue());
try { try {
xwim.try_infer(); unique_ptr<UserIntent> user_intent = make_intent(user_opt);
xwim.dwim(); user_intent->execute();
} catch (XwimError& e) { } catch (XwimError& e) {
spdlog::error(e.what()); spdlog::error(e.what());
} }

View file

@ -1,11 +1,12 @@
xwim_src = ['main.cpp', 'Xwim.cpp', 'Archiver.cpp'] xwim_src = ['main.cpp', 'Archiver.cpp', 'UserOpt.cpp', 'UserIntent.cpp']
xwim_archiver = ['archiver/LibArchiver.cpp'] xwim_archiver = ['archiver/LibArchiver.cpp']
is_static = get_option('default_library')=='static' is_static = get_option('default_library')=='static'
xwim_libs = [dependency('libarchive', required: true, static: is_static), xwim_libs = [dependency('libarchive', required: true, static: is_static),
dependency('fmt', required: true, static: is_static),
dependency('spdlog', required: true, static: is_static), dependency('spdlog', required: true, static: is_static),
dependency('fmt', required: true, static: is_static),
dependency('tclap', required: true, static: is_static)] dependency('tclap', required: true, static: is_static)]
executable('xwim', xwim_src+xwim_archiver, dependencies: xwim_libs) executable('xwim', xwim_src+xwim_archiver, dependencies: xwim_libs)

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <spdlog/common.h> #include <spdlog/common.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <cstdlib> #include <cstdlib>
#ifdef NDEBUG #ifdef NDEBUG
#define XWIM_LOGLEVEL SPDLOG_LEVEL_ERROR #define XWIM_LOGLEVEL SPDLOG_LEVEL_ERROR
@ -57,7 +58,27 @@ spdlog::level::level_enum _init_from_compile() {
* The determined level is then set for the default logger via * The determined level is then set for the default logger via
* `spdlog::set_level`. * `spdlog::set_level`.
*/ */
void init(spdlog::level::level_enum level = spdlog::level::level_enum::off) { void init(int verbosity = -1,
spdlog::level::level_enum level = spdlog::level::level_enum::off) {
if (verbosity != -1) {
switch (verbosity) {
case 0:
spdlog::set_level(spdlog::level::off);
break;
case 1:
spdlog::set_level(spdlog::level::info);
break;
case 2:
spdlog::set_level(spdlog::level::debug);
break;
case 3:
default:
spdlog::set_level(spdlog::level::trace);
break;
}
return;
}
if (spdlog::level::level_enum::off != level) { if (spdlog::level::level_enum::off != level) {
spdlog::set_level(level); spdlog::set_level(level);
return; return;

View file

@ -2,22 +2,10 @@
gtest_proj = subproject('gtest') gtest_proj = subproject('gtest')
gtest_dep = gtest_proj.get_variable('gtest_main_dep') gtest_dep = gtest_proj.get_variable('gtest_main_dep')
xwim_src = ['../src/archive.cpp', # subdir('archives')
'../src/archive_sys.cpp'] user_opt_test_exe = executable('user_opt_test_exe',
sources: ['user_opt_test.cpp', '../src/UserOpt.cpp'],
subdir('archives')
archive_test_exe = executable('archive_test_exe',
sources: ['archive_test.cpp', xwim_src],
include_directories: ['../src'], include_directories: ['../src'],
dependencies: [gtest_dep, xwim_libs]) dependencies: [gtest_dep])
test('user opt parsing test', user_opt_test_exe)
test('archive test', archive_test_exe)
fileformats_test_exe = executable('fileformats_test_exe',
sources: ['fileformats_test.cpp', xwim_src],
include_directories: ['../src'],
dependencies: [gtest_dep, xwim_libs])
test('fileformats test', fileformats_test_exe)

90
test/user_opt_test.cpp Normal file
View file

@ -0,0 +1,90 @@
#include <gtest/gtest-death-test.h>
#include "gtest/gtest.h"
#include <filesystem>
#include <string>
#include "UserOpt.hpp"
TEST(UserOpt, compress) {
using namespace xwim;
// clang-format off
char* args[] = {
const_cast<char*>("xwim"),
const_cast<char*>("-c"),
const_cast<char*>("mandator_paths"),
nullptr};
// clang-format on
UserOpt uo = UserOpt{3, args};
ASSERT_TRUE(uo.compress);
ASSERT_FALSE(uo.extract);
}
TEST(UserOpt, exclusive_actions) {
using namespace xwim;
// clang-format off
char* args[] = {
const_cast<char*>("xwim"),
const_cast<char*>("-c"),
const_cast<char*>("-x"),
const_cast<char*>("mandatory_paths"),
nullptr};
// clang-format on
UserOpt uo = UserOpt{4, args};
ASSERT_TRUE(uo.compress);
ASSERT_TRUE(uo.extract);
}
TEST(UserOpt, whitespace_in_path) {
using namespace xwim;
// clang-format off
char* args[] = {
const_cast<char*>("xwim"),
const_cast<char*>("-c"),
const_cast<char*>("/foo/bar baz/a file"),
nullptr};
// clang-format on
UserOpt uo = UserOpt{3, args};
ASSERT_TRUE(uo.paths.find(std::filesystem::path("/foo/bar baz/a file")) !=
uo.paths.end());
}
TEST(UserOpt, mixed_output_and_paths) {
using namespace xwim;
// clang-format off
char* args[] = {
const_cast<char*>("xwim"),
const_cast<char*>("-o"),
const_cast<char*>("/foo/bar baz/output"),
const_cast<char*>("/foo/bar baz/a path"),
const_cast<char*>("/foo/bar baz/another path"),
nullptr};
// clang-format on
UserOpt uo = UserOpt{5, args};
ASSERT_TRUE(uo.paths.find(std::filesystem::path("/foo/bar baz/a path")) !=
uo.paths.end());
ASSERT_TRUE(uo.paths.find(std::filesystem::path("/foo/bar baz/another path")) !=
uo.paths.end());
ASSERT_TRUE(uo.out == std::filesystem::path("/foo/bar baz/output"));
}
TEST(UserOpt, output_defaults_to_nullopt) {
using namespace xwim;
// clang-format off
char* args[] = {
const_cast<char*>("xwim"),
const_cast<char*>("/foo/bar"),
nullptr};
// clang-format on
UserOpt uo = UserOpt{2, args};
ASSERT_FALSE(uo.out);
}