1// SPDX-License-Identifier: 0BSD
2
3///////////////////////////////////////////////////////////////////////////////
4//
5/// \file       suffix.c
6/// \brief      Checks filename suffix and creates the destination filename
7//
8//  Author:     Lasse Collin
9//
10///////////////////////////////////////////////////////////////////////////////
11
12#include "private.h"
13
14#ifdef __DJGPP__
15#	include <fcntl.h>
16#endif
17
18// For case-insensitive filename suffix on case-insensitive systems
19#if defined(TUKLIB_DOSLIKE) || defined(__VMS)
20#	ifdef HAVE_STRINGS_H
21#		include <strings.h>
22#	endif
23#	ifdef _MSC_VER
24#		define suffix_strcmp _stricmp
25#	else
26#		define suffix_strcmp strcasecmp
27#	endif
28#else
29#	define suffix_strcmp strcmp
30#endif
31
32
33static char *custom_suffix = NULL;
34
35
36/// \brief      Test if the char is a directory separator
37static bool
38is_dir_sep(char c)
39{
40#ifdef TUKLIB_DOSLIKE
41	return c == '/' || c == '\\' || c == ':';
42#else
43	return c == '/';
44#endif
45}
46
47
48/// \brief      Test if the string contains a directory separator
49static bool
50has_dir_sep(const char *str)
51{
52#ifdef TUKLIB_DOSLIKE
53	return strpbrk(str, "/\\:") != NULL;
54#else
55	return strchr(str, '/') != NULL;
56#endif
57}
58
59
60#ifdef __DJGPP__
61/// \brief      Test for special suffix used for 8.3 short filenames (SFN)
62///
63/// \return     If str matches *.?- or *.??-, true is returned. Otherwise
64///             false is returned.
65static bool
66has_sfn_suffix(const char *str, size_t len)
67{
68	if (len >= 4 && str[len - 1] == '-' && str[len - 2] != '.'
69			&& !is_dir_sep(str[len - 2])) {
70		// *.?-
71		if (str[len - 3] == '.')
72			return !is_dir_sep(str[len - 4]);
73
74		// *.??-
75		if (len >= 5 && !is_dir_sep(str[len - 3])
76				&& str[len - 4] == '.')
77			return !is_dir_sep(str[len - 5]);
78	}
79
80	return false;
81}
82#endif
83
84
85/// \brief      Checks if src_name has given compressed_suffix
86///
87/// \param      suffix      Filename suffix to look for
88/// \param      src_name    Input filename
89/// \param      src_len     strlen(src_name)
90///
91/// \return     If src_name has the suffix, src_len - strlen(suffix) is
92///             returned. It's always a positive integer. Otherwise zero
93///             is returned.
94static size_t
95test_suffix(const char *suffix, const char *src_name, size_t src_len)
96{
97	const size_t suffix_len = strlen(suffix);
98
99	// The filename must have at least one character in addition to
100	// the suffix. src_name may contain path to the filename, so we
101	// need to check for directory separator too.
102	if (src_len <= suffix_len
103			|| is_dir_sep(src_name[src_len - suffix_len - 1]))
104		return 0;
105
106	if (suffix_strcmp(suffix, src_name + src_len - suffix_len) == 0)
107		return src_len - suffix_len;
108
109	return 0;
110}
111
112
113/// \brief      Removes the filename suffix of the compressed file
114///
115/// \return     Name of the uncompressed file, or NULL if file has unknown
116///             suffix.
117static char *
118uncompressed_name(const char *src_name, const size_t src_len)
119{
120	static const struct {
121		const char *compressed;
122		const char *uncompressed;
123	} suffixes[] = {
124		{ ".xz",    "" },
125		{ ".txz",   ".tar" }, // .txz abbreviation for .txt.gz is rare.
126		{ ".lzma",  "" },
127#ifdef __DJGPP__
128		{ ".lzm",   "" },
129#endif
130		{ ".tlz",   ".tar" }, // Both .tar.lzma and .tar.lz
131#ifdef HAVE_LZIP_DECODER
132		{ ".lz",    "" },
133#endif
134	};
135
136	const char *new_suffix = "";
137	size_t new_len = 0;
138
139	if (opt_format != FORMAT_RAW) {
140		for (size_t i = 0; i < ARRAY_SIZE(suffixes); ++i) {
141			new_len = test_suffix(suffixes[i].compressed,
142					src_name, src_len);
143			if (new_len != 0) {
144				new_suffix = suffixes[i].uncompressed;
145				break;
146			}
147		}
148
149#ifdef __DJGPP__
150		// Support also *.?- -> *.? and *.??- -> *.?? on DOS.
151		// This is done also when long filenames are available
152		// to keep it easy to decompress files created when
153		// long filename support wasn't available.
154		if (new_len == 0 && has_sfn_suffix(src_name, src_len)) {
155			new_suffix = "";
156			new_len = src_len - 1;
157		}
158#endif
159	}
160
161	if (new_len == 0 && custom_suffix != NULL)
162		new_len = test_suffix(custom_suffix, src_name, src_len);
163
164	if (new_len == 0) {
165		message_warning(_("%s: Filename has an unknown suffix, "
166				"skipping"), src_name);
167		return NULL;
168	}
169
170	const size_t new_suffix_len = strlen(new_suffix);
171	char *dest_name = xmalloc(new_len + new_suffix_len + 1);
172
173	memcpy(dest_name, src_name, new_len);
174	memcpy(dest_name + new_len, new_suffix, new_suffix_len);
175	dest_name[new_len + new_suffix_len] = '\0';
176
177	return dest_name;
178}
179
180
181/// This message is needed in multiple places in compressed_name(),
182/// so the message has been put into its own function.
183static void
184msg_suffix(const char *src_name, const char *suffix)
185{
186	message_warning(_("%s: File already has '%s' suffix, skipping"),
187			src_name, suffix);
188	return;
189}
190
191
192/// \brief      Appends suffix to src_name
193///
194/// In contrast to uncompressed_name(), we check only suffixes that are valid
195/// for the specified file format.
196static char *
197compressed_name(const char *src_name, size_t src_len)
198{
199	// The order of these must match the order in args.h.
200	static const char *const all_suffixes[][4] = {
201		{
202			".xz",
203			".txz",
204			NULL
205		}, {
206			".lzma",
207#ifdef __DJGPP__
208			".lzm",
209#endif
210			".tlz",
211			NULL
212#ifdef HAVE_LZIP_DECODER
213		// This is needed to keep the table indexing in sync with
214		// enum format_type from coder.h.
215		}, {
216/*
217			".lz",
218*/
219			NULL
220#endif
221		}, {
222			// --format=raw requires specifying the suffix
223			// manually or using stdout.
224			NULL
225		}
226	};
227
228	// args.c ensures these.
229	assert(opt_format != FORMAT_AUTO);
230#ifdef HAVE_LZIP_DECODER
231	assert(opt_format != FORMAT_LZIP);
232#endif
233
234	const size_t format = opt_format - 1;
235	const char *const *suffixes = all_suffixes[format];
236
237	// Look for known filename suffixes and refuse to compress them.
238	for (size_t i = 0; suffixes[i] != NULL; ++i) {
239		if (test_suffix(suffixes[i], src_name, src_len) != 0) {
240			msg_suffix(src_name, suffixes[i]);
241			return NULL;
242		}
243	}
244
245#ifdef __DJGPP__
246	// Recognize also the special suffix that is used when long
247	// filename (LFN) support isn't available. This suffix is
248	// recognized on LFN systems too.
249	if (opt_format == FORMAT_XZ && has_sfn_suffix(src_name, src_len)) {
250		msg_suffix(src_name, "-");
251		return NULL;
252	}
253#endif
254
255	if (custom_suffix != NULL) {
256		if (test_suffix(custom_suffix, src_name, src_len) != 0) {
257			msg_suffix(src_name, custom_suffix);
258			return NULL;
259		}
260	}
261
262	const char *suffix = custom_suffix != NULL
263			? custom_suffix : suffixes[0];
264	size_t suffix_len = strlen(suffix);
265
266#ifdef __DJGPP__
267	if (!_use_lfn(src_name)) {
268		// Long filename (LFN) support isn't available and we are
269		// limited to 8.3 short filenames (SFN).
270		//
271		// Look for suffix separator from the filename, and make sure
272		// that it is in the filename, not in a directory name.
273		const char *sufsep = strrchr(src_name, '.');
274		if (sufsep == NULL || sufsep[1] == '\0'
275				|| has_dir_sep(sufsep)) {
276			// src_name has no filename extension.
277			//
278			// Examples:
279			// xz foo         -> foo.xz
280			// xz -F lzma foo -> foo.lzm
281			// xz -S x foo    -> foox
282			// xz -S x foo.   -> foo.x
283			// xz -S x.y foo  -> foox.y
284			// xz -S .x foo   -> foo.x
285			// xz -S .x foo.  -> foo.x
286			//
287			// Avoid double dots:
288			if (sufsep != NULL && sufsep[1] == '\0'
289					&& suffix[0] == '.')
290				--src_len;
291
292		} else if (custom_suffix == NULL
293				&& strcasecmp(sufsep, ".tar") == 0) {
294			// ".tar" is handled specially.
295			//
296			// Examples:
297			// xz foo.tar          -> foo.txz
298			// xz -F lzma foo.tar  -> foo.tlz
299			static const char *const tar_suffixes[] = {
300				".txz", // .tar.xz
301				".tlz", // .tar.lzma
302/*
303				".tlz", // .tar.lz
304*/
305			};
306			suffix = tar_suffixes[format];
307			suffix_len = 4;
308			src_len -= 4;
309
310		} else {
311			if (custom_suffix == NULL && opt_format == FORMAT_XZ) {
312				// Instead of the .xz suffix, use a single
313				// character at the end of the filename
314				// extension. This is to minimize name
315				// conflicts when compressing multiple files
316				// with the same basename. E.g. foo.txt and
317				// foo.exe become foo.tx- and foo.ex-. Dash
318				// is rare as the last character of the
319				// filename extension, so it seems to be
320				// quite safe choice and it stands out better
321				// in directory listings than e.g. x. For
322				// comparison, gzip uses z.
323				suffix = "-";
324				suffix_len = 1;
325			}
326
327			if (suffix[0] == '.') {
328				// The first character of the suffix is a dot.
329				// Throw away the original filename extension
330				// and replace it with the new suffix.
331				//
332				// Examples:
333				// xz -F lzma foo.txt  -> foo.lzm
334				// xz -S .x  foo.txt   -> foo.x
335				src_len = sufsep - src_name;
336
337			} else {
338				// The first character of the suffix is not
339				// a dot. Preserve the first 0-2 characters
340				// of the original filename extension.
341				//
342				// Examples:
343				// xz foo.txt         -> foo.tx-
344				// xz -S x  foo.c     -> foo.cx
345				// xz -S ab foo.c     -> foo.cab
346				// xz -S ab foo.txt   -> foo.tab
347				// xz -S abc foo.txt  -> foo.abc
348				//
349				// Truncate the suffix to three chars:
350				if (suffix_len > 3)
351					suffix_len = 3;
352
353				// If needed, overwrite 1-3 characters.
354				if (strlen(sufsep) > 4 - suffix_len)
355					src_len = sufsep - src_name
356							+ 4 - suffix_len;
357			}
358		}
359	}
360#endif
361
362	char *dest_name = xmalloc(src_len + suffix_len + 1);
363
364	memcpy(dest_name, src_name, src_len);
365	memcpy(dest_name + src_len, suffix, suffix_len);
366	dest_name[src_len + suffix_len] = '\0';
367
368	return dest_name;
369}
370
371
372extern char *
373suffix_get_dest_name(const char *src_name)
374{
375	assert(src_name != NULL);
376
377	// Length of the name is needed in all cases to locate the end of
378	// the string to compare the suffix, so calculate the length here.
379	const size_t src_len = strlen(src_name);
380
381	return opt_mode == MODE_COMPRESS
382			? compressed_name(src_name, src_len)
383			: uncompressed_name(src_name, src_len);
384}
385
386
387extern void
388suffix_set(const char *suffix)
389{
390	// Empty suffix and suffixes having a directory separator are
391	// rejected. Such suffixes would break things later.
392	if (suffix[0] == '\0' || has_dir_sep(suffix))
393		message_fatal(_("%s: Invalid filename suffix"), suffix);
394
395	// Replace the old custom_suffix (if any) with the new suffix.
396	free(custom_suffix);
397	custom_suffix = xstrdup(suffix);
398	return;
399}
400
401
402extern bool
403suffix_is_set(void)
404{
405	return custom_suffix != NULL;
406}
407