Skip to content

Commit 7d267a3

Browse files
committed
[main] rootcp: error out when trying to copy inexisting objects
1 parent 34aa4ba commit 7d267a3

File tree

4 files changed

+66
-13
lines changed

4 files changed

+66
-13
lines changed

main/src/RootObjTree.cxx

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ ROOT::CmdLine::GetMatchingPathsInFile(std::string_view fileName, std::string_vie
3535
}
3636

3737
const auto patternSplits = pattern.empty() ? std::vector<std::string>{} : ROOT::Split(pattern, "/");
38+
std::vector<bool> patternWasMatchedAtLeastOnce(patternSplits.size());
3839

39-
// Match all objects at all nesting levels down to the deepest nesting level of `pattern` (or all nesting levels
40-
// if we have the "recursive listing" flag). The nodes are visited breadth-first.
40+
/// Match all objects at all nesting levels down to the deepest nesting level of `pattern` (or all nesting levels
41+
/// if we have the "recursive listing" flag). The nodes are visited breadth-first.
42+
43+
// Initialize the nodeTree with the root node and mark it as the first node to visit.
4144
{
4245
ROOT::CmdLine::RootObjNode rootNode = {};
4346
rootNode.fName = std::string(fileName);
@@ -57,7 +60,7 @@ ROOT::CmdLine::GetMatchingPathsInFile(std::string_view fileName, std::string_vie
5760
ROOT::CmdLine::RootObjNode *cur = &nodeTree.fNodes[curIdx];
5861
assert(cur->fDir);
5962

60-
// Sort the keys by name
63+
// Gather all keys under this directory and sort them by name.
6164
std::vector<TKey *> keys;
6265
keys.reserve(cur->fDir->GetListOfKeys()->GetEntries());
6366
for (TKey *key : ROOT::Detail::TRangeStaticCast<TKey>(cur->fDir->GetListOfKeys()))
@@ -68,10 +71,19 @@ ROOT::CmdLine::GetMatchingPathsInFile(std::string_view fileName, std::string_vie
6871

6972
namesFound.clear();
7073

74+
// Iterate the keys and find matches
7175
for (TKey *key : keys) {
72-
// Don't recurse lower than requested by `pattern` unless we explicitly have the `recursive listing` flag.
73-
if (cur->fNesting < patternSplits.size() && !MatchesGlob(key->GetName(), patternSplits[cur->fNesting]))
74-
continue;
76+
// NOTE: cur->fNesting can only be >= patternSplits.size() if we have `isRecursive == true` (see the code near
77+
// the end of the outer do/while loop).
78+
// In that case we don't care about matching patterns anymore because we are already beyond the nesting level
79+
// where pattern filtering applies.
80+
// In all other cases, we check if the key name matches the pattern and skip it if it doesn't.
81+
if (cur->fNesting < patternSplits.size()) {
82+
if (MatchesGlob(key->GetName(), patternSplits[cur->fNesting]))
83+
patternWasMatchedAtLeastOnce[cur->fNesting] = true;
84+
else
85+
continue;
86+
}
7587

7688
if (namesFound.count(key->GetName()) > 0) {
7789
std::cerr << "WARNING: Several versions of '" << key->GetName() << "' are present in '" << fileName
@@ -94,7 +106,8 @@ ROOT::CmdLine::GetMatchingPathsInFile(std::string_view fileName, std::string_vie
94106
newChild.fDir = cur->fDir->GetDirectory(key->GetName());
95107
}
96108

97-
// Only recurse into subdirectories that are up to the deepest level we ask for through `pattern`.
109+
// Only recurse into subdirectories that are up to the deepest level we ask for through `pattern` (except in
110+
// case of recursive listing).
98111
if (cur->fNesting < patternSplits.size() || isRecursive) {
99112
for (auto childIdx = cur->fFirstChild; childIdx < cur->fFirstChild + cur->fNChildren; ++childIdx) {
100113
auto &child = nodeTree.fNodes[childIdx];
@@ -112,6 +125,19 @@ ROOT::CmdLine::GetMatchingPathsInFile(std::string_view fileName, std::string_vie
112125
}
113126
} while (!nodesToVisit.empty());
114127

128+
if (!(flags & kIgnoreFailedMatches)) {
129+
for (auto i = 0u; i < patternSplits.size(); ++i) {
130+
// We don't append errors for '*' because its semantics imply "0 or more matches", so 0 matches is a valid
131+
// case. For any other pattern we expect at least 1 match.
132+
if (!patternWasMatchedAtLeastOnce[i] && !patternSplits[i].empty() && patternSplits[i] != "*") {
133+
std::string err = "'" + std::string(fileName) + ":" +
134+
ROOT::Join("/", std::span<const std::string>{patternSplits.data(), i + 1}) +
135+
"' matches no objects.";
136+
source.fErrors.push_back(err);
137+
}
138+
}
139+
}
140+
115141
return source;
116142
}
117143

main/src/RootObjTree.hxx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,12 @@ struct RootSource {
103103
enum EGetMatchingPathsFlags {
104104
/// Recurse into subdirectories when matching objects
105105
kRecursive = 1 << 0,
106+
kIgnoreFailedMatches = 1 << 1,
106107
};
107108

108109
/// Given a file and a "path pattern", returns a RootSource containing the tree of matched objects.
110+
/// If `pattern` is non-empty, not a single '*' and it doesn't match any object in `fileName`,
111+
/// an error will be appended to RootSource::fErrors unless `kIgnoreFailedMatches` is part of `flags`.
109112
///
110113
/// \param fileName The name of the ROOT file to look into
111114
/// \param pattern A glob-like pattern (basically a `ls` pattern). May be empty to match anything.

main/src/rootcp.cxx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -323,16 +323,34 @@ int main(int argc, char **argv)
323323
// a valid location.
324324
// First validate the destination syntax.
325325
const auto destFnameAndPattern = args.fSources.back();
326+
args.fSources.pop_back();
326327
auto splitRes = SplitIntoFileNameAndPattern(destFnameAndPattern);
327328
if (!splitRes) {
328329
Err() << splitRes.GetError()->GetReport() << "\n";
329330
return 1;
330331
}
331332
auto [destFname, destPath] = splitRes.Unwrap();
332333

334+
// Validate and split all input sources into filename + pattern
335+
std::vector<std::pair<std::string_view, std::string_view>> sourcesFileAndPattern;
336+
sourcesFileAndPattern.reserve(args.fSources.size());
337+
bool srcIsSameAsDstFile = false;
338+
for (const auto &src : args.fSources) {
339+
auto res = SplitIntoFileNameAndPattern(src);
340+
if (!res) {
341+
Err() << res.GetError()->GetReport() << "\n";
342+
return 1;
343+
}
344+
auto fNameAndPattern = res.Unwrap();
345+
if (fNameAndPattern.first == destFname) {
346+
srcIsSameAsDstFile = true;
347+
}
348+
sourcesFileAndPattern.push_back(fNameAndPattern);
349+
}
350+
333351
// Check if the operation is allowed.
334-
args.fSources.pop_back();
335-
if (args.fRecreate && std::find(args.fSources.begin(), args.fSources.end(), destFname) != args.fSources.end()) {
352+
353+
if (args.fRecreate && srcIsSameAsDstFile) {
336354
Err() << "cannot recreate destination file if this is also a source file\n";
337355
return 1;
338356
}
@@ -382,8 +400,8 @@ int main(int argc, char **argv)
382400

383401
const std::uint32_t flags = args.fRecursive * EGetMatchingPathsFlags::kRecursive;
384402
bool errors = false;
385-
for (const auto &srcFname : args.fSources) {
386-
auto src = ROOT::CmdLine::ParseRootSource(srcFname, flags);
403+
for (const auto &[srcFname, srcPattern] : sourcesFileAndPattern) {
404+
auto src = ROOT::CmdLine::GetMatchingPathsInFile(srcFname, srcPattern, flags);
387405
if (!src.fErrors.empty()) {
388406
for (const auto &err : src.fErrors)
389407
Err() << err << "\n";
@@ -425,8 +443,8 @@ int main(int argc, char **argv)
425443
}
426444
}
427445

428-
if (errors) {
429-
// Make sure we don't end up with a half-copied file in case of errors.
446+
if (errors && !srcIsSameAsDstFile) {
447+
// If the destination file was fresh, make sure we don't end up with a half-copied file in case of errors.
430448
gSystem->Unlink(std::string(destFname).c_str());
431449
}
432450

roottest/main/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,12 @@ ROOTTEST_ADD_TEST(RootcpReplaceRecursiveCheckOutput
399399
ROOTTEST_ADD_TEST(RootcpReplaceRecursiveClean
400400
COMMAND ${PY_TOOLS_PREFIX}/rootrm${pyext} -r copy_replace_recursive.root
401401
FIXTURES_REQUIRED main-RootcpReplaceRecursiveCheckOutput-fixture)
402+
403+
404+
ROOTTEST_ADD_TEST(SimpleRootcpInexistent
405+
COMMAND ${TOOLS_PREFIX}/rootcp${exeext} test.root:does_not_exist test.root:
406+
PASSREGEX "Error in <rootcp>: 'test.root:does_not_exist' matches no objects.")
407+
402408
#########################################################################
403409

404410
############################# ROOMV TESTS ############################

0 commit comments

Comments
 (0)