GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/sync_item.cc
Date: 2026-08-30 02:40:36
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 667 SyncItem::SyncItem(const std::string &relative_parent_path,
43 const std::string &filename,
44 const SyncUnion *union_engine,
45 667 const SyncItemType entry_type)
46 667 : rdonly_type_(kItemUnknown)
47 667 , graft_size_(-1)
48 667 , scratch_type_(entry_type)
49 667 , filename_(filename)
50 667 , union_engine_(union_engine)
51 667 , whiteout_(false)
52 667 , opaque_(false)
53 667 , masked_hardlink_(false)
54 667 , has_catalog_marker_(false)
55 667 , valid_graft_(false)
56 667 , graft_marker_present_(false)
57 667 , external_data_(false)
58 667 , direct_io_(false)
59 667 , volatile_(false)
60
1/2
✓ Branch 1 taken 667 times.
✗ Branch 2 not taken.
667 , relative_parent_path_(relative_parent_path)
61 667 , graft_chunklist_(NULL)
62 667 , compression_algorithm_(zlib::kZlibDefault)
63 1334 , has_compression_algorithm_(false) {
64 667 content_hash_.algorithm = shash::kAny;
65 667 }
66
67
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 667 times.
1334 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 493 SyncItemType SyncItem::GetRdOnlyFiletype() const {
83 493 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 493 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
493 if (rdonly_stat_.error_code == ENOENT || rdonly_stat_.error_code == ENOTDIR)
92 493 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 638 bool SyncItemNative::IsType(const SyncItemType expected_type) const {
115
2/6
✓ Branch 2 taken 638 times.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 638 times.
638 if (filename().substr(0, 12) == ".cvmfsgraft-") {
116 scratch_type_ = kItemMarker;
117
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 638 times.
638 } else if (scratch_type_ == kItemUnknown) {
118 scratch_type_ = GetScratchFiletype();
119 }
120 638 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 754 void SyncItem::StatGeneric(const string &path,
194 EntryStat *info,
195 const bool refresh) {
196
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 754 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
754 if (info->obtained && !refresh)
197 return;
198 754 const int retval = platform_lstat(path.c_str(), &info->stat);
199
1/2
✓ Branch 0 taken 754 times.
✗ Branch 1 not taken.
754 info->error_code = (retval != 0) ? errno : 0;
200 754 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 493 std::string SyncItem::GetRdOnlyPath() const {
255
1/2
✓ Branch 1 taken 493 times.
✗ Branch 2 not taken.
493 const string relative_path = GetRelativePath().empty()
256 ? ""
257
5/18
✗ Branch 0 not taken.
✓ Branch 1 taken 493 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 493 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 493 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 493 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 493 times.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
986 : "/" + GetRelativePath();
258
2/4
✓ Branch 1 taken 493 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 493 times.
✗ Branch 5 not taken.
1479 return union_engine_->rdonly_path() + relative_path;
259 493 }
260
261 522 std::string SyncItem::GetUnionPath() const {
262
1/2
✓ Branch 1 taken 522 times.
✗ Branch 2 not taken.
522 const string relative_path = GetRelativePath().empty()
263 ? ""
264
5/18
✗ Branch 0 not taken.
✓ Branch 1 taken 522 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 522 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 522 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 522 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 522 times.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
1044 : "/" + GetRelativePath();
265
2/4
✓ Branch 1 taken 522 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 522 times.
✗ Branch 5 not taken.
1566 return union_engine_->union_path() + relative_path;
266 522 }
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 261 void SyncItem::CheckMarkerFiles() {
277
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 261 times.
261 if (IsRegularFile()) {
278 CheckGraft();
279
1/2
✓ Branch 1 taken 261 times.
✗ Branch 2 not taken.
261 } else if (IsDirectory()) {
280 261 CheckCatalogMarker();
281 }
282 261 }
283
284 261 void SyncItem::CheckCatalogMarker() {
285
2/4
✓ Branch 1 taken 261 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 261 times.
✗ Branch 5 not taken.
261 const std::string path(GetUnionPath() + "/.cvmfscatalog");
286 261 EntryStat stat;
287 261 StatGeneric(path, &stat, false);
288
1/2
✓ Branch 0 taken 261 times.
✗ Branch 1 not taken.
261 if (stat.error_code) {
289 261 has_catalog_marker_ = false;
290 261 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 261 }
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