1 1.1 christos /* 2 1.1 christos * Copyright (c) Meta Platforms, Inc. and affiliates. 3 1.1 christos * All rights reserved. 4 1.1 christos * 5 1.1 christos * This source code is licensed under both the BSD-style license (found in the 6 1.1 christos * LICENSE file in the root directory of this source tree) and the GPLv2 (found 7 1.1 christos * in the COPYING file in the root directory of this source tree). 8 1.1 christos */ 9 1.1 christos #include "Options.h" 10 1.1 christos #include "util.h" 11 1.1 christos #include "utils/ScopeGuard.h" 12 1.1 christos 13 1.1 christos #include <algorithm> 14 1.1 christos #include <cassert> 15 1.1 christos #include <cstdio> 16 1.1 christos #include <cstring> 17 1.1 christos #include <iterator> 18 1.1 christos #include <thread> 19 1.1 christos #include <vector> 20 1.1 christos 21 1.1 christos 22 1.1 christos namespace pzstd { 23 1.1 christos 24 1.1 christos namespace { 25 1.1 christos unsigned defaultNumThreads() { 26 1.1 christos #ifdef PZSTD_NUM_THREADS 27 1.1 christos return PZSTD_NUM_THREADS; 28 1.1 christos #else 29 1.1 christos return std::thread::hardware_concurrency(); 30 1.1 christos #endif 31 1.1 christos } 32 1.1 christos 33 1.1 christos unsigned parseUnsigned(const char **arg) { 34 1.1 christos unsigned result = 0; 35 1.1 christos while (**arg >= '0' && **arg <= '9') { 36 1.1 christos result *= 10; 37 1.1 christos result += **arg - '0'; 38 1.1 christos ++(*arg); 39 1.1 christos } 40 1.1 christos return result; 41 1.1 christos } 42 1.1 christos 43 1.1 christos const char *getArgument(const char *options, const char **argv, int &i, 44 1.1 christos int argc) { 45 1.1 christos if (options[1] != 0) { 46 1.1 christos return options + 1; 47 1.1 christos } 48 1.1 christos ++i; 49 1.1 christos if (i == argc) { 50 1.1 christos std::fprintf(stderr, "Option -%c requires an argument, but none provided\n", 51 1.1 christos *options); 52 1.1 christos return nullptr; 53 1.1 christos } 54 1.1 christos return argv[i]; 55 1.1 christos } 56 1.1 christos 57 1.1 christos const std::string kZstdExtension = ".zst"; 58 1.1 christos constexpr char kStdIn[] = "-"; 59 1.1 christos constexpr char kStdOut[] = "-"; 60 1.1 christos constexpr unsigned kDefaultCompressionLevel = 3; 61 1.1 christos constexpr unsigned kMaxNonUltraCompressionLevel = 19; 62 1.1 christos 63 1.1 christos #ifdef _WIN32 64 1.1 christos const char nullOutput[] = "nul"; 65 1.1 christos #else 66 1.1 christos const char nullOutput[] = "/dev/null"; 67 1.1 christos #endif 68 1.1 christos 69 1.1 christos void notSupported(const char *option) { 70 1.1 christos std::fprintf(stderr, "Operation not supported: %s\n", option); 71 1.1 christos } 72 1.1 christos 73 1.1 christos void usage() { 74 1.1 christos std::fprintf(stderr, "Usage:\n"); 75 1.1 christos std::fprintf(stderr, " pzstd [args] [FILE(s)]\n"); 76 1.1 christos std::fprintf(stderr, "Parallel ZSTD options:\n"); 77 1.1 christos std::fprintf(stderr, " -p, --processes # : number of threads to use for (de)compression (default:<numcpus>)\n"); 78 1.1 christos 79 1.1 christos std::fprintf(stderr, "ZSTD options:\n"); 80 1.1 christos std::fprintf(stderr, " -# : # compression level (1-%d, default:%d)\n", kMaxNonUltraCompressionLevel, kDefaultCompressionLevel); 81 1.1 christos std::fprintf(stderr, " -d, --decompress : decompression\n"); 82 1.1 christos std::fprintf(stderr, " -o file : result stored into `file` (only if 1 input file)\n"); 83 1.1 christos std::fprintf(stderr, " -f, --force : overwrite output without prompting, (de)compress links\n"); 84 1.1 christos std::fprintf(stderr, " --rm : remove source file(s) after successful (de)compression\n"); 85 1.1 christos std::fprintf(stderr, " -k, --keep : preserve source file(s) (default)\n"); 86 1.1 christos std::fprintf(stderr, " -h, --help : display help and exit\n"); 87 1.1 christos std::fprintf(stderr, " -V, --version : display version number and exit\n"); 88 1.1 christos std::fprintf(stderr, " -v, --verbose : verbose mode; specify multiple times to increase log level (default:2)\n"); 89 1.1 christos std::fprintf(stderr, " -q, --quiet : suppress warnings; specify twice to suppress errors too\n"); 90 1.1 christos std::fprintf(stderr, " -c, --stdout : write to standard output (even if it is the console)\n"); 91 1.1 christos #ifdef UTIL_HAS_CREATEFILELIST 92 1.1 christos std::fprintf(stderr, " -r : operate recursively on directories\n"); 93 1.1 christos #endif 94 1.1 christos std::fprintf(stderr, " --ultra : enable levels beyond %i, up to %i (requires more memory)\n", kMaxNonUltraCompressionLevel, ZSTD_maxCLevel()); 95 1.1 christos std::fprintf(stderr, " -C, --check : integrity check (default)\n"); 96 1.1 christos std::fprintf(stderr, " --no-check : no integrity check\n"); 97 1.1 christos std::fprintf(stderr, " -t, --test : test compressed file integrity\n"); 98 1.1 christos std::fprintf(stderr, " -- : all arguments after \"--\" are treated as files\n"); 99 1.1 christos } 100 1.1 christos } // anonymous namespace 101 1.1 christos 102 1.1 christos Options::Options() 103 1.1 christos : numThreads(defaultNumThreads()), maxWindowLog(23), 104 1.1 christos compressionLevel(kDefaultCompressionLevel), decompress(false), 105 1.1 christos overwrite(false), keepSource(true), writeMode(WriteMode::Auto), 106 1.1 christos checksum(true), verbosity(2) {} 107 1.1 christos 108 1.1 christos Options::Status Options::parse(int argc, const char **argv) { 109 1.1 christos bool test = false; 110 1.1 christos bool recursive = false; 111 1.1 christos bool ultra = false; 112 1.1 christos bool forceStdout = false; 113 1.1 christos bool followLinks = false; 114 1.1 christos // Local copy of input files, which are pointers into argv. 115 1.1 christos std::vector<const char *> localInputFiles; 116 1.1 christos for (int i = 1; i < argc; ++i) { 117 1.1 christos const char *arg = argv[i]; 118 1.1 christos // Protect against empty arguments 119 1.1 christos if (arg[0] == 0) { 120 1.1 christos continue; 121 1.1 christos } 122 1.1 christos // Everything after "--" is an input file 123 1.1 christos if (!std::strcmp(arg, "--")) { 124 1.1 christos ++i; 125 1.1 christos std::copy(argv + i, argv + argc, std::back_inserter(localInputFiles)); 126 1.1 christos break; 127 1.1 christos } 128 1.1 christos // Long arguments that don't have a short option 129 1.1 christos { 130 1.1 christos bool isLongOption = true; 131 1.1 christos if (!std::strcmp(arg, "--rm")) { 132 1.1 christos keepSource = false; 133 1.1 christos } else if (!std::strcmp(arg, "--ultra")) { 134 1.1 christos ultra = true; 135 1.1 christos maxWindowLog = 0; 136 1.1 christos } else if (!std::strcmp(arg, "--no-check")) { 137 1.1 christos checksum = false; 138 1.1 christos } else if (!std::strcmp(arg, "--sparse")) { 139 1.1 christos writeMode = WriteMode::Sparse; 140 1.1 christos notSupported("Sparse mode"); 141 1.1 christos return Status::Failure; 142 1.1 christos } else if (!std::strcmp(arg, "--no-sparse")) { 143 1.1 christos writeMode = WriteMode::Regular; 144 1.1 christos notSupported("Sparse mode"); 145 1.1 christos return Status::Failure; 146 1.1 christos } else if (!std::strcmp(arg, "--dictID")) { 147 1.1 christos notSupported(arg); 148 1.1 christos return Status::Failure; 149 1.1 christos } else if (!std::strcmp(arg, "--no-dictID")) { 150 1.1 christos notSupported(arg); 151 1.1 christos return Status::Failure; 152 1.1 christos } else { 153 1.1 christos isLongOption = false; 154 1.1 christos } 155 1.1 christos if (isLongOption) { 156 1.1 christos continue; 157 1.1 christos } 158 1.1 christos } 159 1.1 christos // Arguments with a short option simply set their short option. 160 1.1 christos const char *options = nullptr; 161 1.1 christos if (!std::strcmp(arg, "--processes")) { 162 1.1 christos options = "p"; 163 1.1 christos } else if (!std::strcmp(arg, "--version")) { 164 1.1 christos options = "V"; 165 1.1 christos } else if (!std::strcmp(arg, "--help")) { 166 1.1 christos options = "h"; 167 1.1 christos } else if (!std::strcmp(arg, "--decompress")) { 168 1.1 christos options = "d"; 169 1.1 christos } else if (!std::strcmp(arg, "--force")) { 170 1.1 christos options = "f"; 171 1.1 christos } else if (!std::strcmp(arg, "--stdout")) { 172 1.1 christos options = "c"; 173 1.1 christos } else if (!std::strcmp(arg, "--keep")) { 174 1.1 christos options = "k"; 175 1.1 christos } else if (!std::strcmp(arg, "--verbose")) { 176 1.1 christos options = "v"; 177 1.1 christos } else if (!std::strcmp(arg, "--quiet")) { 178 1.1 christos options = "q"; 179 1.1 christos } else if (!std::strcmp(arg, "--check")) { 180 1.1 christos options = "C"; 181 1.1 christos } else if (!std::strcmp(arg, "--test")) { 182 1.1 christos options = "t"; 183 1.1 christos } else if (arg[0] == '-' && arg[1] != 0) { 184 1.1 christos options = arg + 1; 185 1.1 christos } else { 186 1.1 christos localInputFiles.emplace_back(arg); 187 1.1 christos continue; 188 1.1 christos } 189 1.1 christos assert(options != nullptr); 190 1.1 christos 191 1.1 christos bool finished = false; 192 1.1 christos while (!finished && *options != 0) { 193 1.1 christos // Parse the compression level 194 1.1 christos if (*options >= '0' && *options <= '9') { 195 1.1 christos compressionLevel = parseUnsigned(&options); 196 1.1 christos continue; 197 1.1 christos } 198 1.1 christos 199 1.1 christos switch (*options) { 200 1.1 christos case 'h': 201 1.1 christos case 'H': 202 1.1 christos usage(); 203 1.1 christos return Status::Message; 204 1.1 christos case 'V': 205 1.1 christos std::fprintf(stderr, "PZSTD version: %s.\n", ZSTD_VERSION_STRING); 206 1.1 christos return Status::Message; 207 1.1 christos case 'p': { 208 1.1 christos finished = true; 209 1.1 christos const char *optionArgument = getArgument(options, argv, i, argc); 210 1.1 christos if (optionArgument == nullptr) { 211 1.1 christos return Status::Failure; 212 1.1 christos } 213 1.1 christos if (*optionArgument < '0' || *optionArgument > '9') { 214 1.1 christos std::fprintf(stderr, "Option -p expects a number, but %s provided\n", 215 1.1 christos optionArgument); 216 1.1 christos return Status::Failure; 217 1.1 christos } 218 1.1 christos numThreads = parseUnsigned(&optionArgument); 219 1.1 christos if (*optionArgument != 0) { 220 1.1 christos std::fprintf(stderr, 221 1.1 christos "Option -p expects a number, but %u%s provided\n", 222 1.1 christos numThreads, optionArgument); 223 1.1 christos return Status::Failure; 224 1.1 christos } 225 1.1 christos break; 226 1.1 christos } 227 1.1 christos case 'o': { 228 1.1 christos finished = true; 229 1.1 christos const char *optionArgument = getArgument(options, argv, i, argc); 230 1.1 christos if (optionArgument == nullptr) { 231 1.1 christos return Status::Failure; 232 1.1 christos } 233 1.1 christos outputFile = optionArgument; 234 1.1 christos break; 235 1.1 christos } 236 1.1 christos case 'C': 237 1.1 christos checksum = true; 238 1.1 christos break; 239 1.1 christos case 'k': 240 1.1 christos keepSource = true; 241 1.1 christos break; 242 1.1 christos case 'd': 243 1.1 christos decompress = true; 244 1.1 christos break; 245 1.1 christos case 'f': 246 1.1 christos overwrite = true; 247 1.1 christos forceStdout = true; 248 1.1 christos followLinks = true; 249 1.1 christos break; 250 1.1 christos case 't': 251 1.1 christos test = true; 252 1.1 christos decompress = true; 253 1.1 christos break; 254 1.1 christos #ifdef UTIL_HAS_CREATEFILELIST 255 1.1 christos case 'r': 256 1.1 christos recursive = true; 257 1.1 christos break; 258 1.1 christos #endif 259 1.1 christos case 'c': 260 1.1 christos outputFile = kStdOut; 261 1.1 christos forceStdout = true; 262 1.1 christos break; 263 1.1 christos case 'v': 264 1.1 christos ++verbosity; 265 1.1 christos break; 266 1.1 christos case 'q': 267 1.1 christos --verbosity; 268 1.1 christos // Ignore them for now 269 1.1 christos break; 270 1.1 christos // Unsupported options from Zstd 271 1.1 christos case 'D': 272 1.1 christos case 's': 273 1.1 christos notSupported("Zstd dictionaries."); 274 1.1 christos return Status::Failure; 275 1.1 christos case 'b': 276 1.1 christos case 'e': 277 1.1 christos case 'i': 278 1.1 christos case 'B': 279 1.1 christos notSupported("Zstd benchmarking options."); 280 1.1 christos return Status::Failure; 281 1.1 christos default: 282 1.1 christos std::fprintf(stderr, "Invalid argument: %s\n", arg); 283 1.1 christos return Status::Failure; 284 1.1 christos } 285 1.1 christos if (!finished) { 286 1.1 christos ++options; 287 1.1 christos } 288 1.1 christos } // while (*options != 0); 289 1.1 christos } // for (int i = 1; i < argc; ++i); 290 1.1 christos 291 1.1 christos // Set options for test mode 292 1.1 christos if (test) { 293 1.1 christos outputFile = nullOutput; 294 1.1 christos keepSource = true; 295 1.1 christos } 296 1.1 christos 297 1.1 christos // Input file defaults to standard input if not provided. 298 1.1 christos if (localInputFiles.empty()) { 299 1.1 christos localInputFiles.emplace_back(kStdIn); 300 1.1 christos } 301 1.1 christos 302 1.1 christos // Check validity of input files 303 1.1 christos if (localInputFiles.size() > 1) { 304 1.1 christos const auto it = std::find(localInputFiles.begin(), localInputFiles.end(), 305 1.1 christos std::string{kStdIn}); 306 1.1 christos if (it != localInputFiles.end()) { 307 1.1 christos std::fprintf( 308 1.1 christos stderr, 309 1.1 christos "Cannot specify standard input when handling multiple files\n"); 310 1.1 christos return Status::Failure; 311 1.1 christos } 312 1.1 christos } 313 1.1 christos if (localInputFiles.size() > 1 || recursive) { 314 1.1 christos if (!outputFile.empty() && outputFile != nullOutput) { 315 1.1 christos std::fprintf( 316 1.1 christos stderr, 317 1.1 christos "Cannot specify an output file when handling multiple inputs\n"); 318 1.1 christos return Status::Failure; 319 1.1 christos } 320 1.1 christos } 321 1.1 christos 322 1.1 christos g_utilDisplayLevel = verbosity; 323 1.1 christos // Remove local input files that are symbolic links 324 1.1 christos if (!followLinks) { 325 1.1 christos std::remove_if(localInputFiles.begin(), localInputFiles.end(), 326 1.1 christos [&](const char *path) { 327 1.1 christos bool isLink = UTIL_isLink(path); 328 1.1 christos if (isLink && verbosity >= 2) { 329 1.1 christos std::fprintf( 330 1.1 christos stderr, 331 1.1 christos "Warning : %s is symbolic link, ignoring\n", 332 1.1 christos path); 333 1.1 christos } 334 1.1 christos return isLink; 335 1.1 christos }); 336 1.1 christos } 337 1.1 christos 338 1.1 christos // Translate input files/directories into files to (de)compress 339 1.1 christos if (recursive) { 340 1.1 christos FileNamesTable* const files = UTIL_createExpandedFNT(localInputFiles.data(), localInputFiles.size(), followLinks); 341 1.1 christos if (files == nullptr) { 342 1.1 christos std::fprintf(stderr, "Error traversing directories\n"); 343 1.1 christos return Status::Failure; 344 1.1 christos } 345 1.1 christos auto guard = 346 1.1 christos makeScopeGuard([&] { UTIL_freeFileNamesTable(files); }); 347 1.1 christos if (files->tableSize == 0) { 348 1.1 christos std::fprintf(stderr, "No files found\n"); 349 1.1 christos return Status::Failure; 350 1.1 christos } 351 1.1 christos inputFiles.resize(files->tableSize); 352 1.1 christos std::copy(files->fileNames, files->fileNames + files->tableSize, inputFiles.begin()); 353 1.1 christos } else { 354 1.1 christos inputFiles.resize(localInputFiles.size()); 355 1.1 christos std::copy(localInputFiles.begin(), localInputFiles.end(), 356 1.1 christos inputFiles.begin()); 357 1.1 christos } 358 1.1 christos localInputFiles.clear(); 359 1.1 christos assert(!inputFiles.empty()); 360 1.1 christos 361 1.1 christos // If reading from standard input, default to standard output 362 1.1 christos if (inputFiles[0] == kStdIn && outputFile.empty()) { 363 1.1 christos assert(inputFiles.size() == 1); 364 1.1 christos outputFile = "-"; 365 1.1 christos } 366 1.1 christos 367 1.1 christos if (inputFiles[0] == kStdIn && IS_CONSOLE(stdin)) { 368 1.1 christos assert(inputFiles.size() == 1); 369 1.1 christos std::fprintf(stderr, "Cannot read input from interactive console\n"); 370 1.1 christos return Status::Failure; 371 1.1 christos } 372 1.1 christos if (outputFile == "-" && IS_CONSOLE(stdout) && !(forceStdout && decompress)) { 373 1.1 christos std::fprintf(stderr, "Will not write to console stdout unless -c or -f is " 374 1.1 christos "specified and decompressing\n"); 375 1.1 christos return Status::Failure; 376 1.1 christos } 377 1.1 christos 378 1.1 christos // Check compression level 379 1.1 christos { 380 1.1 christos unsigned maxCLevel = 381 1.1 christos ultra ? ZSTD_maxCLevel() : kMaxNonUltraCompressionLevel; 382 1.1 christos if (compressionLevel > maxCLevel || compressionLevel == 0) { 383 1.1 christos std::fprintf(stderr, "Invalid compression level %u.\n", compressionLevel); 384 1.1 christos return Status::Failure; 385 1.1 christos } 386 1.1 christos } 387 1.1 christos 388 1.1 christos // Check that numThreads is set 389 1.1 christos if (numThreads == 0) { 390 1.1 christos std::fprintf(stderr, "Invalid arguments: # of threads not specified " 391 1.1 christos "and unable to determine hardware concurrency.\n"); 392 1.1 christos return Status::Failure; 393 1.1 christos } 394 1.1 christos 395 1.1 christos // Modify verbosity 396 1.1 christos // If we are piping input and output, turn off interaction 397 1.1 christos if (inputFiles[0] == kStdIn && outputFile == kStdOut && verbosity == 2) { 398 1.1 christos verbosity = 1; 399 1.1 christos } 400 1.1 christos // If we are in multi-file mode, turn off interaction 401 1.1 christos if (inputFiles.size() > 1 && verbosity == 2) { 402 1.1 christos verbosity = 1; 403 1.1 christos } 404 1.1 christos 405 1.1 christos return Status::Success; 406 1.1 christos } 407 1.1 christos 408 1.1 christos std::string Options::getOutputFile(const std::string &inputFile) const { 409 1.1 christos if (!outputFile.empty()) { 410 1.1 christos return outputFile; 411 1.1 christos } 412 1.1 christos // Attempt to add/remove zstd extension from the input file 413 1.1 christos if (decompress) { 414 1.1 christos int stemSize = inputFile.size() - kZstdExtension.size(); 415 1.1 christos if (stemSize > 0 && inputFile.substr(stemSize) == kZstdExtension) { 416 1.1 christos return inputFile.substr(0, stemSize); 417 1.1 christos } else { 418 1.1 christos return ""; 419 1.1 christos } 420 1.1 christos } else { 421 1.1 christos return inputFile + kZstdExtension; 422 1.1 christos } 423 1.1 christos } 424 1.1 christos } 425