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 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 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 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 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 * 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 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 * 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 * 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 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 403 suffix_is_set(void) 404 { 405 return custom_suffix != NULL; 406 } 407