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
33 static char *custom_suffix = NULL;
34
35
36 /// \brief Test if the char is a directory separator
37 static bool
is_dir_sep(char c)38 is_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
49 static bool
has_dir_sep(const char * str)50 has_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.
65 static bool
has_sfn_suffix(const char * str,size_t len)66 has_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.
94 static size_t
test_suffix(const char * suffix,const char * src_name,size_t src_len)95 test_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.
117 static char *
uncompressed_name(const char * src_name,const size_t src_len)118 uncompressed_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.
183 static void
msg_suffix(const char * src_name,const char * suffix)184 msg_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.
196 static char *
compressed_name(const char * src_name,size_t src_len)197 compressed_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
372 extern char *
suffix_get_dest_name(const char * src_name)373 suffix_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
387 extern void
suffix_set(const char * suffix)388 suffix_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
402 extern bool
suffix_is_set(void)403 suffix_is_set(void)
404 {
405 return custom_suffix != NULL;
406 }
407