Compare commits

...

23 commits
0.3 ... 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
a7737533c2
Update README
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-02-25 21:10:31 +01:00
df9c9d48cc
Publish build artifacts to dirl
All checks were successful
continuous-integration/drone/push Build is passing
Publish build artifacts to
https://dirlist.friedl.net/cicd/xwim
2021-02-25 19:51:30 +01:00
f80dc1decd
Strip release
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-24 22:23:14 +01:00
8873e7930b
Static release build, cleanup
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-02-24 22:20:28 +01:00
a6fb93a484
Static build, disable test and coverage
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-24 22:10:52 +01:00
fe4a5f4460
Use default_library for static build toggle
Some checks failed
continuous-integration/drone/push Build is failing
2021-02-24 21:25:17 +01:00
c3d9dd6360
Fix multiple folder compression
Some checks failed
continuous-integration/drone/push Build is failing
2021-02-21 12:07:19 +01:00
24f1407ed9
Fix folder compression
Some checks failed
continuous-integration/drone/push Build is failing
2021-02-21 12:02:01 +01:00
12afa628d0
Empty trailing path, bump version
Some checks failed
continuous-integration/drone/push Build is failing
2021-02-21 11:17:42 +01:00
6a7e98dbf6
Move extraction output according to plan
Some checks failed
continuous-integration/drone/push Build is failing
2021-02-20 07:26:07 +01:00
20 changed files with 1052 additions and 392 deletions

View file

@ -3,17 +3,38 @@ type: docker
name: default name: default
steps: steps:
- name: build - name: build-shared
image: arminfriedl/xwim-build image: arminfriedl/xwim-build:shared
commands: commands:
- meson wrap install gtest - meson wrap install gtest || true
- meson build -Db_coverage=true - meson target/shared
- ninja -C build - ninja -C target/shared
- ninja -C build test && ninja -C build coverage - mv target/shared/src/xwim xwim-x86_64-glibc-linux-shared
- echo "******** TEST LOGS ***********"
- cat build/meson-logs/testlog.txt - name: build-static
- echo "****** COVERAGE LOGS *********" image: arminfriedl/xwim-build:static
- cat build/meson-logs/coverage.txt commands:
- meson wrap install gtest || true
- meson --default-library=static target/static
- ninja -C target/static
- mv target/static/src/xwim xwim-x86_64-musl-linux-static
- name: publish-binaries
image: appleboy/drone-scp
settings:
host: friedl.net
username:
from_secret: deploy_user
password:
from_secret: deploy_password
port: 22
target: /var/services/dirlist/repo/cicd/xwim/${DRONE_COMMIT_SHA:0:8}/
source:
- xwim-x86_64-glibc-linux-shared
- xwim-x86_64-musl-linux-static
depends_on:
- build-shared
- build-static
trigger: trigger:
event: event:
@ -26,21 +47,34 @@ type: docker
name: release name: release
steps: steps:
- name: build - name: build-shared
image: arminfriedl/xwim-build image: arminfriedl/xwim-build:shared
commands: commands:
- meson wrap install gtest - meson wrap install gtest || true
- meson --buildtype=release build - meson --buildtype=release target/shared
- ninja -C build - ninja -C target/shared
- mkdir xwim-${DRONE_TAG}-x86_64-glibc-linux - strip target/shared/src/xwim
- mv build/src/xwim xwim-${DRONE_TAG}-x86_64-glibc-linux - mkdir xwim-${DRONE_TAG}-x86_64-glibc-linux-shared
- mv target/shared/src/xwim xwim-${DRONE_TAG}-x86_64-glibc-linux-shared
- name: build-static
image: arminfriedl/xwim-build:static
commands:
- meson wrap install gtest || true
- meson --buildtype=release --default-library=static target/static
- ninja -C target/static
- strip target/static/src/xwim
- mkdir xwim-${DRONE_TAG}-x86_64-musl-linux-static
- mv target/static/src/xwim xwim-${DRONE_TAG}-x86_64-musl-linux-static
- name: package - name: package
image: arminfriedl/xwim-build image: arminfriedl/xwim-build
commands: commands:
- tar cjf xwim-${DRONE_TAG}-x86_64-glibc-linux.tar.bz2 xwim-${DRONE_TAG}-x86_64-glibc-linux/xwim - tar czf xwim-${DRONE_TAG}-x86_64-glibc-linux-shared.tar.gz xwim-${DRONE_TAG}-x86_64-glibc-linux-shared/xwim
- tar czf xwim-${DRONE_TAG}-x86_64-glibc-linux.tar.gz xwim-${DRONE_TAG}-x86_64-glibc-linux/xwim - tar czf xwim-${DRONE_TAG}-x86_64-musl-linux-static.tar.gz xwim-${DRONE_TAG}-x86_64-musl-linux-static/xwim
- zip -r xwim-${DRONE_TAG}-x86_64-glibc-linux.zip xwim-${DRONE_TAG}-x86_64-glibc-linux depends_on:
- build-shared
- build-static
- name: publish - name: publish
image: plugins/gitea-release image: plugins/gitea-release
@ -49,13 +83,14 @@ steps:
api_key: api_key:
from_secret: gitea_token from_secret: gitea_token
files: files:
- xwim-${DRONE_TAG}-x86_64-glibc-linux.tar.bz2 - xwim-${DRONE_TAG}-x86_64-glibc-linux-shared.tar.gz
- xwim-${DRONE_TAG}-x86_64-glibc-linux.tar.gz - xwim-${DRONE_TAG}-x86_64-musl-linux-static.tar.gz
- xwim-${DRONE_TAG}-x86_64-glibc-linux.zip
title: xwim ${DRONE_TAG} title: xwim ${DRONE_TAG}
checksum: checksum:
- md5 - md5
- sha256 - sha256
depends_on:
- package
trigger: trigger:
event: event:

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

@ -7,7 +7,7 @@ Do What I Mean Extractor
[xkcd-1168](https://xkcd.com/1168/) [xkcd-1168](https://xkcd.com/1168/)
Continuing the emacs tradition of "Do What I Mean" tools, xwim is a replacement Continuing the emacs tradition of "Do What I Mean" tools, xwim is replacement
for the excellent, but unfortunately unmaintained, for the excellent, but unfortunately unmaintained,
[dtrx](https://github.com/brettcs/dtrx). xwim is a command line tool that [dtrx](https://github.com/brettcs/dtrx). xwim is a command line tool that
targets two problems with archives: targets two problems with archives:
@ -17,6 +17,27 @@ considerably between formats
- Inconsiderately packaged archives tend to spill their content over the - Inconsiderately packaged archives tend to spill their content over the
directory they are extracted to directory they are extracted to
`dtrx` is a Python script that sets up the command line and calls appropriate
archiving binaries (if installed). In contrast `xwim` is a compiled binary based
directly on archiving libraries, which some may appreciate. It can optionally be
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:
@ -25,8 +46,8 @@ xwim archive.tar.gz
``` ```
This will extract the archive to the current folder. If the archive contains a This will extract the archive to the current folder. If the archive contains a
single root folder it is just extracted as is. Otherwise xwim first creates a single root folder it is just extracted as is. Otherwise xwim creates a folder
folder named after the archive and extracts the contents there. named after the archive and extracts the contents there.
```shell ```shell
@ -77,26 +98,13 @@ xwim will create a folder `archive` in the current directory and extract the
archive contents there. archive contents there.
# Supported formats # Supported formats
xwim supports most formats supported by [libarchive](https://libarchive.org/): Currently `xwim` supports `tar.gz` and `zip` archives. However, this will
rapidly expand to many more formats until a stable release is officially
announced.
- 7-zip: 7z, 7zip Take a look `Archiver.hpp` if you want to help and have some time for testing.
- zip: jar, zip Most formats can readily be added if they are supported by libarchive. For other
- bzip2: bz2, bzip2 formats you have to add an `Archiver` implementation.
- gzip: gz, gzip
- xzip: xz
- rar: rar
- tar with compression: tgz, tar.gz, tar.bz2, tar.xz
# Install
xwim is currently released as a dynamically linked glibc binary only. The
releases can be downloaded from https://git.friedl.net/incubator/xwim/releases
and should run on most glibc based GNU/Linux distributions. 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)
Approaching the first stable release we will release for more platforms.
# 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
@ -161,7 +169,12 @@ mail for an account on https://git.friedl.net.
- <strong>Parsing filters is unsupported</strong> - <strong>Parsing filters is unsupported</strong>
There is a somewhat long standing There is a somewhat long standing
[bug](https://github.com/libarchive/libarchive/issues/373) in the underlying [bug](https://github.com/libarchive/libarchive/issues/373) in libarchive. rar
libarchive library. rar files might fail with `Parsing filters is files might fail with `Parsing filters is unsupported`. This is because `rar`
unsupported`. In case you run into this issue, the only workaround for now is is a proprietary format and `libarchive` does not implement the full machinery
to use another extraction tool. necessary to support `rar` completely. `xwim` is all about convenience. If you
want to help with supporting `rar`, please keep in mind that this means we
have we want to take the [official `unrar`
library](https://www.rarlab.com/rar_add.htm) if possible. This is also a
licensing issue as `unrar` is proprietary and its license seemingly not GPL
compatible.

View file

@ -1,11 +1,13 @@
project('xwim', 'cpp', project('xwim', 'cpp',
version: '0.3', version: '0.4',
default_options: ['cpp_std=c++17', default_options: ['cpp_std=c++17',
'warning_level=3', 'warning_level=3',
'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;
@ -18,17 +26,26 @@ fs::path archive_extension(const fs::path& path) {
// TODO: creates lots of paths, refactor // TODO: creates lots of paths, refactor
fs::path ext; fs::path ext;
fs::path tmp_ext; fs::path tmp_ext;
fs::path tmp_path = path; fs::path tmp_path;
// cater for trailing `/` which is represented
// as empty path element
for (auto p : path) {
if (!p.empty()) {
tmp_path /= p;
}
}
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;
} // else: (Combined) extension not known, keep `ext` as-is but try longer } // else: (Combined) extension not known, keep `ext` as-is but try
// extensions // longer extensions
tmp_path = tmp_path.stem(); tmp_path = tmp_path.stem();
} }
@ -40,31 +57,55 @@ fs::path archive_extension(const fs::path& path) {
fs::path strip_archive_extension(const fs::path& path) { fs::path strip_archive_extension(const fs::path& path) {
// TODO: creates lots of paths, refactor // TODO: creates lots of paths, refactor
int longest_ext = 0; int longest_ext = 0;
int tmp_longest_ext = 0;
fs::path tmp_ext; fs::path tmp_ext;
fs::path tmp_path = path; fs::path tmp_path;
fs::path stem_path = path; fs::path stem_path;
// cater for trailing `/` which is represented
// as empty path element
for(auto p: path) {
if(!p.empty()) {
tmp_path /= p;
}
}
stem_path = tmp_path;
spdlog::debug("Checking {} extensions", tmp_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); spdlog::debug("Looking for {} in known extensions", tmp_ext);
if (search != extensions_format.end()) { Format format = find_extension_format(tmp_ext);
tmp_longest_ext++;
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++; longest_ext = tmp_longest_ext;
} // else: (Combined) extension not known, keep `longest_ext` as-is but try longer } // else: (Combined) extension not known, keep `longest_ext` as-is but try
// extensions // longer extensions
spdlog::debug("Stemming {} to {}", tmp_path, tmp_path.stem());
tmp_path = tmp_path.stem(); tmp_path = tmp_path.stem();
} }
tmp_path = path; spdlog::debug("Found {} extensions", longest_ext);
for(int i=0; i<longest_ext; i++) tmp_path = tmp_path.stem(); tmp_path = stem_path;
for (int i = 0; i < longest_ext; i++) tmp_path = tmp_path.stem();
spdlog::debug("Stripped path is {} ", tmp_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);
@ -79,19 +120,22 @@ 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::ZIP: case Format::TAR_COMPRESS: case Format::TAR_LZIP:
return make_unique<LibArchiver>(); case Format::TAR_XZ: case Format::TAR_ZSTD:
case Format::ZIP:
return make_unique<LibArchiver>();
default: default:
throw XwimError{ throw XwimError{
"Cannot construct archiver for {}. `extension_format` surjection " "Cannot construct archiver for {}. `extension_format` surjection "
@ -99,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,134 +0,0 @@
#include "Xwim.hpp"
#include <spdlog/spdlog.h>
#include <cstdlib>
#include <filesystem>
#include <ios>
#include <iostream>
#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);
break;
default:
spdlog::error("Unknown action");
}
}
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());
fs::path path = (*ins.begin()).stem();
path += default_extension;
out = path;
} 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()));
}
}
}

View file

@ -1,62 +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();
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;
@ -20,40 +20,69 @@ static int copy_data(shared_ptr<archive> reader, shared_ptr<archive> writer);
void LibArchiver::compress(set<fs::path> ins, fs::path archive_out) { void LibArchiver::compress(set<fs::path> ins, fs::path archive_out) {
spdlog::debug("Compressing to {}", archive_out); spdlog::debug("Compressing to {}", archive_out);
int r; // libarchive error handling
static char buff[16384]; // read buffer
// cannot use unique_ptr here since unique_ptr requires a // cannot use unique_ptr here since unique_ptr requires a
// 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());
archive_entry *entry = archive_entry_new(); shared_ptr<archive> reader;
char buff[8192];
for(auto path: ins) { shared_ptr<archive_entry> entry = shared_ptr<archive_entry>(archive_entry_new(), archive_entry_free);
archive_entry_set_pathname(entry, path.c_str());
archive_entry_set_size(entry, fs::file_size(path));
archive_entry_set_filetype(entry, AE_IFREG);
archive_entry_set_perm(entry, 0644);
archive_write_header(writer.get(), entry);
int fd = open(path.c_str(), O_RDONLY); for (auto in : ins) {
int len = read(fd, buff, sizeof(buff)); spdlog::debug("Compressing {}", in);
while (len > 0) { reader = shared_ptr<archive>(archive_read_disk_new(), archive_read_free);
archive_write_data(writer.get(), buff, len); archive_read_disk_set_standard_lookup(reader.get());
len = read(fd, buff, sizeof(buff));
r = archive_read_disk_open(reader.get(), in.c_str());
if (r != ARCHIVE_OK) {
throw XwimError{"Failed opening {}. {}", in,
archive_error_string(reader.get())};
} }
close(fd); for (;;) {
archive_entry_clear(entry); r = archive_read_next_header2(reader.get(), entry.get());
if (r == ARCHIVE_EOF) break;
if (r != ARCHIVE_OK) {
throw XwimError{"Failed compressing archive entry. {}",
archive_error_string(reader.get())};
}
spdlog::debug("Adding {} to archive", archive_entry_pathname(entry.get()));
r = archive_write_header(writer.get(), entry.get());
if (r != ARCHIVE_OK) {
throw XwimError{"Failed writing archive entry. {}",
archive_error_string(writer.get())};
}
/* For now, we use a simpler loop to copy data
* into the target archive. */
int fd = open(archive_entry_sourcepath(entry.get()), O_RDONLY);
ssize_t len = read(fd, buff, sizeof(buff));
while (len > 0) {
archive_write_data(writer.get(), buff, len);
len = read(fd, buff, sizeof(buff));
}
close(fd);
archive_entry_clear(entry.get());
archive_read_disk_descend(reader.get());
}
} }
} }
void LibArchiver::extract(fs::path archive_in, fs::path out) { void LibArchiver::extract(fs::path archive_in, fs::path out) {
spdlog::debug("Extracting archive {} to {}", archive_in, out); spdlog::debug("Extracting archive {} to {}", archive_in, out);
int r; // libarchive error handling int r; // libarchive error handling
// cannot use unique_ptr here since unique_ptr requires a // cannot use unique_ptr here since unique_ptr requires a
// complete type. `archive` is forward declared only. // complete type. `archive` is forward declared only.
@ -75,18 +104,20 @@ void LibArchiver::extract(fs::path archive_in, fs::path out) {
fs::path cur_path = fs::current_path(); fs::path cur_path = fs::current_path();
fs::current_path(out); fs::current_path(out);
archive_entry* entry; archive_entry *entry;
for (;;) { for (;;) {
r = archive_read_next_header(reader.get(), &entry); r = archive_read_next_header(reader.get(), &entry);
if (r == ARCHIVE_EOF) break; if (r == ARCHIVE_EOF) break;
if (r != ARCHIVE_OK) { if (r != ARCHIVE_OK) {
throw XwimError{"Failed extracting archive entry. {}", archive_error_string(reader.get())}; throw XwimError{"Failed extracting archive entry. {}",
archive_error_string(reader.get())};
} }
r = archive_write_header(writer.get(), entry); r = archive_write_header(writer.get(), entry);
if (r != ARCHIVE_OK) { if (r != ARCHIVE_OK) {
throw XwimError{"Failed writing archive entry header. {}", archive_error_string(writer.get())}; throw XwimError{"Failed writing archive entry header. {}",
archive_error_string(writer.get())};
} }
if (archive_entry_size(entry) > 0) { if (archive_entry_size(entry) > 0) {

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,9 +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']
xwim_libs = [dependency('libarchive', required: true), is_static = get_option('default_library')=='static'
dependency('fmt', required: true),
dependency('spdlog', required: true), xwim_libs = [dependency('libarchive', required: true, static: is_static),
dependency('tclap', required: true)] dependency('spdlog', required: true, static: is_static),
dependency('fmt', 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);
}