diff --git a/Help/command/file.rst b/Help/command/file.rst index 42b087161e..d17057a6c5 100644 --- a/Help/command/file.rst +++ b/Help/command/file.rst @@ -1080,6 +1080,7 @@ Archiving [DESTINATION ] [ENCODING ] [PATTERNS ...] + [PATTERNS_EXCLUDE ...] [LIST_ONLY] [VERBOSE] [TOUCH]) @@ -1137,6 +1138,18 @@ Archiving patterns. Wildcards are supported. If the ``PATTERNS`` option is not given, the entire archive will be listed or extracted. + ``PATTERNS_EXCLUDE ...`` + .. versionadded:: 4.5 + + Do not extract or list files and directories that match one of the given + patterns. Wildcards are supported. This option may be combined with + ``PATTERNS``; an entry that matches both an inclusion pattern and an + exclusion pattern is excluded. + + Unlike ``PATTERNS``, exclusion patterns are not anchored to the start of + an entry's path: a pattern matches if it matches any portion of the path. + This is consistent with the ``--exclude`` option of command-line ``tar``. + ``LIST_ONLY`` List the files in the archive rather than extract them. diff --git a/Help/release/dev/file-ARCHIVE_EXTRACT-patterns-exclude.rst b/Help/release/dev/file-ARCHIVE_EXTRACT-patterns-exclude.rst new file mode 100644 index 0000000000..c661ceb24c --- /dev/null +++ b/Help/release/dev/file-ARCHIVE_EXTRACT-patterns-exclude.rst @@ -0,0 +1,6 @@ +file-ARCHIVE_EXTRACT-patterns-exclude +------------------------------------- + +* The :command:`file(ARCHIVE_EXTRACT)` command gained a ``PATTERNS_EXCLUDE`` + option to omit archive entries matching the given patterns from being + extracted or listed. diff --git a/Source/cmFileCommand.cxx b/Source/cmFileCommand.cxx index 366314c86a..222acd7f6f 100644 --- a/Source/cmFileCommand.cxx +++ b/Source/cmFileCommand.cxx @@ -3888,17 +3888,20 @@ bool HandleArchiveExtractCommand(std::vector const& args, bool ListOnly = false; std::string Destination; ArgumentParser::MaybeEmpty> Patterns; + ArgumentParser::MaybeEmpty> PatternsExclude; bool Touch = false; }; - static auto const parser = cmArgumentParser{} - .Bind("INPUT"_s, &Arguments::Input) - .Bind("ENCODING"_s, &Arguments::Encoding) - .Bind("VERBOSE"_s, &Arguments::Verbose) - .Bind("LIST_ONLY"_s, &Arguments::ListOnly) - .Bind("DESTINATION"_s, &Arguments::Destination) - .Bind("PATTERNS"_s, &Arguments::Patterns) - .Bind("TOUCH"_s, &Arguments::Touch); + static auto const parser = + cmArgumentParser{} + .Bind("INPUT"_s, &Arguments::Input) + .Bind("ENCODING"_s, &Arguments::Encoding) + .Bind("VERBOSE"_s, &Arguments::Verbose) + .Bind("LIST_ONLY"_s, &Arguments::ListOnly) + .Bind("DESTINATION"_s, &Arguments::Destination) + .Bind("PATTERNS"_s, &Arguments::Patterns) + .Bind("PATTERNS_EXCLUDE"_s, &Arguments::PatternsExclude) + .Bind("TOUCH"_s, &Arguments::Touch); std::vector unrecognizedArguments; auto parsedArgs = @@ -3928,6 +3931,7 @@ bool HandleArchiveExtractCommand(std::vector const& args, if (parsedArgs.ListOnly) { if (!cmSystemTools::ListTar(inFile, parsedArgs.Patterns, + parsedArgs.PatternsExclude, parsedArgs.Encoding, parsedArgs.Verbose)) { status.SetError(cmStrCat("failed to list: ", inFile)); cmSystemTools::SetFatalErrorOccurred(); @@ -3962,7 +3966,7 @@ bool HandleArchiveExtractCommand(std::vector const& args, } if (!cmSystemTools::ExtractTar( - inFile, parsedArgs.Patterns, + inFile, parsedArgs.Patterns, parsedArgs.PatternsExclude, parsedArgs.Touch ? cmSystemTools::cmTarExtractTimestamps::No : cmSystemTools::cmTarExtractTimestamps::Yes, parsedArgs.Encoding, parsedArgs.Verbose)) { diff --git a/Source/cmSystemTools.cxx b/Source/cmSystemTools.cxx index d9a77deade..7711dd4cf3 100644 --- a/Source/cmSystemTools.cxx +++ b/Source/cmSystemTools.cxx @@ -2505,6 +2505,7 @@ bool copy_data(struct archive* ar, struct archive* aw) bool extract_tar(std::string const& arFileName, std::vector const& files, + std::vector const& excludeFiles, std::string const& encoding, bool verbose, cmSystemTools::cmTarExtractTimestamps extractTimestamps, bool extract) @@ -2551,6 +2552,14 @@ bool extract_tar(std::string const& arFileName, } } + for (auto const& filename : excludeFiles) { + if (archive_match_exclude_pattern(matching, filename.c_str()) != + ARCHIVE_OK) { + cmSystemTools::Error("Failed to add to exclusion list: " + filename); + return false; + } + } + int r = cm_archive_read_open_filename(a, arFileName.c_str(), 10240); if (r) { ArchiveError("Problem with archive_read_open_filename(): ", a); @@ -2644,15 +2653,17 @@ bool extract_tar(std::string const& arFileName, bool cmSystemTools::ExtractTar(std::string const& arFileName, std::vector const& files, + std::vector const& excludeFiles, cmTarExtractTimestamps extractTimestamps, std::string const& encoding, bool verbose) { #if !defined(CMAKE_BOOTSTRAP) - return extract_tar(arFileName, files, encoding, verbose, extractTimestamps, - true); + return extract_tar(arFileName, files, excludeFiles, encoding, verbose, + extractTimestamps, true); #else (void)arFileName; (void)files; + (void)excludeFiles; (void)extractTimestamps; (void)encoding; (void)verbose; @@ -2662,14 +2673,16 @@ bool cmSystemTools::ExtractTar(std::string const& arFileName, bool cmSystemTools::ListTar(std::string const& arFileName, std::vector const& files, + std::vector const& excludeFiles, std::string const& encoding, bool verbose) { #if !defined(CMAKE_BOOTSTRAP) - return extract_tar(arFileName, files, encoding, verbose, + return extract_tar(arFileName, files, excludeFiles, encoding, verbose, cmTarExtractTimestamps::Yes, false); #else (void)arFileName; (void)files; + (void)excludeFiles; (void)encoding; (void)verbose; return false; diff --git a/Source/cmSystemTools.h b/Source/cmSystemTools.h index 17a6310bda..e72e5ea187 100644 --- a/Source/cmSystemTools.h +++ b/Source/cmSystemTools.h @@ -523,6 +523,7 @@ public: static bool ListTar(std::string const& arFileName, std::vector const& files, + std::vector const& excludeFiles, std::string const& encoding, bool verbose); static bool CreateTar(std::string const& arFileName, std::vector const& files, @@ -534,6 +535,7 @@ public: int compressionLevel = 0, int numThreads = 1); static bool ExtractTar(std::string const& arFileName, std::vector const& files, + std::vector const& excludeFiles, cmTarExtractTimestamps extractTimestamps, std::string const& encoding, bool verbose); diff --git a/Source/cmcmd.cxx b/Source/cmcmd.cxx index 6610da07c0..e942370ae4 100644 --- a/Source/cmcmd.cxx +++ b/Source/cmcmd.cxx @@ -2121,7 +2121,7 @@ int cmcmd::ExecuteCMakeCommand(std::vector const& args, } if (action == cmSystemTools::TarActionList) { - if (!cmSystemTools::ListTar(outFile, files, encoding, verbose)) { + if (!cmSystemTools::ListTar(outFile, files, {}, encoding, verbose)) { cmSystemTools::Error("Problem listing tar: " + outFile); return 1; } @@ -2136,7 +2136,7 @@ int cmcmd::ExecuteCMakeCommand(std::vector const& args, return 1; } } else if (action == cmSystemTools::TarActionExtract) { - if (!cmSystemTools::ExtractTar(outFile, files, extractTimestamps, + if (!cmSystemTools::ExtractTar(outFile, files, {}, extractTimestamps, encoding, verbose)) { cmSystemTools::Error( cmStrCat("Problem extracting tar:\n ", outFile)); diff --git a/Tests/Fuzzing/cmArchiveExtractFuzzer.cxx b/Tests/Fuzzing/cmArchiveExtractFuzzer.cxx index 4fb2b73757..ab72a9d423 100644 --- a/Tests/Fuzzing/cmArchiveExtractFuzzer.cxx +++ b/Tests/Fuzzing/cmArchiveExtractFuzzer.cxx @@ -86,9 +86,10 @@ extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) std::vector files; // Extract without verbose, with timestamps + std::vector excludeFiles; bool result1 = cmSystemTools::ExtractTar( - archiveFile, files, cmSystemTools::cmTarExtractTimestamps::Yes, "UTF-8", - false); + archiveFile, files, excludeFiles, + cmSystemTools::cmTarExtractTimestamps::Yes, "UTF-8", false); (void)result1; // Restore directory BEFORE removing (can't remove cwd) @@ -103,8 +104,8 @@ extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) // Extract with verbose, without timestamps files.clear(); bool result2 = cmSystemTools::ExtractTar( - archiveFile, files, cmSystemTools::cmTarExtractTimestamps::No, "UTF-8", - true); + archiveFile, files, excludeFiles, + cmSystemTools::cmTarExtractTimestamps::No, "UTF-8", true); (void)result2; // Restore directory diff --git a/Tests/RunCMake/File_Archive/RunCMakeTest.cmake b/Tests/RunCMake/File_Archive/RunCMakeTest.cmake index 63071cb7e4..6b4da44b08 100644 --- a/Tests/RunCMake/File_Archive/RunCMakeTest.cmake +++ b/Tests/RunCMake/File_Archive/RunCMakeTest.cmake @@ -68,6 +68,10 @@ endif() # Extracting only selected files or directories run_cmake(zip-filtered) +# Excluding selected files or directories from extraction +run_cmake(zip-filtered-exclude) +run_cmake(zip-filtered-exclude-precedence) + run_cmake(create-missing-args) run_cmake(extract-missing-args) diff --git a/Tests/RunCMake/File_Archive/zip-filtered-exclude-precedence.cmake b/Tests/RunCMake/File_Archive/zip-filtered-exclude-precedence.cmake new file mode 100644 index 0000000000..5930e77d5e --- /dev/null +++ b/Tests/RunCMake/File_Archive/zip-filtered-exclude-precedence.cmake @@ -0,0 +1,28 @@ +set(OUTPUT_NAME "test.zip") + +set(ARCHIVE_FORMAT zip) + +# Combine inclusion and exclusion patterns. When an entry matches both, +# the exclusion takes precedence. +set(DECOMPRESSION_OPTIONS + PATTERNS + "compress_dir/d1/*" # include contents of d1 + "compress_dir/d_4/*" # include contents of d_4 ... + PATTERNS_EXCLUDE + "d_4" # ... but exclude d_4: exclusion wins +) + +# Only the included-and-not-excluded entry survives. +set(CUSTOM_CHECK_FILES + "d1/f1.txt" +) + +set(NOT_EXISTING_FILES_CHECK + "f1.txt" # never included by PATTERNS + "d 2/f1.txt" # never included by PATTERNS + "d_4/f1.txt" # included by PATTERNS but excluded: exclusion wins +) + +include(${CMAKE_CURRENT_LIST_DIR}/roundtrip.cmake) + +check_magic("504b0304" LIMIT 4 HEX) diff --git a/Tests/RunCMake/File_Archive/zip-filtered-exclude.cmake b/Tests/RunCMake/File_Archive/zip-filtered-exclude.cmake new file mode 100644 index 0000000000..866236a677 --- /dev/null +++ b/Tests/RunCMake/File_Archive/zip-filtered-exclude.cmake @@ -0,0 +1,30 @@ +set(OUTPUT_NAME "test.zip") + +set(ARCHIVE_FORMAT zip) + +# Extract the whole archive except entries matching PATTERNS_EXCLUDE. +set(DECOMPRESSION_OPTIONS + PATTERNS_EXCLUDE + "compress_dir/d 2/*" # exclude everything under a directory + "d-4" # exclude by unanchored name (dir + contents) + "no_such_entry" # matches nothing: must not be an error +) + +# Everything that was not excluded must still be extracted. +set(CUSTOM_CHECK_FILES + "f1.txt" + "d1/f1.txt" + "d + 3/f1.txt" + "d_4/f1.txt" # underscore, not the excluded "d-4" + "My Special Directory/f1.txt" +) + +# The excluded entries must not be extracted. +set(NOT_EXISTING_FILES_CHECK + "d 2/f1.txt" + "d-4/f1.txt" +) + +include(${CMAKE_CURRENT_LIST_DIR}/roundtrip.cmake) + +check_magic("504b0304" LIMIT 4 HEX)