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