GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/sync_union_tarball.cc
Date: 2026-08-30 02:40:36
Exec Total Coverage
Lines: 118 196 60.2%
Branches: 106 331 32.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System
3 */
4
5 #include "sync_union_tarball.h"
6
7 #include <pthread.h>
8 #include <unistd.h>
9
10 #include <cassert>
11 #include <cstdio>
12 #include <list>
13 #include <set>
14 #include <string>
15 #include <vector>
16
17 #include "duplex_libarchive.h"
18 #include "sync_item.h"
19 #include "sync_item_dummy.h"
20 #include "sync_item_tar.h"
21 #include "sync_mediator.h"
22 #include "sync_union.h"
23 #include "util/concurrency.h"
24 #include "util/exception.h"
25 #include "util/fs_traversal.h"
26 #include "util/posix.h"
27 #include "util/smalloc.h"
28
29 namespace publish {
30
31 174 SyncUnionTarball::SyncUnionTarball(AbstractSyncMediator *mediator,
32 const std::string &rdonly_path,
33 const std::string &tarball_path,
34 const std::string &base_directory,
35 const uid_t uid,
36 const gid_t gid,
37 const std::string &to_delete,
38 const bool create_catalog_on_root,
39 const bool fast_delete,
40 const std::string &path_delimiter,
41 174 const bool tolerate_missing_hardlinks)
42 : SyncUnion(mediator, rdonly_path, "", "")
43 174 , src(NULL)
44 174 , tarball_path_(tarball_path)
45
1/2
✓ Branch 1 taken 174 times.
✗ Branch 2 not taken.
174 , base_directory_(base_directory)
46 174 , uid_(uid)
47 174 , gid_(gid)
48
1/2
✓ Branch 1 taken 174 times.
✗ Branch 2 not taken.
174 , to_delete_(to_delete)
49 174 , create_catalog_on_root_(create_catalog_on_root)
50 174 , fast_delete_(fast_delete)
51
1/2
✓ Branch 1 taken 174 times.
✗ Branch 2 not taken.
174 , path_delimiter_(path_delimiter)
52 174 , tolerate_missing_hardlinks_(tolerate_missing_hardlinks)
53
6/12
✓ Branch 2 taken 174 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 174 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 174 times.
✗ Branch 10 not taken.
✓ Branch 16 taken 174 times.
✗ Branch 17 not taken.
✓ Branch 23 taken 174 times.
✗ Branch 24 not taken.
✓ Branch 26 taken 174 times.
✗ Branch 27 not taken.
348 , read_archive_signal_(new Signal) { }
54
55
1/2
✓ Branch 0 taken 174 times.
✗ Branch 1 not taken.
348 SyncUnionTarball::~SyncUnionTarball() { delete read_archive_signal_; }
56
57 174 bool SyncUnionTarball::Initialize() {
58 int result;
59
60 // We are just deleting entity from the repo
61
2/2
✓ Branch 1 taken 87 times.
✓ Branch 2 taken 87 times.
174 if (tarball_path_ == "") {
62
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 87 times.
87 assert(NULL == src);
63 87 return SyncUnion::Initialize();
64 }
65
66 87 src = archive_read_new();
67
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 87 times.
87 if (src == NULL) {
68 LogCvmfs(kLogUnionFs, kLogStderr,
69 "Impossible to create an archive reader");
70 return false;
71 }
72
73 // Registering the tar/empty formats on a freshly allocated reader can only
74 // fail due to an internal libarchive/allocation error, never because of
75 // anything in the tarball itself, so treat failure as a bug, not a runtime
76 // condition to handle gracefully.
77 87 result = archive_read_support_format_tar(src);
78
1/2
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
87 if (result == ARCHIVE_OK)
79 87 result = archive_read_support_format_empty(src);
80
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 87 times.
87 if (result != ARCHIVE_OK) {
81 PANIC(kLogStderr | kLogSyslogErr,
82 "Impossible to configure the archive reader: %s",
83 archive_error_string(src));
84 }
85
86
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 87 times.
87 if (tarball_path_ == "-") {
87 result = archive_read_open_filename(src, NULL, kBlockSize);
88 } else {
89
1/2
✓ Branch 1 taken 87 times.
✗ Branch 2 not taken.
87 const std::string tarball_absolute_path = GetAbsolutePath(tarball_path_);
90
1/2
✓ Branch 2 taken 87 times.
✗ Branch 3 not taken.
87 result = archive_read_open_filename(src, tarball_absolute_path.c_str(),
91 kBlockSize);
92 87 }
93
94
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 87 times.
87 if (result != ARCHIVE_OK) {
95 LogCvmfs(kLogUnionFs, kLogStderr, "Impossible to open the archive: %s",
96 archive_error_string(src));
97 archive_read_free(src);
98 src = NULL;
99 return false;
100 }
101
102 87 return SyncUnion::Initialize();
103 }
104
105 /*
106 * Libarchive is not thread aware, so we need to make sure that before
107 * to read/"open" the next header in the archive the content of the
108 *
109 * present header is been consumed completely.
110 * Different thread read/"open" the header from the one that consumes
111 * it so we opted for a Signal that is backed by a conditional variable.
112 * We wait for the signal just before to read the header.
113 * Then when we have done with the header the Signal is fired.
114 * The Signal can be fired inside the main loop if we don't need to read
115 * data, or when the IngestionSource get closed, which means that we are
116 * not reading data anymore from there.
117 * This whole process is not necessary for directories since we don't
118 * actually need to read data from them.
119 *
120 * It may be needed to add a catalog as a root of the archive.
121 * A possible way to do it is by creating an virtual `.cvmfscatalog` file and
122 * push it into the usual pipeline.
123 * This operation must be done only once, and it seems like a good idea to do
124 * it at the first iteration of the loop, hence this logic is managed by the
125 * `first_iteration` boolean flag.
126 */
127 174 void SyncUnionTarball::Traverse() {
128
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 174 times.
174 assert(this->IsInitialized());
129
130 /*
131 * As first step we eliminate the requested directories.
132 */
133
2/2
✓ Branch 1 taken 87 times.
✓ Branch 2 taken 87 times.
174 if (to_delete_ != "") {
134 vector<std::string> to_eliminate_vec = SplitStringMultiChar(
135
1/2
✓ Branch 1 taken 87 times.
✗ Branch 2 not taken.
87 to_delete_, path_delimiter_);
136
137 87 for (vector<string>::iterator s = to_eliminate_vec.begin();
138
2/2
✓ Branch 2 taken 116 times.
✓ Branch 3 taken 87 times.
203 s != to_eliminate_vec.end();
139 116 ++s) {
140 116 std::string parent_path;
141 116 std::string filename;
142
1/2
✓ Branch 2 taken 116 times.
✗ Branch 3 not taken.
116 SplitPath(*s, &parent_path, &filename);
143
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 116 times.
116 if (parent_path == ".")
144 parent_path = "";
145 const SharedPtr<SyncItem> sync_entry = CreateSyncItem(parent_path,
146
1/2
✓ Branch 1 taken 116 times.
✗ Branch 2 not taken.
116 filename, kItemDir);
147
1/2
✓ Branch 2 taken 116 times.
✗ Branch 3 not taken.
116 mediator_->Remove(sync_entry, fast_delete_);
148 116 }
149 87 }
150
151 // we are simply deleting entity from the repo
152
2/2
✓ Branch 0 taken 87 times.
✓ Branch 1 taken 87 times.
174 if (NULL == src)
153 87 return;
154
155 // Prime the signal so the first Wait() in the loop below can proceed.
156 87 read_archive_signal_->Wakeup();
157
158 87 struct archive_entry *entry = archive_entry_new();
159 while (true) {
160 // Get the lock, wait if lock is not available yet
161 493 read_archive_signal_->Wait();
162
163 493 const int result = archive_read_next_header2(src, entry);
164
165
2/6
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✓ Branch 2 taken 87 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 406 times.
✗ Branch 5 not taken.
493 switch (result) {
166 case ARCHIVE_FATAL: {
167 PANIC(kLogStderr, "Fatal error in reading the archive.\n%s\n",
168 archive_error_string(src));
169 break; // Only exit point with error
170 }
171
172 case ARCHIVE_RETRY: {
173 LogCvmfs(kLogUnionFs, kLogStdout,
174 "Error in reading the header, retrying.\n%s\n",
175 archive_error_string(src));
176 continue;
177 break;
178 }
179
180 87 case ARCHIVE_EOF: {
181
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 87 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 87 times.
87 if (create_catalog_on_root_ && (base_directory_ != "/")) {
182 CreateDirectories(base_directory_); // necessary for empty archives
183 const SharedPtr<SyncItem> catalog = SharedPtr<SyncItem>(
184 new SyncItemDummyCatalog(base_directory_, this));
185 ProcessFile(catalog);
186 to_create_catalog_dirs_.insert(base_directory_);
187 }
188 87 for (set<string>::iterator dir = to_create_catalog_dirs_.begin();
189
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 87 times.
87 dir != to_create_catalog_dirs_.end();
190 ++dir) {
191 assert(dirs_.find(*dir) != dirs_.end());
192 const SharedPtr<SyncItem> to_mark = dirs_[*dir];
193 assert(to_mark->IsDirectory());
194 to_mark->SetCatalogMarker();
195 to_mark->MakePlaceholderDirectory();
196 ProcessDirectory(to_mark);
197 }
198 87 return; // Only successful exit point
199 break;
200 }
201
202 case ARCHIVE_WARN: {
203 LogCvmfs(kLogUnionFs, kLogStderr,
204 "Warning in uncompression reading, going on.\n %s",
205 archive_error_string(src));
206 // We actually want this to enter the ARCHIVE_OK case
207 }
208
209 406 case ARCHIVE_OK: {
210 406 ProcessArchiveEntry(entry);
211 406 break;
212 }
213
214 default: {
215 // We should never enter in this branch, but just for safeness we prefer
216 // to abort in case we hit a case we don't how to manage.
217 PANIC(kLogStderr, "Enter in unknown state. Aborting.\nError: %s\n",
218 result, archive_error_string(src));
219 }
220 }
221 406 }
222 }
223
224 406 void SyncUnionTarball::ProcessArchiveEntry(struct archive_entry *entry) {
225
2/4
✓ Branch 2 taken 406 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 406 times.
✗ Branch 6 not taken.
406 std::string archive_file_path(archive_entry_pathname(entry));
226
1/2
✓ Branch 1 taken 406 times.
✗ Branch 2 not taken.
406 archive_file_path = SanitizePath(archive_file_path);
227
228 406 const std::string complete_path = base_directory_ != "/"
229 406 ? MakeCanonicalPath(base_directory_
230
1/4
✓ Branch 1 taken 406 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
406 + "/"
231
3/8
✓ Branch 1 taken 406 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 406 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 406 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
1218 + archive_file_path)
232
2/6
✓ Branch 0 taken 406 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 406 times.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
812 : MakeCanonicalPath(archive_file_path);
233
234 406 std::string parent_path;
235 406 std::string filename;
236
1/2
✓ Branch 1 taken 406 times.
✗ Branch 2 not taken.
406 SplitPath(complete_path, &parent_path, &filename);
237
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 406 times.
406 if (parent_path == ".")
238 parent_path.clear();
239
240
1/2
✓ Branch 1 taken 406 times.
✗ Branch 2 not taken.
406 CreateDirectories(parent_path);
241
242 const SharedPtr<SyncItem> sync_entry = SharedPtr<SyncItem>(
243 new SyncItemTar(parent_path, filename, src, entry, read_archive_signal_,
244
3/6
✓ Branch 1 taken 406 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 406 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 406 times.
✗ Branch 8 not taken.
406 this, uid_, gid_));
245
246
2/4
✓ Branch 1 taken 406 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 406 times.
406 if (NULL != archive_entry_hardlink(entry)) {
247 const std::string hardlink_name(
248 SanitizePath(archive_entry_hardlink(entry)));
249 const std::string hardlink = base_directory_ != "/"
250 ? base_directory_ + "/" + hardlink_name
251 : hardlink_name;
252
253 // Capture the link's own ownership/permissions so that, if the target is
254 // not part of this archive, an empty file can be materialized in its place
255 // (see PostUpload and tolerate_missing_hardlinks_). Read directly from the
256 // tar header, applying the same uid/gid override as SyncItemTar.
257 const struct stat *link_stat = archive_entry_stat(entry);
258 const uid_t link_uid = (uid_ != -1u) ? uid_ : link_stat->st_uid;
259 const gid_t link_gid = (gid_ != -1u) ? gid_ : link_stat->st_gid;
260 const HardlinkDestination destination(complete_path, link_stat->st_mode,
261 link_uid, link_gid,
262 link_stat->st_mtime);
263 if (hardlinks_.find(hardlink) != hardlinks_.end()) {
264 hardlinks_.find(hardlink)->second.push_back(destination);
265 } else {
266 std::list<HardlinkDestination> to_hardlink;
267 to_hardlink.push_back(destination);
268 hardlinks_[hardlink] = to_hardlink;
269 }
270 if (filename == ".cvmfscatalog") {
271 // the file is created in the PostUpload phase
272 to_create_catalog_dirs_.insert(parent_path);
273 }
274 read_archive_signal_->Wakeup();
275 return;
276 }
277
278
3/4
✓ Branch 2 taken 406 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 116 times.
✓ Branch 5 taken 290 times.
406 if (sync_entry->IsDirectory()) {
279
3/5
✓ Branch 2 taken 116 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 58 times.
✓ Branch 6 taken 58 times.
116 if (know_directories_.find(complete_path) != know_directories_.end()) {
280
1/2
✓ Branch 2 taken 58 times.
✗ Branch 3 not taken.
58 sync_entry->MakePlaceholderDirectory();
281 }
282
1/2
✓ Branch 2 taken 116 times.
✗ Branch 3 not taken.
116 ProcessUnmaterializedDirectory(sync_entry);
283
2/4
✓ Branch 1 taken 116 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 116 times.
✗ Branch 5 not taken.
116 dirs_[complete_path] = sync_entry;
284
1/2
✓ Branch 1 taken 116 times.
✗ Branch 2 not taken.
116 know_directories_.insert(complete_path);
285
286
1/2
✓ Branch 1 taken 116 times.
✗ Branch 2 not taken.
116 read_archive_signal_->Wakeup(); // We don't need to read data and we
287 // can read the next header
288
289
3/4
✓ Branch 2 taken 290 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 261 times.
✓ Branch 5 taken 29 times.
290 } else if (sync_entry->IsRegularFile()) {
290 // inside the process pipeline we will wake up the signal
291
1/2
✓ Branch 2 taken 261 times.
✗ Branch 3 not taken.
261 ProcessFile(sync_entry);
292
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 261 times.
261 if (filename == ".cvmfscatalog") {
293 to_create_catalog_dirs_.insert(parent_path);
294 }
295
296
1/8
✓ Branch 2 taken 29 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
29 } else if (sync_entry->IsSymlink() || sync_entry->IsFifo()
297 || sync_entry->IsSocket() || sync_entry->IsCharacterDevice()
298
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 29 times.
✗ Branch 9 not taken.
29 || sync_entry->IsBlockDevice()) {
299 // we avoid to add an entity called as a catalog marker if it is not a
300 // regular file
301
1/2
✓ Branch 1 taken 29 times.
✗ Branch 2 not taken.
29 if (filename != ".cvmfscatalog") {
302
1/2
✓ Branch 2 taken 29 times.
✗ Branch 3 not taken.
29 ProcessFile(sync_entry);
303 } else {
304 PANIC(kLogStderr,
305 "Found entity called as a catalog marker '%s' that however is "
306 "not a regular file, abort",
307 complete_path.c_str());
308 }
309
310 // here we don't need to read data from the tar file so we can wake up
311 // immediately the signal
312
1/2
✓ Branch 1 taken 29 times.
✗ Branch 2 not taken.
29 read_archive_signal_->Wakeup();
313
314 } else {
315 PANIC(kLogStderr, "Fatal error found unexpected file: \n%s\n",
316 filename.c_str());
317 // if for any reason this code path change and we don't abort anymore,
318 // remember to wakeup the signal, otherwise we will be stuck in a deadlock
319 //
320 // read_archive_signal_->Wakeup();
321 }
322
5/10
✓ Branch 1 taken 406 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 406 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 406 times.
✗ Branch 8 not taken.
✓ Branch 10 taken 406 times.
✗ Branch 11 not taken.
✓ Branch 13 taken 406 times.
✗ Branch 14 not taken.
406 }
323
324 406 std::string SyncUnionTarball::SanitizePath(const std::string &path) {
325
1/2
✓ Branch 1 taken 406 times.
✗ Branch 2 not taken.
406 if (path.length() >= 2) {
326
2/6
✗ Branch 1 not taken.
✓ Branch 2 taken 406 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 406 times.
406 if (path[0] == '.' && path[1] == '/') {
327 return path.substr(2);
328 }
329 }
330
1/2
✓ Branch 1 taken 406 times.
✗ Branch 2 not taken.
406 if (path.length() >= 1) {
331
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 406 times.
406 if (path[0] == '/') {
332 return path.substr(1);
333 }
334 }
335 406 return path;
336 }
337
338 void SyncUnionTarball::PostUpload() {
339 // When tolerating missing hardlink targets we ask Clone not to abort on a
340 // missing source; it returns false instead and we materialize an empty file.
341 const bool fail_if_target_missing = !tolerate_missing_hardlinks_;
342 std::map<const std::string, std::list<HardlinkDestination> >::iterator
343 hardlink;
344 for (hardlink = hardlinks_.begin(); hardlink != hardlinks_.end();
345 ++hardlink) {
346 const std::string &target = hardlink->first;
347 std::list<HardlinkDestination>::iterator entry;
348 for (entry = hardlink->second.begin(); entry != hardlink->second.end();
349 ++entry) {
350 const bool cloned = mediator_->Clone(entry->path, target,
351 fail_if_target_missing);
352 if (cloned)
353 continue;
354
355 // The hardlink target is not part of this archive (e.g. a cross-layer
356 // hardlink in an OCI image layer). Materialize an empty regular file at
357 // the link's path, preserving its ownership and permissions, and push it
358 // through the normal ingestion pipeline so the empty object is stored.
359 std::string parent_path;
360 std::string filename;
361 SplitPath(entry->path, &parent_path, &filename);
362 if (parent_path == ".")
363 parent_path.clear();
364 const SharedPtr<SyncItem> empty_file = SharedPtr<SyncItem>(
365 new SyncItemDummyFile(parent_path, filename, this, entry->mode,
366 entry->uid, entry->gid, entry->mtime));
367 ProcessFile(empty_file);
368 LogCvmfs(kLogUnionFs, kLogStderr | kLogSyslogWarn,
369 "hardlink target '%s' is not present in the tarball; "
370 "materialized an empty file at '%s'",
371 target.c_str(), entry->path.c_str());
372 }
373 }
374 }
375
376 std::string SyncUnionTarball::UnwindWhiteoutFilename(
377 SharedPtr<SyncItem> entry) const {
378 return entry->filename();
379 }
380
381 116 bool SyncUnionTarball::IsOpaqueDirectory(SharedPtr<SyncItem> directory) const {
382 116 return false;
383 }
384
385 116 bool SyncUnionTarball::IsWhiteoutEntry(SharedPtr<SyncItem> entry) const {
386 116 return false;
387 }
388
389 /* Tar files are not necessarily traversed in order from root to leave.
390 * So it may happens that we are expanding the file `/a/b/c.txt` without
391 * having created yet the directory `/a/b/`.
392 * In order to overcome this limitation the following function create dummy
393 * directories that can be used as placeholder and that they will be overwritten
394 * as soon as the real directory is found in the tarball
395 */
396 551 void SyncUnionTarball::CreateDirectories(const std::string &target) {
397
3/5
✓ Branch 2 taken 551 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 319 times.
✓ Branch 6 taken 232 times.
551 if (know_directories_.find(target) != know_directories_.end())
398 406 return;
399
2/2
✓ Branch 1 taken 87 times.
✓ Branch 2 taken 145 times.
232 if (target == ".")
400 87 return;
401
402
1/2
✓ Branch 2 taken 145 times.
✗ Branch 3 not taken.
145 std::string dirname = "";
403
1/2
✓ Branch 2 taken 145 times.
✗ Branch 3 not taken.
145 std::string filename = "";
404
1/2
✓ Branch 1 taken 145 times.
✗ Branch 2 not taken.
145 SplitPath(target, &dirname, &filename);
405
1/2
✓ Branch 1 taken 145 times.
✗ Branch 2 not taken.
145 CreateDirectories(dirname);
406
407
2/2
✓ Branch 1 taken 87 times.
✓ Branch 2 taken 58 times.
145 if (dirname == ".")
408
1/2
✓ Branch 1 taken 87 times.
✗ Branch 2 not taken.
87 dirname = "";
409 const SharedPtr<SyncItem> dummy = SharedPtr<SyncItem>(
410
3/6
✓ Branch 1 taken 145 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 145 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 145 times.
✗ Branch 8 not taken.
145 new SyncItemDummyDir(dirname, filename, this, kItemDir, uid_, gid_));
411
412
1/2
✓ Branch 2 taken 145 times.
✗ Branch 3 not taken.
145 ProcessUnmaterializedDirectory(dummy);
413
2/4
✓ Branch 1 taken 145 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 145 times.
✗ Branch 5 not taken.
145 dirs_[target] = dummy;
414
1/2
✓ Branch 1 taken 145 times.
✗ Branch 2 not taken.
145 know_directories_.insert(target);
415 145 }
416
417 } // namespace publish
418