GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/sync_item.cc
Date: 2026-10-04 02:40:33
Exec Total Coverage
Lines: 59 268 22.0%
Branches: 29 364 8.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM file system
3 */
4
5 #include "sync_item.h"
6
7
8 #if !defined(__APPLE__)
9 #include <sys/sysmacros.h>
10 #endif // __APPLE__
11
12 #include <cerrno>
13 #include <vector>
14
15 #include "ingestion/ingestion_source.h"
16 #include "sync_union.h"
17 #include "util/string.h"
18 #include "util/exception.h"
19
20 using namespace std; // NOLINT
21
22 namespace publish {
23
24 ✗ SyncItem::SyncItem()
25 ✗ : rdonly_type_(static_cast<SyncItemType>(0))
26 ✗ , graft_size_(-1)
27 ✗ , scratch_type_(static_cast<SyncItemType>(0))
28 ✗ , union_engine_(NULL)
29 ✗ , whiteout_(false)
30 ✗ , opaque_(false)
31 ✗ , masked_hardlink_(false)
32 ✗ , has_catalog_marker_(false)
33 ✗ , valid_graft_(false)
34 ✗ , graft_marker_present_(false)
35 ✗ , external_data_(false)
36 ✗ , direct_io_(false)
37 ✗ , volatile_(false)
38 ✗ , graft_chunklist_(NULL)
39 ✗ , compression_algorithm_(zlib::kZlibDefault)
40 ✗ , has_compression_algorithm_(false) { }
41
42 1058 SyncItem::SyncItem(const std::string &relative_parent_path,
43 const std::string &filename,
44 const SyncUnion *union_engine,
45 1058 const SyncItemType entry_type)
46 1058 : rdonly_type_(kItemUnknown)
47 1058 , graft_size_(-1)
48 1058 , scratch_type_(entry_type)
49 1058 , filename_(filename)
50 1058 , union_engine_(union_engine)
51 1058 , whiteout_(false)
52 1058 , opaque_(false)
53 1058 , masked_hardlink_(false)
54 1058 , has_catalog_marker_(false)
55 1058 , valid_graft_(false)
56 1058 , graft_marker_present_(false)
57 1058 , external_data_(false)
58 1058 , direct_io_(false)
59 1058 , volatile_(false)
60
1/2
✓ Branch 1 taken 1058 times.
✗ Branch 2 not taken.
1058 , relative_parent_path_(relative_parent_path)
61 1058 , graft_chunklist_(NULL)
62 1058 , compression_algorithm_(zlib::kZlibDefault)
63 2116 , has_compression_algorithm_(false) {
64 1058 content_hash_.algorithm = shash::kAny;
65 1058 }
66
67
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1058 times.
2116 SyncItem::~SyncItem() { delete graft_chunklist_; }
68
69
70 ✗ SyncItemType SyncItem::GetGenericFiletype(
71 const SyncItem::EntryStat &stat) const {
72 ✗ const SyncItemType type = stat.GetSyncItemType();
73 ✗ if (type == kItemUnknown) {
74 ✗ PANIC(kLogStderr,
75 "[WARNING] '%s' has an unsupported file type (st_mode: %d errno: %d)",
76 GetRelativePath().c_str(), stat.stat.st_mode, stat.error_code);
77 }
78 ✗ return type;
79 }
80
81
82 782 SyncItemType SyncItem::GetRdOnlyFiletype() const {
83 782 StatRdOnly();
84 // file could not exist in read-only branch, or a regular file could have
85 // been replaced by a directory in the read/write branch, like:
86 // rdonly:
87 // /foo/bar/regular_file <-- ENOTDIR when asking for (.../is_dir_now)
88 // r/w:
89 // /foo/bar/regular_file/
90 // /foo/bar/regular_file/is_dir_now
91
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 782 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
782 if (rdonly_stat_.error_code == ENOENT || rdonly_stat_.error_code == ENOTDIR)
92 782 return kItemNew;
93 ✗ return GetGenericFiletype(rdonly_stat_);
94 }
95
96
97 ✗ SyncItemType SyncItemNative::GetScratchFiletype() const {
98 ✗ StatScratch(/* refresh= */ false);
99 ✗ if (scratch_stat_.error_code != 0) {
100 ✗ PANIC(kLogStderr, "[WARNING] Failed to stat() '%s' in scratch. (errno: %s)",
101 GetRelativePath().c_str(), scratch_stat_.error_code);
102 }
103
104 ✗ return GetGenericFiletype(scratch_stat_);
105 }
106
107 ✗ SyncItemType SyncItem::GetUnionFiletype() const {
108 ✗ StatUnion();
109 ✗ if (union_stat_.error_code == ENOENT || union_stat_.error_code == ENOTDIR)
110 ✗ return kItemUnknown;
111 ✗ return GetGenericFiletype(union_stat_);
112 }
113
114 1012 bool SyncItemNative::IsType(const SyncItemType expected_type) const {
115
2/6
✓ Branch 2 taken 1012 times.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 1012 times.
1012 if (filename().substr(0, 12) == ".cvmfsgraft-") {
116 ✗ scratch_type_ = kItemMarker;
117
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1012 times.
1012 } else if (scratch_type_ == kItemUnknown) {
118 ✗ scratch_type_ = GetScratchFiletype();
119 }
120 1012 return scratch_type_ == expected_type;
121 }
122
123 ✗ void SyncItem::MarkAsWhiteout(const std::string &actual_filename) {
124 ✗ StatScratch(/* refresh= */ true);
125 // Mark the file as whiteout entry and strip the whiteout prefix
126 ✗ whiteout_ = true;
127 ✗ filename_ = actual_filename;
128
129 // Find the entry in the repository
130 ✗ StatRdOnly(true); // <== refreshing the stat (filename might have changed)
131
132 ✗ const SyncItemType deleted_type = (rdonly_stat_.error_code == 0)
133 ✗ ? GetRdOnlyFiletype()
134 ✗ : kItemUnknown;
135
136 ✗ rdonly_type_ = deleted_type;
137 ✗ scratch_type_ = deleted_type;
138
139 ✗ if (deleted_type == kItemUnknown) {
140 // Marking a SyncItem as 'whiteout' but no file to be removed found: This
141 // should not happen (actually AUFS prevents users from creating whiteouts)
142 // but can be provoked through an AUFS 'bug' (see test 593 or CVM-880).
143 // --> Warn the user, continue with kItemUnknown and cross your fingers!
144 ✗ PrintWarning("'" + GetRelativePath()
145 ✗ + "' should be deleted, but was not found in repository.");
146 }
147 }
148
149
150 ✗ void SyncItem::MarkAsOpaqueDirectory() {
151 ✗ assert(IsDirectory());
152 ✗ opaque_ = true;
153 }
154
155
156 ✗ unsigned int SyncItem::GetRdOnlyLinkcount() const {
157 ✗ StatRdOnly();
158 ✗ return rdonly_stat_.stat.st_nlink;
159 }
160
161
162 ✗ uint64_t SyncItem::GetRdOnlyInode() const {
163 ✗ StatRdOnly();
164 ✗ return rdonly_stat_.stat.st_ino;
165 }
166
167
168 ✗ unsigned int SyncItem::GetUnionLinkcount() const {
169 ✗ StatUnion();
170 ✗ return union_stat_.stat.st_nlink;
171 }
172
173
174 ✗ uint64_t SyncItem::GetUnionInode() const {
175 ✗ StatUnion();
176 ✗ return union_stat_.stat.st_ino;
177 }
178
179 ✗ uint64_t SyncItem::GetScratchSize() const {
180 ✗ StatScratch(/* refresh= */ false);
181 ✗ return scratch_stat_.stat.st_size;
182 }
183
184 ✗ uint64_t SyncItem::GetRdOnlySize() const {
185 ✗ StatRdOnly();
186 ✗ return rdonly_stat_.stat.st_size;
187 }
188
189 ✗ IngestionSource *SyncItemNative::CreateIngestionSource() const {
190 ✗ return new FileIngestionSource(GetUnionPath());
191 }
192
193 1196 void SyncItem::StatGeneric(const string &path,
194 EntryStat *info,
195 const bool refresh) {
196
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1196 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1196 if (info->obtained && !refresh)
197 ✗ return;
198 1196 const int retval = platform_lstat(path.c_str(), &info->stat);
199
1/2
✓ Branch 0 taken 1196 times.
✗ Branch 1 not taken.
1196 info->error_code = (retval != 0) ? errno : 0;
200 1196 info->obtained = true;
201 }
202
203
204 ✗ catalog::DirectoryEntryBase SyncItemNative::CreateBasicCatalogDirent(
205 bool enable_mtime_ns) const {
206 ✗ catalog::DirectoryEntryBase dirent;
207
208 // inode and parent inode is determined at runtime of client
209 ✗ dirent.inode_ = catalog::DirectoryEntry::kInvalidInode;
210
211 // this might mask the actual link count in case hardlinks are not supported
212 // (i.e. on setups using OverlayFS)
213 ✗ dirent.linkcount_ = HasHardlinks() ? this->GetUnionStat().st_nlink : 1;
214
215 ✗ dirent.mode_ = this->GetUnionStat().st_mode;
216 ✗ dirent.uid_ = this->GetUnionStat().st_uid;
217 ✗ dirent.gid_ = this->GetUnionStat().st_gid;
218 ✗ dirent.size_ = graft_size_ > -1 ? graft_size_ : this->GetUnionStat().st_size;
219 ✗ dirent.mtime_ = this->GetUnionStat().st_mtime;
220 ✗ dirent.checksum_ = this->GetContentHash();
221 ✗ dirent.is_external_file_ = this->IsExternalData();
222 ✗ dirent.is_direct_io_ = this->IsDirectIo();
223 ✗ dirent.is_volatile_ = this->IsVolatile();
224 ✗ dirent.compression_algorithm_ = this->GetCompressionAlgorithm();
225
226 ✗ dirent.name_.Assign(filename().data(), filename().length());
227
228 ✗ if (this->IsSymlink()) {
229 char slnk[PATH_MAX + 1];
230 ✗ const ssize_t length = readlink((this->GetUnionPath()).c_str(), slnk,
231 PATH_MAX);
232 ✗ assert(length >= 0);
233 ✗ dirent.symlink_.Assign(slnk, length);
234 }
235
236 ✗ if (this->IsCharacterDevice() || this->IsBlockDevice()) {
237 ✗ dirent.size_ = makedev(GetRdevMajor(), GetRdevMinor());
238 }
239
240 ✗ if (enable_mtime_ns) {
241 #ifdef __APPLE__
242 dirent.mtime_ns_ = static_cast<int32_t>(
243 this->GetUnionStat().st_mtimespec.tv_nsec);
244 #else
245 ✗ dirent.mtime_ns_ = static_cast<int32_t>(
246 ✗ this->GetUnionStat().st_mtim.tv_nsec);
247 #endif
248 }
249
250 ✗ return dirent;
251 }
252
253
254 782 std::string SyncItem::GetRdOnlyPath() const {
255
1/2
✓ Branch 1 taken 782 times.
✗ Branch 2 not taken.
782 const string relative_path = GetRelativePath().empty()
256 ? ""
257
5/18
✗ Branch 0 not taken.
✓ Branch 1 taken 782 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 782 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 782 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 782 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 782 times.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
1564 : "/" + GetRelativePath();
258
2/4
✓ Branch 1 taken 782 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 782 times.
✗ Branch 5 not taken.
2346 return union_engine_->rdonly_path() + relative_path;
259 782 }
260
261 828 std::string SyncItem::GetUnionPath() const {
262
1/2
✓ Branch 1 taken 828 times.
✗ Branch 2 not taken.
828 const string relative_path = GetRelativePath().empty()
263 ? ""
264
5/18
✗ Branch 0 not taken.
✓ Branch 1 taken 828 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 828 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 828 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 828 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 828 times.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
1656 : "/" + GetRelativePath();
265
2/4
✓ Branch 1 taken 828 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 828 times.
✗ Branch 5 not taken.
2484 return union_engine_->union_path() + relative_path;
266 828 }
267
268 ✗ std::string SyncItem::GetScratchPath() const {
269 ✗ const string relative_path = GetRelativePath().empty()
270 ? ""
271 ✗ : "/" + GetRelativePath();
272 ✗ return union_engine_->scratch_path() + relative_path;
273 // return union_engine_->scratch_path() + filename();
274 }
275
276 414 void SyncItem::CheckMarkerFiles() {
277
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 414 times.
414 if (IsRegularFile()) {
278 ✗ CheckGraft();
279
1/2
✓ Branch 1 taken 414 times.
✗ Branch 2 not taken.
414 } else if (IsDirectory()) {
280 414 CheckCatalogMarker();
281 }
282 414 }
283
284 414 void SyncItem::CheckCatalogMarker() {
285
2/4
✓ Branch 1 taken 414 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 414 times.
✗ Branch 5 not taken.
414 const std::string path(GetUnionPath() + "/.cvmfscatalog");
286 414 EntryStat stat;
287 414 StatGeneric(path, &stat, false);
288
1/2
✓ Branch 0 taken 414 times.
✗ Branch 1 not taken.
414 if (stat.error_code) {
289 414 has_catalog_marker_ = false;
290 414 return;
291 }
292 ✗ if (stat.GetSyncItemType() == kItemFile) {
293 ✗ has_catalog_marker_ = true;
294 ✗ return;
295 }
296 ✗ PANIC(kLogStderr, "Error: '%s' is not a regular file.", path.c_str());
297 414 }
298
299
300 ✗ std::string SyncItem::GetGraftMarkerPath() const {
301 ✗ return union_engine_->scratch_path() + "/"
302 ✗ + ((relative_parent_path_.empty())
303 ✗ ? ".cvmfsgraft-" + filename_
304 ✗ : relative_parent_path_
305 ✗ + (filename_.empty() ? ""
306 ✗ : ("/.cvmfsgraft-" + filename_)));
307 }
308
309 ✗ void SyncItem::CheckGraft() {
310 ✗ valid_graft_ = false;
311 ✗ bool found_checksum = false;
312 ✗ const std::string checksum_type;
313 ✗ const std::string checksum_value;
314 ✗ const std::string graftfile = GetGraftMarkerPath();
315 ✗ LogCvmfs(kLogFsTraversal, kLogDebug, "Checking potential graft path %s.",
316 graftfile.c_str());
317 ✗ FILE *fp = fopen(graftfile.c_str(), "r");
318 ✗ if (fp == NULL) {
319 // This sync item can be a file from a removed directory tree on overlayfs.
320 // In this case, the entire tree is missing on the scratch directory and
321 // the errno is ENOTDIR.
322 ✗ if ((errno != ENOENT) && (errno != ENOTDIR)) {
323 ✗ LogCvmfs(kLogFsTraversal, kLogWarning,
324 "Unable to open graft file "
325 "(%s): %s (errno=%d)",
326 ✗ graftfile.c_str(), strerror(errno), errno);
327 }
328 ✗ return;
329 }
330 ✗ graft_marker_present_ = true;
331 ✗ valid_graft_ = true;
332 ✗ std::string line;
333 ✗ const std::vector<std::string> contents;
334
335 ✗ std::vector<off_t> chunk_offsets;
336 ✗ std::vector<shash::Any> chunk_checksums;
337
338 ✗ while (GetLineFile(fp, &line)) {
339 ✗ std::string trimmed_line = Trim(line);
340
341 ✗ if (!trimmed_line.size()) {
342 ✗ continue;
343 }
344 ✗ if (trimmed_line[0] == '#') {
345 ✗ continue;
346 }
347
348 ✗ std::vector<std::string> info = SplitStringBounded(2, trimmed_line, '=');
349
350 ✗ if (info.size() != 2) {
351 ✗ LogCvmfs(kLogFsTraversal, kLogWarning, "Invalid line in graft file: %s",
352 trimmed_line.c_str());
353 }
354 ✗ info[0] = Trim(info[0]);
355 ✗ info[1] = Trim(info[1]);
356 ✗ if (info[0] == "size") {
357 uint64_t tmp_size;
358 ✗ if (!String2Uint64Parse(info[1], &tmp_size)) {
359 ✗ LogCvmfs(kLogFsTraversal, kLogWarning,
360 "Failed to parse value of %s "
361 "to integer: %s (errno=%d)",
362 ✗ trimmed_line.c_str(), strerror(errno), errno);
363 ✗ continue;
364 }
365 ✗ graft_size_ = tmp_size;
366 ✗ } else if (info[0] == "checksum") {
367 ✗ const std::string hash_str = info[1];
368 ✗ const shash::HexPtr hashP(hash_str);
369 ✗ if (hashP.IsValid()) {
370 ✗ content_hash_ = shash::MkFromHexPtr(hashP);
371 ✗ found_checksum = true;
372 } else {
373 ✗ LogCvmfs(kLogFsTraversal, kLogWarning, "Invalid checksum value: %s.",
374 ✗ info[1].c_str());
375 }
376 ✗ continue;
377 ✗ } else if (info[0] == "chunk_offsets") {
378 ✗ std::vector<std::string> offsets = SplitString(info[1], ',');
379 ✗ for (std::vector<std::string>::const_iterator it = offsets.begin();
380 ✗ it != offsets.end();
381 ✗ it++) {
382 uint64_t val;
383 ✗ if (!String2Uint64Parse(*it, &val)) {
384 ✗ valid_graft_ = false;
385 ✗ LogCvmfs(kLogFsTraversal, kLogWarning, "Invalid chunk offset: %s.",
386 it->c_str());
387 ✗ break;
388 }
389 ✗ chunk_offsets.push_back(val);
390 }
391 ✗ } else if (info[0] == "chunk_checksums") {
392 ✗ std::vector<std::string> csums = SplitString(info[1], ',');
393 ✗ for (std::vector<std::string>::const_iterator it = csums.begin();
394 ✗ it != csums.end();
395 ✗ it++) {
396 ✗ const shash::HexPtr hashP(*it);
397 ✗ if (hashP.IsValid()) {
398 ✗ chunk_checksums.push_back(shash::MkFromHexPtr(hashP));
399 } else {
400 ✗ LogCvmfs(kLogFsTraversal, kLogWarning,
401 "Invalid chunk checksum "
402 "value: %s.",
403 it->c_str());
404 ✗ valid_graft_ = false;
405 ✗ break;
406 }
407 }
408 ✗ } else if (info[0] == "compression") {
409 ✗ SetCompressionAlgorithm(zlib::ParseCompressionAlgorithm(info[1]));
410 }
411 }
412 ✗ if (!feof(fp)) {
413 ✗ LogCvmfs(kLogFsTraversal, kLogWarning,
414 "Unable to read from catalog "
415 "marker (%s): %s (errno=%d)",
416 ✗ graftfile.c_str(), strerror(errno), errno);
417 }
418 ✗ fclose(fp);
419 ✗ valid_graft_ = valid_graft_ && (graft_size_ > -1) && found_checksum
420 ✗ && (chunk_checksums.size() == chunk_offsets.size());
421
422 ✗ if (!valid_graft_ || chunk_offsets.empty())
423 ✗ return;
424
425 // Parse chunks
426 ✗ graft_chunklist_ = new FileChunkList(chunk_offsets.size());
427 ✗ off_t last_offset = chunk_offsets[0];
428 ✗ if (last_offset != 0) {
429 ✗ LogCvmfs(kLogFsTraversal, kLogWarning,
430 "First chunk offset must be 0"
431 " (in graft marker %s).",
432 graftfile.c_str());
433 ✗ valid_graft_ = false;
434 }
435 ✗ for (unsigned idx = 1; idx < chunk_offsets.size(); idx++) {
436 ✗ const off_t cur_offset = chunk_offsets[idx];
437 ✗ if (last_offset >= cur_offset) {
438 ✗ LogCvmfs(kLogFsTraversal, kLogWarning,
439 "Chunk offsets must be sorted "
440 "in strictly increasing order (in graft marker %s).",
441 graftfile.c_str());
442 ✗ valid_graft_ = false;
443 ✗ break;
444 }
445 ✗ const size_t cur_size = cur_offset - last_offset;
446 ✗ graft_chunklist_->PushBack(
447 ✗ FileChunk(chunk_checksums[idx - 1], last_offset, cur_size));
448 ✗ last_offset = cur_offset;
449 }
450 ✗ if (graft_size_ <= last_offset) {
451 ✗ LogCvmfs(kLogFsTraversal, kLogWarning,
452 "Last offset must be strictly "
453 "less than total file size (in graft marker %s).",
454 graftfile.c_str());
455 ✗ valid_graft_ = false;
456 }
457 ✗ graft_chunklist_->PushBack(FileChunk(chunk_checksums.back(), last_offset,
458 ✗ graft_size_ - last_offset));
459 }
460
461 } // namespace publish
462