///////////////////////////////////////////////////////////////////////////////
//
/// \file suffix.c
/// \brief Checks filename suffix and creates the destination filename
//
// Author: Lasse Collin
//
// This file has been put into the public domain.
// You can do whatever you want with this file.
//
///////////////////////////////////////////////////////////////////////////////
#include "private.h"
// For case-insensitive filename suffix on case-insensitive systems
#if defined(TUKLIB_DOSLIKE) || defined(__VMS)
# define strcmp strcasecmp
#endif
static char *custom_suffix = NULL;
/// \brief Test if the char is a directory separator
static bool
is_dir_sep(char c)
{
#ifdef TUKLIB_DOSLIKE
return c == '/' || c == '\\' || c == ':';
#else
return c == '/';
#endif
}
/// \brief Test if the string contains a directory separator
static bool
has_dir_sep(const char *str)
{
#ifdef TUKLIB_DOSLIKE
return strpbrk(str, "/\\:") != NULL;
#else
return strchr(str, '/') != NULL;
#endif
}
/// \brief Checks if src_name has given compressed_suffix
///
/// \param suffix Filename suffix to look for
/// \param src_name Input filename
/// \param src_len strlen(src_name)
///
/// \return If src_name has the suffix, src_len - strlen(suffix) is
/// returned. It's always a positive integer. Otherwise zero
/// is returned.
static size_t
test_suffix(const char *suffix, const char *src_name, size_t src_len)
{
const size_t suffix_len = strlen(suffix);
// The filename must have at least one character in addition to
// the suffix. src_name may contain path to the filename, so we
// need to check for directory separator too.
if (src_len <= suffix_len
|| is_dir_sep(src_name[src_len - suffix_len - 1]))
return 0;
if (strcmp(suffix, src_name + src_len - suffix_len) == 0)
return src_len - suffix_len;
return 0;
}
/// \brief Removes the filename suffix of the compressed file
///
/// \return Name of the uncompressed file, or NULL if file has unknown
/// suffix.
static char *
uncompressed_name(const char *src_name, const size_t src_len)
{
static const struct {
const char *compressed;
const char *uncompressed;
} suffixes[] = {
{ ".xz", "" },
{ ".txz", ".tar" }, // .txz abbreviation for .txt.gz is rare.
{ ".lzma", "" },
{ ".tlz", ".tar" },
// { ".gz", "" },
// { ".tgz", ".tar" },
};
const char *new_suffix = "";
size_t new_len = 0;
if (opt_format == FORMAT_RAW) {
// Don't check for known suffixes when --format=raw was used.
if (custom_suffix == NULL) {
message_error(_("%s: With --format=raw, "
"--suffix=.SUF is required unless "
"writing to stdout"), src_name);
return NULL;
}
} else {
for (size_t i = 0; i < ARRAY_SIZE(suffixes); ++i) {
new_len = test_suffix(suffixes[i].compressed,
src_name, src_len);
if (new_len != 0) {
new_suffix = suffixes[i].uncompressed;
break;
}
}
}
if (new_len == 0 && custom_suffix != NULL)
new_len = test_suffix(custom_suffix, src_name, src_len);
if (new_len == 0) {
message_warning(_("%s: Filename has an unknown suffix, "
"skipping"), src_name);
return NULL;
}
const size_t new_suffix_len = strlen(new_suffix);
char *dest_name = xmalloc(new_len + new_suffix_len + 1);
memcpy(dest_name, src_name, new_len);
memcpy(dest_name + new_len, new_suffix, new_suffix_len);
dest_name[new_len + new_suffix_len] = '\0';
return dest_name;
}
/// \brief Appends suffix to src_name
///
/// In contrast to uncompressed_name(), we check only suffixes that are valid
/// for the specified file format.
static char *
compressed_name(const char *src_name, const size_t src_len)
{
// The order of these must match the order in args.h.
static const char *const all_suffixes[][3] = {
{
".xz",
".txz",
NULL
}, {
".lzma",
".tlz",
NULL
/*
}, {
".gz",
".tgz",
NULL
*/
}, {
// --format=raw requires specifying the suffix
// manually or using stdout.
NULL
}
};
// args.c ensures this.
assert(opt_format != FORMAT_AUTO);
const size_t format = opt_format - 1;
const char *const *suffixes = all_suffixes[format];
for (size_t i = 0; suffixes[i] != NULL; ++i) {
if (test_suffix(suffixes[i], src_name, src_len) != 0) {
message_warning(_("%s: File already has `%s' "
"suffix, skipping"), src_name,
suffixes[i]);
return NULL;
}
}
if (custom_suffix != NULL) {
if (test_suffix(custom_suffix, src_name, src_len) != 0) {
message_warning(_("%s: File already has `%s' "
"suffix, skipping"), src_name,
custom_suffix);
return NULL;
}
}
// TODO: Hmm, maybe it would be better to validate this in args.c,
// since the suffix handling when decoding is weird now.
if (opt_format == FORMAT_RAW && custom_suffix == NULL) {
message_error(_("%s: With --format=raw, "
"--suffix=.SUF is required unless "
"writing to stdout"), src_name);
return NULL;
}
const char *suffix = custom_suffix != NULL
? custom_suffix : suffixes[0];
const size_t suffix_len = strlen(suffix);
char *dest_name = xmalloc(src_len + suffix_len + 1);
memcpy(dest_name, src_name, src_len);
memcpy(dest_name + src_len, suffix, suffix_len);
dest_name[src_len + suffix_len] = '\0';
return dest_name;
}
extern char *
suffix_get_dest_name(const char *src_name)
{
assert(src_name != NULL);
// Length of the name is needed in all cases to locate the end of
// the string to compare the suffix, so calculate the length here.
const size_t src_len = strlen(src_name);
return opt_mode == MODE_COMPRESS
? compressed_name(src_name, src_len)
: uncompressed_name(src_name, src_len);
}
extern void
suffix_set(const char *suffix)
{
// Empty suffix and suffixes having a directory separator are
// rejected. Such suffixes would break things later.
if (suffix[0] == '\0' || has_dir_sep(suffix))
message_fatal(_("%s: Invalid filename suffix"), suffix);
// Replace the old custom_suffix (if any) with the new suffix.
free(custom_suffix);
custom_suffix = xstrdup(suffix);
return;
}