GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/swissknife_check.cc
Date: 2026-10-04 02:40:33
Exec Total Coverage
Lines: 0 629 0.0%
Branches: 0 468 0.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System.
3 *
4 * This tool checks a cvmfs repository for file catalog errors.
5 */
6
7 #include "swissknife_check.h"
8
9 #include <inttypes.h>
10 #include <unistd.h>
11
12 #include <cassert>
13 #include <map>
14 #include <memory>
15 #include <set>
16 #include <string>
17 #include <vector>
18
19 #include "catalog_sql.h"
20 #include "compression/compression.h"
21 #include "file_chunk.h"
22 #include "history_sqlite.h"
23 #include "manifest.h"
24 #include "network/download.h"
25 #include "network/sink_path.h"
26 #include "reflog.h"
27 #include "sanitizer.h"
28 #include "shortstring.h"
29 #include "util/exception.h"
30 #include "util/logging.h"
31 #include "util/posix.h"
32
33 using namespace std; // NOLINT
34
35 // for map of duplicate entries; as in kvstore.cc
36 ✗ static inline uint32_t hasher_any(const shash::Any &key) {
37 // We'll just do the same thing as hasher_md5, since every hash is at
38 // least as large.
39 ✗ return *const_cast<uint32_t *>(reinterpret_cast<const uint32_t *>(key.digest)
40 ✗ + 1);
41 }
42
43
44 namespace swissknife {
45
46 ✗ CommandCheck::CommandCheck()
47 ✗ : check_chunks_(false)
48 ✗ , no_duplicates_map_(false)
49 ✗ , is_remote_(false)
50 ✗ , inclusion_spec_(NULL) {
51 ✗ const shash::Any hash_null;
52 ✗ duplicates_map_.Init(16, hash_null, hasher_any);
53 }
54
55 ✗ bool CommandCheck::CompareEntries(const catalog::DirectoryEntry &a,
56 const catalog::DirectoryEntry &b,
57 const bool compare_names,
58 const bool is_transition_point) {
59 typedef catalog::DirectoryEntry::Difference Difference;
60
61 ✗ const catalog::DirectoryEntry::Differences diffs = a.CompareTo(b);
62 ✗ if (diffs == Difference::kIdentical) {
63 ✗ return true;
64 }
65
66 // in case of a nested catalog transition point the controlling flags are
67 // supposed to differ. If this is the only difference we are done...
68 ✗ if (is_transition_point
69 ✗ && (diffs ^ Difference::kNestedCatalogTransitionFlags) == 0) {
70 ✗ return true;
71 }
72
73 ✗ bool retval = true;
74 ✗ if (compare_names) {
75 ✗ if (diffs & Difference::kName) {
76 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "names differ: %s / %s", a.name().c_str(),
77 b.name().c_str());
78 ✗ retval = false;
79 }
80 }
81 ✗ if (diffs & Difference::kLinkcount) {
82 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "linkcounts differ: %u / %u", a.linkcount(),
83 b.linkcount());
84 ✗ retval = false;
85 }
86 ✗ if (diffs & Difference::kHardlinkGroup) {
87 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "hardlink groups differ: %u / %u",
88 a.hardlink_group(), b.hardlink_group());
89 ✗ retval = false;
90 }
91 ✗ if (diffs & Difference::kSize) {
92 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "sizes differ: %" PRIu64 " / %" PRIu64,
93 a.size(), b.size());
94 ✗ retval = false;
95 }
96 ✗ if (diffs & Difference::kMode) {
97 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "modes differ: %u / %u", a.mode(),
98 b.mode());
99 ✗ retval = false;
100 }
101 ✗ if (diffs & Difference::kMtime) {
102 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "timestamps differ: %lu / %lu", a.mtime(),
103 b.mtime());
104 ✗ retval = false;
105 }
106 ✗ if (diffs & Difference::kChecksum) {
107 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "content hashes differ: %s / %s",
108 a.checksum().ToString().c_str(), b.checksum().ToString().c_str());
109 ✗ retval = false;
110 }
111 ✗ if (diffs & Difference::kSymlink) {
112 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "symlinks differ: %s / %s",
113 a.symlink().c_str(), b.symlink().c_str());
114 ✗ retval = false;
115 }
116 ✗ if (diffs & Difference::kExternalFileFlag) {
117 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
118 "external file flag differs: %d / %d "
119 "(%s / %s)",
120 a.IsExternalFile(), b.IsExternalFile(), a.name().c_str(),
121 b.name().c_str());
122 ✗ retval = false;
123 }
124 ✗ if (diffs & Difference::kVolatileFlag) {
125 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
126 "volatile flag differs: %d / %d "
127 "(%s / %s)",
128 a.IsVolatile(), b.IsVolatile(), a.name().c_str(),
129 b.name().c_str());
130 ✗ retval = false;
131 }
132 ✗ if (diffs & Difference::kHasXattrsFlag) {
133 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
134 "extended attributes differ: %d / %d "
135 "(%s / %s)",
136 a.HasXattrs(), b.HasXattrs(), a.name().c_str(), b.name().c_str());
137 ✗ retval = false;
138 }
139 ✗ if (!is_transition_point) {
140 ✗ if (diffs & Difference::kUid) {
141 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "uids differ: %d / %d (%s / %s)", a.uid(),
142 b.uid(), a.name().c_str(), b.name().c_str());
143 ✗ retval = false;
144 }
145 ✗ if (diffs & Difference::kGid) {
146 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "gids differ: %d / %d (%s / %s)", a.gid(),
147 b.gid(), a.name().c_str(), b.name().c_str());
148 ✗ retval = false;
149 }
150 }
151
152 ✗ return retval;
153 }
154
155
156 ✗ bool CommandCheck::CompareCounters(const catalog::Counters &a,
157 const catalog::Counters &b) {
158 ✗ const catalog::Counters::FieldsMap map_a = a.GetFieldsMap();
159 ✗ const catalog::Counters::FieldsMap map_b = b.GetFieldsMap();
160
161 ✗ bool retval = true;
162 ✗ catalog::Counters::FieldsMap::const_iterator i = map_a.begin();
163 ✗ const catalog::Counters::FieldsMap::const_iterator iend = map_a.end();
164 ✗ for (; i != iend; ++i) {
165 ✗ const catalog::Counters::FieldsMap::const_iterator comp = map_b.find(
166 ✗ i->first);
167 ✗ assert(comp != map_b.end());
168
169 ✗ if (*(i->second) != *(comp->second)) {
170 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
171 "catalog statistics mismatch: %s (expected: %" PRIu64 " / "
172 "in catalog: %" PRIu64 ")",
173 comp->first.c_str(), *(i->second), *(comp->second));
174 ✗ retval = false;
175 }
176 }
177
178 ✗ return retval;
179 }
180
181
182 /**
183 * Checks for existence of a file either locally or via HTTP head
184 */
185 ✗ bool CommandCheck::Exists(const string &file) {
186 ✗ if (!is_remote_) {
187 ✗ return FileExists(file) || SymlinkExists(file);
188 } else {
189 ✗ const string url = repo_base_path_ + "/" + file;
190 ✗ LogCvmfs(kLogCvmfs, kLogVerboseMsg, "[Exists::url] %s", url.c_str());
191 ✗ download::JobInfo head(&url, false);
192 ✗ return download_manager()->Fetch(&head) == download::kFailOk;
193 }
194 }
195
196
197 /**
198 * Copies a file from the repository into a temporary file.
199 */
200 ✗ string CommandCheck::FetchPath(const string &path) {
201 ✗ string tmp_path;
202 ✗ FILE *f = CreateTempFile(temp_directory_ + "/cvmfstmp", kDefaultFileMode,
203 "w+", &tmp_path);
204 ✗ assert(f != NULL);
205
206 ✗ const string url = repo_base_path_ + "/" + path;
207 ✗ if (is_remote_) {
208 ✗ cvmfs::FileSink filesink(f);
209 ✗ download::JobInfo download_job(&url, false, false, NULL, &filesink);
210 ✗ const download::Failures retval = download_manager()->Fetch(&download_job);
211 ✗ if (retval != download::kFailOk) {
212 ✗ PANIC(kLogStderr, "failed to read %s", url.c_str());
213 }
214 ✗ } else {
215 ✗ const bool retval = CopyPath2File(url, f);
216 ✗ if (!retval) {
217 ✗ PANIC(kLogStderr, "failed to read %s", url.c_str());
218 }
219 }
220
221 ✗ fclose(f);
222 ✗ return tmp_path;
223 }
224
225
226 /**
227 * Verifies reflog checksum and looks for presence of the entry points
228 * referenced in the manifest.
229 */
230 ✗ bool CommandCheck::InspectReflog(const shash::Any &reflog_hash,
231 manifest::Manifest *manifest) {
232 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Inspecting log of references");
233 ✗ const string reflog_path = FetchPath(".cvmfsreflog");
234 ✗ shash::Any computed_hash(reflog_hash.algorithm);
235 ✗ manifest::Reflog::HashDatabase(reflog_path, &computed_hash);
236 ✗ if (computed_hash != reflog_hash) {
237 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
238 "The .cvmfsreflog has unexpected content hash %s (expected %s)",
239 computed_hash.ToString().c_str(), reflog_hash.ToString().c_str());
240 ✗ unlink(reflog_path.c_str());
241 ✗ return false;
242 }
243
244 const std::unique_ptr<manifest::Reflog> reflog(
245 ✗ manifest::Reflog::Open(reflog_path));
246 ✗ assert(reflog.get() != nullptr);
247 ✗ reflog->TakeDatabaseFileOwnership();
248
249 ✗ if (!reflog->ContainsCatalog(manifest->catalog_hash())) {
250 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
251 "failed to find catalog root hash %s in .cvmfsreflog",
252 manifest->catalog_hash().ToString().c_str());
253 ✗ return false;
254 }
255
256 ✗ if (!reflog->ContainsCertificate(manifest->certificate())) {
257 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
258 "failed to find certificate hash %s in .cvmfsreflog",
259 manifest->certificate().ToString().c_str());
260 ✗ return false;
261 }
262
263 ✗ if (!manifest->history().IsNull()
264 ✗ && !reflog->ContainsHistory(manifest->history())) {
265 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
266 "failed to find tag database's hash %s in .cvmfsreflog",
267 manifest->history().ToString().c_str());
268 ✗ return false;
269 }
270
271 ✗ if (!manifest->meta_info().IsNull()
272 ✗ && !reflog->ContainsMetainfo(manifest->meta_info())) {
273 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
274 "failed to find meta info hash %s in .cvmfsreflog",
275 manifest->meta_info().ToString().c_str());
276 ✗ return false;
277 }
278
279 ✗ return true;
280 }
281
282
283 /**
284 * Verifies the logical consistency of the tag database.
285 */
286 ✗ bool CommandCheck::InspectHistory(history::History *history) {
287 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Inspecting tag database");
288 bool retval;
289 ✗ vector<history::History::Tag> tags;
290 ✗ retval = history->List(&tags);
291 ✗ if (!retval) {
292 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to enumerate tags");
293 ✗ return false;
294 }
295 ✗ vector<history::History::Branch> branches;
296 ✗ retval = history->ListBranches(&branches);
297 ✗ if (!retval) {
298 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to enumerate branches");
299 ✗ return false;
300 }
301
302 ✗ bool result = true;
303
304 ✗ map<string, uint64_t> initial_revisions;
305 ✗ const sanitizer::BranchSanitizer sanitizer;
306 ✗ for (unsigned i = 0; i < branches.size(); ++i) {
307 ✗ if (!sanitizer.IsValid(branches[i].branch)) {
308 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "invalid branch name: %s",
309 branches[i].branch.c_str());
310 ✗ result = false;
311 }
312 ✗ initial_revisions[branches[i].branch] = branches[i].initial_revision;
313 }
314
315 ✗ set<string> used_branches; // all branches referenced in tag db
316 // TODO(jblomer): same root hash implies same size and revision
317 ✗ for (unsigned i = 0; i < tags.size(); ++i) {
318 ✗ used_branches.insert(tags[i].branch);
319 ✗ const map<string, uint64_t>::const_iterator iter = initial_revisions.find(
320 ✗ tags[i].branch);
321 ✗ if (iter == initial_revisions.end()) {
322 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "invalid branch %s in tag %s",
323 tags[i].branch.c_str(), tags[i].name.c_str());
324 ✗ result = false;
325 } else {
326 ✗ if (tags[i].revision < iter->second) {
327 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
328 "invalid revision %" PRIu64 " of tag %s", tags[i].revision,
329 tags[i].name.c_str());
330 ✗ result = false;
331 }
332 }
333 }
334
335 ✗ if (used_branches.size() != branches.size()) {
336 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "unused, dangling branches stored");
337 ✗ result = false;
338 }
339
340 ✗ return result;
341 }
342
343
344 /**
345 * Recursive catalog walk-through
346 *
347 * TODO(vavolkl): This method is large and does a lot of checks
348 * that could be split into smaller ones.
349 *
350 */
351 ✗ bool CommandCheck::Find(const catalog::Catalog *catalog,
352 const PathString &path,
353 catalog::DeltaCounters *computed_counters,
354 set<PathString> *bind_mountpoints) {
355 ✗ catalog::DirectoryEntryList entries;
356 ✗ catalog::DirectoryEntry this_directory;
357
358 ✗ if (!catalog->LookupPath(path, &this_directory)) {
359 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to lookup %s", path.c_str());
360 ✗ return false;
361 }
362 ✗ if (!catalog->ListingPath(path, &entries)) {
363 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to list %s", path.c_str());
364 ✗ return false;
365 }
366
367 ✗ uint32_t num_subdirs = 0;
368 ✗ bool retval = true;
369 typedef map<uint32_t, vector<catalog::DirectoryEntry> > HardlinkMap;
370 ✗ HardlinkMap hardlinks;
371 ✗ bool found_nested_marker = false;
372
373 ✗ for (unsigned i = 0; i < entries.size(); ++i) {
374 // for performance reasons, keep track of files already checked
375 // and only run requests once per hash
376 ✗ const bool entry_needs_check = !entries[i].checksum().IsNull()
377 ✗ && !entries[i].IsExternalFile()
378 ✗ && !(catalog::g_ignore_legacy_bulk_hashes
379 ✗ && entries[i].IsChunkedFile())
380 ✗ &&
381 // fallback cli option can force the entry to
382 // be checked
383 ✗ (no_duplicates_map_
384 ✗ || !duplicates_map_.Contains(
385 ✗ entries[i].checksum()));
386 ✗ if (entry_needs_check && !no_duplicates_map_)
387 ✗ duplicates_map_.Insert(entries[i].checksum(), 1);
388
389 ✗ PathString full_path(path);
390 ✗ full_path.Append("/", 1);
391 ✗ full_path.Append(entries[i].name().GetChars(),
392 ✗ entries[i].name().GetLength());
393 ✗ LogCvmfs(kLogCvmfs, kLogVerboseMsg, "[path] %s [needs check] %i",
394 full_path.c_str(), entry_needs_check);
395
396
397 // Name must not be empty
398 ✗ if (entries[i].name().IsEmpty()) {
399 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "empty path at %s", full_path.c_str());
400 ✗ retval = false;
401 }
402
403 // Catalog markers should indicate nested catalogs
404 ✗ if (entries[i].name() == NameString(string(".cvmfscatalog"))) {
405 ✗ if (catalog->mountpoint() != path) {
406 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
407 "found abandoned nested catalog marker at %s",
408 full_path.c_str());
409 ✗ retval = false;
410 }
411 ✗ found_nested_marker = true;
412 }
413
414 // Check if checksum is not null
415 ✗ if (entries[i].IsRegular() && !entries[i].IsChunkedFile()
416 ✗ && entries[i].checksum().IsNull()) {
417 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
418 "regular file pointing to zero-hash: '%s'", full_path.c_str());
419 ✗ retval = false;
420 }
421
422 // Check if the chunk is there
423 ✗ if (check_chunks_ && entry_needs_check) {
424 ✗ string chunk_path = "data/" + entries[i].checksum().MakePath();
425 ✗ if (entries[i].IsDirectory())
426 ✗ chunk_path += shash::kSuffixMicroCatalog;
427 ✗ if (!Exists(chunk_path)) {
428 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "data chunk %s (%s) missing",
429 entries[i].checksum().ToString().c_str(), full_path.c_str());
430 ✗ retval = false;
431 }
432 }
433
434 // Add hardlinks to counting map
435 ✗ if ((entries[i].linkcount() > 1) && !entries[i].IsDirectory()) {
436 ✗ if (entries[i].hardlink_group() == 0) {
437 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "invalid hardlink group for %s",
438 full_path.c_str());
439 ✗ retval = false;
440 } else {
441 ✗ const HardlinkMap::iterator hardlink_group = hardlinks.find(
442 ✗ entries[i].hardlink_group());
443 ✗ if (hardlink_group == hardlinks.end()) {
444 ✗ hardlinks[entries[i].hardlink_group()];
445 ✗ hardlinks[entries[i].hardlink_group()].push_back(entries[i]);
446 } else {
447 ✗ if (!CompareEntries(entries[i], (hardlink_group->second)[0], false)) {
448 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "hardlink %s doesn't match",
449 full_path.c_str());
450 ✗ retval = false;
451 }
452 ✗ hardlink_group->second.push_back(entries[i]);
453 } // Hardlink added to map
454 } // Hardlink group > 0
455 } // Hardlink found
456
457 // For any kind of entry, the linkcount should be > 0
458 ✗ if (entries[i].linkcount() == 0) {
459 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "Entry %s has linkcount 0.",
460 entries[i].name().c_str());
461 ✗ retval = false;
462 }
463
464 // Checks depending of entry type
465 ✗ if (!entries[i].IsRegular()) {
466 ✗ if (entries[i].IsDirectIo()) {
467 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "invalid direct i/o flag found: %s",
468 full_path.c_str());
469 ✗ retval = false;
470 }
471 }
472 ✗ if (entries[i].IsDirectory()) {
473 ✗ computed_counters->self.directories++;
474 ✗ num_subdirs++;
475 // Directory size
476 // if (entries[i].size() < 4096) {
477 // LogCvmfs(kLogCvmfs, kLogStderr, "invalid file size for %s",
478 // full_path.c_str());
479 // retval = false;
480 // }
481 // No directory hardlinks
482 ✗ if (entries[i].hardlink_group() != 0) {
483 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "directory hardlink found at %s",
484 full_path.c_str());
485 ✗ retval = false;
486 }
487 ✗ if (entries[i].IsNestedCatalogMountpoint()
488 ✗ || entries[i].IsBindMountpoint()) {
489 // Find transition point
490 ✗ if (entries[i].IsNestedCatalogMountpoint())
491 ✗ computed_counters->self.nested_catalogs++;
492 ✗ shash::Any tmp;
493 uint64_t tmp2;
494 ✗ const PathString mountpoint(full_path);
495 ✗ if (!catalog->FindNested(mountpoint, &tmp, &tmp2)) {
496 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "nested catalog at %s not registered",
497 full_path.c_str());
498 ✗ retval = false;
499 }
500
501 // check that the nested mountpoint is empty in the current catalog
502 ✗ catalog::DirectoryEntryList nested_entries;
503 ✗ if (catalog->ListingPath(full_path, &nested_entries)
504 ✗ && !nested_entries.empty()) {
505 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
506 "non-empty nested catalog mountpoint "
507 "at %s.",
508 full_path.c_str());
509 ✗ retval = false;
510 }
511
512 ✗ if (entries[i].IsBindMountpoint()) {
513 ✗ bind_mountpoints->insert(full_path);
514 ✗ if (entries[i].IsNestedCatalogMountpoint()) {
515 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
516 "bind mountpoint and nested mountpoint mutually exclusive"
517 " at %s.",
518 full_path.c_str());
519 ✗ retval = false;
520 }
521 }
522 ✗ } else {
523 // Recurse
524 ✗ if (!Find(catalog, full_path, computed_counters, bind_mountpoints))
525 ✗ retval = false;
526 }
527 ✗ } else if (entries[i].IsLink()) {
528 ✗ computed_counters->self.symlinks++;
529 // No hash for symbolics links
530 ✗ if (!entries[i].checksum().IsNull()) {
531 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "symbolic links with hash at %s",
532 full_path.c_str());
533 ✗ retval = false;
534 }
535 // Right size of symbolic link?
536 ✗ if (entries[i].size() != entries[i].symlink().GetLength()) {
537 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
538 "wrong symbolic link size for %s; "
539 "expected %u, got %lu",
540 full_path.c_str(), entries[i].symlink().GetLength(),
541 entries[i].size());
542 ✗ retval = false;
543 }
544 ✗ } else if (entries[i].IsRegular()) {
545 ✗ computed_counters->self.regular_files++;
546 ✗ computed_counters->self.file_size += entries[i].size();
547 ✗ } else if (entries[i].IsSpecial()) {
548 ✗ computed_counters->self.specials++;
549 // Size zero for special files
550 ✗ if (entries[i].size() != 0) {
551 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
552 "unexpected non-zero special file size %s", full_path.c_str());
553 ✗ retval = false;
554 }
555 // No hash for special files
556 ✗ if (!entries[i].checksum().IsNull()) {
557 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "special file with hash at %s",
558 full_path.c_str());
559 ✗ retval = false;
560 }
561 // No symlink
562 ✗ if (entries[i].symlink().GetLength() > 0) {
563 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
564 "special file with non-zero symlink at %s", full_path.c_str());
565 ✗ retval = false;
566 }
567 } else {
568 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "unknown file type %s",
569 full_path.c_str());
570 ✗ retval = false;
571 }
572
573 ✗ if (entries[i].HasXattrs()) {
574 ✗ computed_counters->self.xattrs++;
575 }
576
577 ✗ if (entries[i].IsExternalFile()) {
578 ✗ computed_counters->self.externals++;
579 ✗ computed_counters->self.external_file_size += entries[i].size();
580 ✗ if (!entries[i].IsRegular()) {
581 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
582 "only regular files can be external: %s", full_path.c_str());
583 ✗ retval = false;
584 }
585 }
586
587 // checking file chunk integrity
588 ✗ if (entries[i].IsChunkedFile()) {
589 ✗ FileChunkList chunks;
590 ✗ catalog->ListPathChunks(full_path, entries[i].hash_algorithm(), &chunks);
591
592 ✗ computed_counters->self.chunked_files++;
593 ✗ computed_counters->self.chunked_file_size += entries[i].size();
594 ✗ computed_counters->self.file_chunks += chunks.size();
595
596 // do we find file chunks for the chunked file in this catalog?
597 ✗ if (chunks.size() == 0) {
598 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "no file chunks found for big file %s",
599 full_path.c_str());
600 ✗ retval = false;
601 }
602
603 ✗ size_t aggregated_file_size = 0;
604 ✗ off_t next_offset = 0;
605
606 ✗ for (unsigned j = 0; j < chunks.size(); ++j) {
607 ✗ const FileChunk this_chunk = chunks.At(j);
608 // check if the chunk boundaries fit together...
609 ✗ if (next_offset != this_chunk.offset()) {
610 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "misaligned chunk offsets for %s",
611 full_path.c_str());
612 ✗ retval = false;
613 }
614 ✗ next_offset = this_chunk.offset() + this_chunk.size();
615 ✗ aggregated_file_size += this_chunk.size();
616
617 // are all data chunks in the data store?
618 ✗ if (check_chunks_ && !entries[i].IsExternalFile()) {
619 ✗ const shash::Any &chunk_hash = this_chunk.content_hash();
620 // for performance reasons, only perform the check once
621 // and skip if the hash has been checked before
622 ✗ bool chunk_needs_check = true;
623 ✗ if (!no_duplicates_map_ && !duplicates_map_.Contains(chunk_hash)) {
624 ✗ duplicates_map_.Insert(chunk_hash, 1);
625 ✗ } else if (!no_duplicates_map_) {
626 ✗ chunk_needs_check = false;
627 }
628 ✗ if (chunk_needs_check) {
629 ✗ const string chunk_path = "data/" + chunk_hash.MakePath();
630 ✗ if (!Exists(chunk_path)) {
631 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
632 "partial data chunk %s (%s -> "
633 "offset: %ld | size: %lu) missing",
634 this_chunk.content_hash().ToStringWithSuffix().c_str(),
635 full_path.c_str(), this_chunk.offset(),
636 this_chunk.size());
637 ✗ retval = false;
638 }
639 }
640 }
641 }
642
643 // is the aggregated chunk size equal to the actual file size?
644 ✗ if (aggregated_file_size != entries[i].size()) {
645 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
646 "chunks of file %s produce a size "
647 "mismatch. Calculated %zu bytes | %lu "
648 "bytes expected",
649 full_path.c_str(), aggregated_file_size, entries[i].size());
650 ✗ retval = false;
651 }
652 }
653 ✗ } // Loop through entries
654
655 // Check if nested catalog marker has been found
656 ✗ if (!path.IsEmpty() && (path == catalog->mountpoint())
657 ✗ && !found_nested_marker) {
658 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "nested catalog without marker at %s",
659 path.c_str());
660 ✗ retval = false;
661 }
662
663 // Check directory linkcount
664 ✗ if (this_directory.linkcount() != num_subdirs + 2) {
665 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
666 "wrong linkcount for %s; "
667 "expected %u, got %u",
668 path.c_str(), num_subdirs + 2, this_directory.linkcount());
669 ✗ retval = false;
670 }
671
672 // Check hardlink linkcounts
673 ✗ for (HardlinkMap::const_iterator i = hardlinks.begin(),
674 ✗ iEnd = hardlinks.end();
675 ✗ i != iEnd;
676 ✗ ++i) {
677 ✗ if (i->second[0].linkcount() != i->second.size()) {
678 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
679 "hardlink linkcount wrong for %s, "
680 "expected %lu, got %u",
681 (path.ToString() + "/" + i->second[0].name().ToString()).c_str(),
682 i->second.size(), i->second[0].linkcount());
683 ✗ retval = false;
684 }
685 }
686
687 ✗ return retval;
688 }
689
690
691 ✗ string CommandCheck::DownloadPiece(const shash::Any catalog_hash) {
692 ✗ const string source = "data/" + catalog_hash.MakePath();
693 ✗ const string dest = temp_directory_ + "/" + catalog_hash.ToString();
694 ✗ const string url = repo_base_path_ + "/" + source;
695
696 ✗ cvmfs::PathSink pathsink(dest);
697 download::JobInfo download_catalog(&url, true, false, &catalog_hash,
698 ✗ &pathsink);
699 ✗ const download::Failures retval = download_manager()->Fetch(
700 &download_catalog);
701 ✗ if (retval != download::kFailOk) {
702 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to download object %s (%d)",
703 catalog_hash.ToString().c_str(), retval);
704 ✗ return "";
705 }
706
707 ✗ return dest;
708 }
709
710
711 ✗ string CommandCheck::DecompressPiece(const shash::Any catalog_hash) {
712 ✗ const string source = "data/" + catalog_hash.MakePath();
713 ✗ const string dest = temp_directory_ + "/" + catalog_hash.ToString();
714 ✗ if (!zlib::DecompressPath2Path(source, dest))
715 ✗ return "";
716
717 ✗ return dest;
718 }
719
720
721 ✗ catalog::Catalog *CommandCheck::FetchCatalog(const string &path,
722 const shash::Any &catalog_hash,
723 const uint64_t catalog_size) {
724 ✗ string tmp_file;
725 ✗ if (!is_remote_)
726 ✗ tmp_file = DecompressPiece(catalog_hash);
727 else
728 ✗ tmp_file = DownloadPiece(catalog_hash);
729
730 ✗ if (tmp_file == "") {
731 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to load catalog %s",
732 catalog_hash.ToString().c_str());
733 ✗ return NULL;
734 }
735
736 ✗ catalog::Catalog *catalog = catalog::Catalog::AttachFreely(path, tmp_file,
737 catalog_hash);
738 ✗ const int64_t catalog_file_size = GetFileSize(tmp_file);
739 ✗ if (catalog_file_size <= 0) {
740 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "Error downloading catalog %s at %s %s",
741 catalog_hash.ToString().c_str(), path.c_str(), tmp_file.c_str());
742 ✗ assert(catalog_file_size > 0);
743 }
744 ✗ unlink(tmp_file.c_str());
745
746 ✗ if ((catalog_size > 0) && (uint64_t(catalog_file_size) != catalog_size)) {
747 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
748 "catalog file size mismatch, "
749 "expected %" PRIu64 ", got %" PRIu64,
750 catalog_size, catalog_file_size);
751 ✗ delete catalog;
752 ✗ return NULL;
753 }
754
755 ✗ return catalog;
756 }
757
758
759 ✗ bool CommandCheck::FindSubtreeRootCatalog(const string &subtree_path,
760 shash::Any *root_hash,
761 uint64_t *root_size) {
762 ✗ catalog::Catalog *current_catalog = FetchCatalog("", *root_hash);
763 ✗ if (current_catalog == NULL) {
764 ✗ return false;
765 }
766
767 typedef vector<string> Tokens;
768 ✗ const Tokens path_tokens = SplitString(subtree_path, '/');
769
770 ✗ string current_path = "";
771
772 ✗ Tokens::const_iterator i = path_tokens.begin();
773 ✗ const Tokens::const_iterator iend = path_tokens.end();
774 ✗ for (; i != iend; ++i) {
775 ✗ if (i->empty()) {
776 ✗ continue;
777 }
778
779 ✗ current_path += "/" + *i;
780 ✗ if (current_catalog->FindNested(PathString(current_path), root_hash,
781 root_size)) {
782 ✗ delete current_catalog;
783
784 ✗ if (current_path.length() < subtree_path.length()) {
785 ✗ current_catalog = FetchCatalog(current_path, *root_hash);
786 ✗ if (current_catalog == NULL) {
787 ✗ break;
788 }
789 } else {
790 ✗ return true;
791 }
792 }
793 }
794 ✗ return false;
795 }
796
797
798 /**
799 * Recursion on nested catalog level. No ownership of computed_counters.
800 */
801 ✗ bool CommandCheck::InspectTree(const string &path,
802 const shash::Any &catalog_hash,
803 const uint64_t catalog_size,
804 const bool is_nested_catalog,
805 const catalog::DirectoryEntry *transition_point,
806 catalog::DeltaCounters *computed_counters,
807 bool *pruned_subtree) {
808 ✗ LogCvmfs(kLogCvmfs, kLogStdout | kLogInform, "[inspecting catalog] %s at %s",
809 catalog_hash.ToString().c_str(), path == "" ? "/" : path.c_str());
810
811 ✗ const catalog::Catalog *catalog = FetchCatalog(path, catalog_hash,
812 catalog_size);
813 ✗ if (catalog == NULL) {
814 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to open catalog %s",
815 catalog_hash.ToString().c_str());
816 ✗ return false;
817 }
818
819 ✗ int retval = true;
820
821 ✗ if (catalog->root_prefix() != PathString(path.data(), path.length())) {
822 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
823 "root prefix mismatch; "
824 "expected %s, got %s",
825 path.c_str(), catalog->root_prefix().c_str());
826 ✗ retval = false;
827 }
828
829 // Check transition point
830 ✗ catalog::DirectoryEntry root_entry;
831 ✗ if (!catalog->LookupPath(catalog->root_prefix(), &root_entry)) {
832 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to lookup root entry (%s)",
833 path.c_str());
834 ✗ retval = false;
835 }
836 ✗ if (!root_entry.IsDirectory()) {
837 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "root entry not a directory (%s)",
838 path.c_str());
839 ✗ retval = false;
840 }
841 ✗ if (is_nested_catalog) {
842 ✗ if (transition_point != NULL
843 ✗ && !CompareEntries(*transition_point, root_entry, true, true)) {
844 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
845 "transition point and root entry differ (%s)", path.c_str());
846 ✗ retval = false;
847 }
848 ✗ if (!root_entry.IsNestedCatalogRoot()) {
849 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
850 "nested catalog root expected but not found (%s)", path.c_str());
851 ✗ retval = false;
852 }
853 } else {
854 ✗ if (root_entry.IsNestedCatalogRoot()) {
855 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
856 "nested catalog root found but not expected (%s)", path.c_str());
857 ✗ retval = false;
858 }
859 }
860
861 // Partial replication: excluded subtrees are not replicated at all (their
862 // catalogs are pruned during snapshot), so InspectTree is only ever reached
863 // for catalogs that are present. Pruning of the recursion happens in the
864 // nested-catalog loop below; here we just track whether anything was pruned
865 // so the aggregate counter comparison can be relaxed accordingly.
866 ✗ bool local_pruned = false;
867
868 // Traverse the catalog
869 ✗ set<PathString> bind_mountpoints;
870 ✗ if (!Find(catalog, PathString(path.data(), path.length()), computed_counters,
871 &bind_mountpoints)) {
872 ✗ retval = false;
873 }
874
875 // Check number of entries
876 ✗ if (root_entry.HasXattrs())
877 ✗ computed_counters->self.xattrs++;
878 ✗ const uint64_t num_found_entries = 1 + computed_counters->self.regular_files
879 ✗ + computed_counters->self.symlinks
880 ✗ + computed_counters->self.specials
881 ✗ + computed_counters->self.directories;
882 ✗ if (num_found_entries != catalog->GetNumEntries()) {
883 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
884 "dangling entries in catalog, "
885 "expected %" PRIu64 ", got %" PRIu64,
886 catalog->GetNumEntries(), num_found_entries);
887 ✗ retval = false;
888 }
889
890 // Recurse into nested catalogs
891 const catalog::Catalog::NestedCatalogList
892 ✗ &nested_catalogs = catalog->ListNestedCatalogs();
893 const catalog::Catalog::NestedCatalogList
894 ✗ own_nested_catalogs = catalog->ListOwnNestedCatalogs();
895 ✗ if (own_nested_catalogs.size()
896 ✗ != static_cast<uint64_t>(computed_counters->self.nested_catalogs)) {
897 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
898 "number of nested catalogs does not match;"
899 " expected %lu, got %lu",
900 computed_counters->self.nested_catalogs,
901 own_nested_catalogs.size());
902 ✗ retval = false;
903 }
904 ✗ set<PathString> nested_catalog_paths;
905 ✗ for (catalog::Catalog::NestedCatalogList::const_iterator
906 ✗ i = nested_catalogs.begin(),
907 ✗ iEnd = nested_catalogs.end();
908 ✗ i != iEnd;
909 ✗ ++i) {
910 ✗ nested_catalog_paths.insert(i->mountpoint);
911 }
912 ✗ if (nested_catalog_paths.size() != nested_catalogs.size()) {
913 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
914 "duplicates among nested catalogs and bind mountpoints");
915 ✗ retval = false;
916 }
917
918 ✗ for (catalog::Catalog::NestedCatalogList::const_iterator
919 ✗ i = nested_catalogs.begin(),
920 ✗ iEnd = nested_catalogs.end();
921 ✗ i != iEnd;
922 ✗ ++i) {
923 ✗ if (bind_mountpoints.find(i->mountpoint) != bind_mountpoints.end()) {
924 ✗ catalog::DirectoryEntry bind_mountpoint;
925 ✗ const PathString mountpoint("/" + i->mountpoint.ToString().substr(1));
926 ✗ if (!catalog->LookupPath(mountpoint, &bind_mountpoint)) {
927 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to lookup bind mountpoint %s",
928 mountpoint.c_str());
929 ✗ retval = false;
930 }
931 LogCvmfs(kLogCvmfs, kLogDebug, "skipping bind mountpoint %s",
932 i->mountpoint.c_str());
933 ✗ continue;
934 }
935 // Partial replication: a subtree that contains no included path is pruned
936 // on the Stratum-1 (catalog + objects absent), so we must not descend into
937 // it. Skipping recursion here also means the aggregate counters for this
938 // catalog will be lower than the stored ones; record that so the counter
939 // comparison can be relaxed below.
940 ✗ if (inclusion_spec_ != NULL
941 ✗ && inclusion_spec_->IsExcluded(i->mountpoint.ToString())) {
942 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
943 " Skipping pruned (excluded) subtree at %s",
944 i->mountpoint.c_str());
945 ✗ local_pruned = true;
946 ✗ continue;
947 }
948 ✗ catalog::DirectoryEntry nested_transition_point;
949 ✗ if (!catalog->LookupPath(i->mountpoint, &nested_transition_point)) {
950 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to lookup transition point %s",
951 i->mountpoint.c_str());
952 ✗ retval = false;
953 } else {
954 ✗ catalog::DeltaCounters nested_counters;
955 ✗ const bool is_nested = true;
956 ✗ bool nested_pruned = false;
957 ✗ if (!InspectTree(i->mountpoint.ToString(), i->hash, i->size, is_nested,
958 &nested_transition_point, &nested_counters,
959 &nested_pruned))
960 ✗ retval = false;
961 ✗ if (nested_pruned)
962 ✗ local_pruned = true;
963 ✗ nested_counters.PopulateToParent(computed_counters);
964 }
965 }
966
967 ✗ if (pruned_subtree != NULL && local_pruned)
968 ✗ *pruned_subtree = true;
969
970 // Check statistics counters
971 // Additionally account for root directory
972 ✗ computed_counters->self.directories++;
973 // Partial replication: when descendant subtrees have been pruned, the
974 // aggregate (subtree) counters stored in this catalog necessarily exceed
975 // what we could recompute, so the comparison would always fail. Skip it for
976 // affected catalogs; self counters and structure are still fully verified.
977 ✗ if (local_pruned) {
978 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
979 " Skipping aggregate counter check at %s (pruned subtree)",
980 path == "" ? "/" : path.c_str());
981 } else {
982 ✗ catalog::Counters compare_counters;
983 ✗ compare_counters.ApplyDelta(*computed_counters);
984 ✗ const catalog::Counters stored_counters = catalog->GetCounters();
985 ✗ if (!CompareCounters(compare_counters, stored_counters)) {
986 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "statistics counter mismatch [%s]",
987 catalog_hash.ToString().c_str());
988 ✗ retval = false;
989 }
990 }
991
992 ✗ delete catalog;
993 ✗ return retval;
994 }
995
996
997 ✗ int CommandCheck::Main(const swissknife::ArgumentList &args) {
998 ✗ string tag_name;
999 ✗ string subtree_path = "";
1000 ✗ string pubkey_path = "";
1001 ✗ string repo_name = "";
1002 ✗ string reflog_chksum_path = "";
1003
1004 ✗ temp_directory_ = (args.find('t') != args.end()) ? *args.find('t')->second
1005 ✗ : "/tmp";
1006 ✗ if (args.find('n') != args.end())
1007 ✗ tag_name = *args.find('n')->second;
1008 ✗ if (args.find('c') != args.end())
1009 ✗ check_chunks_ = true;
1010 ✗ if (args.find('d') != args.end())
1011 ✗ no_duplicates_map_ = true;
1012 ✗ if (args.find('l') != args.end()) {
1013 const unsigned log_level = kLogLevel0
1014 ✗ << String2Uint64(*args.find('l')->second);
1015 ✗ if (log_level > kLogNone) {
1016 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "invalid log level");
1017 ✗ return 1;
1018 }
1019 ✗ SetLogVerbosity(static_cast<LogLevels>(log_level));
1020 }
1021 ✗ if (args.find('k') != args.end())
1022 ✗ pubkey_path = *args.find('k')->second;
1023 ✗ if (DirectoryExists(pubkey_path))
1024 ✗ pubkey_path = JoinStrings(FindFilesBySuffix(pubkey_path, ".pub"), ":");
1025 ✗ if (args.find('N') != args.end())
1026 ✗ repo_name = *args.find('N')->second;
1027
1028 ✗ if (args.find('E') != args.end()) {
1029 ✗ inclusion_spec_ = catalog::InclusionSpec::Create(*args.find('E')->second);
1030 ✗ if (inclusion_spec_ == NULL || !inclusion_spec_->IsValid()) {
1031 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
1032 "Failed to parse inclusion spec from '%s'",
1033 args.find('E')->second->c_str());
1034 ✗ return 1;
1035 }
1036 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
1037 "Partial replication: will skip pruned (excluded) subtrees");
1038 }
1039
1040 ✗ repo_base_path_ = MakeCanonicalPath(*args.find('r')->second);
1041 ✗ if (args.find('s') != args.end())
1042 ✗ subtree_path = MakeCanonicalPath(*args.find('s')->second);
1043 ✗ if (args.find('R') != args.end())
1044 ✗ reflog_chksum_path = *args.find('R')->second;
1045
1046 // Repository can be HTTP address or on local file system
1047 ✗ is_remote_ = IsHttpUrl(repo_base_path_);
1048
1049 // initialize the (swissknife global) download and signature managers
1050 ✗ if (is_remote_) {
1051 ✗ const bool follow_redirects = (args.count('L') > 0);
1052 ✗ const string proxy = (args.count('@') > 0) ? *args.find('@')->second : "";
1053 ✗ if (!this->InitDownloadManager(follow_redirects, proxy)) {
1054 ✗ return 1;
1055 }
1056
1057 ✗ if (pubkey_path.empty() || repo_name.empty()) {
1058 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
1059 "please provide pubkey and repo name for "
1060 "remote repositories");
1061 ✗ return 1;
1062 }
1063
1064 ✗ if (!this->InitSignatureManager(pubkey_path)) {
1065 ✗ return 1;
1066 }
1067 }
1068
1069 // Load Manifest
1070 ✗ std::unique_ptr<manifest::Manifest> manifest;
1071 ✗ bool successful = true;
1072
1073 ✗ if (is_remote_) {
1074 ✗ manifest.reset(FetchRemoteManifest(repo_base_path_, repo_name));
1075 } else {
1076 ✗ if (chdir(repo_base_path_.c_str()) != 0) {
1077 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to switch to directory %s",
1078 repo_base_path_.c_str());
1079 ✗ return 1;
1080 }
1081 ✗ manifest.reset(OpenLocalManifest(".cvmfspublished"));
1082 }
1083
1084 ✗ if (manifest.get() == nullptr) {
1085 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to load repository manifest");
1086 ✗ return 1;
1087 }
1088
1089 // Check meta-info object
1090 ✗ if (!manifest->meta_info().IsNull()) {
1091 ✗ string tmp_file;
1092 ✗ if (!is_remote_)
1093 ✗ tmp_file = DecompressPiece(manifest->meta_info());
1094 else
1095 ✗ tmp_file = DownloadPiece(manifest->meta_info());
1096 ✗ if (tmp_file == "") {
1097 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to load repository metainfo %s",
1098 manifest->meta_info().ToString().c_str());
1099 ✗ return 1;
1100 }
1101 ✗ unlink(tmp_file.c_str());
1102 }
1103
1104 ✗ shash::Any reflog_hash;
1105 ✗ if (!reflog_chksum_path.empty()) {
1106 ✗ if (!manifest::Reflog::ReadChecksum(reflog_chksum_path, &reflog_hash)) {
1107 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to read reflog checksum file");
1108 ✗ return 1;
1109 }
1110 } else {
1111 ✗ reflog_hash = manifest->reflog_hash();
1112 }
1113
1114 ✗ if (Exists(".cvmfsreflog")) {
1115 ✗ if (reflog_hash.IsNull()) {
1116 // If there is a reflog, we want to check it
1117 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
1118 ".cvmfsreflog present but no checksum provided, aborting");
1119 ✗ return 1;
1120 }
1121 ✗ const bool retval = InspectReflog(reflog_hash, manifest.get());
1122 ✗ if (!retval) {
1123 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to verify reflog");
1124 ✗ return 1;
1125 }
1126 } else {
1127 ✗ if (!reflog_hash.IsNull()) {
1128 // There is a checksum but no reflog; possibly the checksum is for the
1129 // from the manifest for the stratum 0 reflog
1130 ✗ if (!reflog_chksum_path.empty()) {
1131 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
1132 "local reflog checksum set but reflog itself is missing, "
1133 "aborting");
1134 ✗ return 1;
1135 }
1136 }
1137 }
1138
1139 // Load history
1140 ✗ std::unique_ptr<history::History> tag_db;
1141 ✗ if (!manifest->history().IsNull()) {
1142 ✗ string tmp_file;
1143 ✗ if (!is_remote_)
1144 ✗ tmp_file = DecompressPiece(manifest->history());
1145 else
1146 ✗ tmp_file = DownloadPiece(manifest->history());
1147 ✗ if (tmp_file == "") {
1148 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to load history database %s",
1149 manifest->history().ToString().c_str());
1150 ✗ return 1;
1151 }
1152 ✗ tag_db.reset(history::SqliteHistory::Open(tmp_file));
1153 ✗ if (tag_db.get() == nullptr) {
1154 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to open history database %s",
1155 manifest->history().ToString().c_str());
1156 ✗ return 1;
1157 }
1158 ✗ tag_db->TakeDatabaseFileOwnership();
1159 ✗ successful = InspectHistory(tag_db.get()) && successful;
1160 }
1161
1162 ✗ if (manifest->has_alt_catalog_path()) {
1163 ✗ if (!Exists(manifest->certificate().MakeAlternativePath())) {
1164 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
1165 "failed to find alternative certificate link %s",
1166 manifest->certificate().MakeAlternativePath().c_str());
1167 ✗ return 1;
1168 }
1169 ✗ if (!Exists(manifest->catalog_hash().MakeAlternativePath())) {
1170 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
1171 "failed to find alternative catalog link %s",
1172 manifest->catalog_hash().MakeAlternativePath().c_str());
1173 ✗ return 1;
1174 }
1175 }
1176
1177 ✗ shash::Any root_hash = manifest->catalog_hash();
1178 ✗ uint64_t root_size = manifest->catalog_size();
1179 ✗ if (tag_name != "") {
1180 ✗ if (tag_db.get() == nullptr) {
1181 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "no history");
1182 ✗ return 1;
1183 }
1184 ✗ history::History::Tag tag;
1185 ✗ const bool retval = tag_db->GetByName(tag_name, &tag);
1186 ✗ if (!retval) {
1187 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "no such tag: %s", tag_name.c_str());
1188 ✗ return 1;
1189 }
1190 ✗ root_hash = tag.root_hash;
1191 ✗ root_size = tag.size;
1192 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Inspecting repository tag %s",
1193 tag_name.c_str());
1194 }
1195
1196 ✗ const bool is_nested_catalog = (!subtree_path.empty());
1197 ✗ if (is_nested_catalog
1198 ✗ && !FindSubtreeRootCatalog(subtree_path, &root_hash, &root_size)) {
1199 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "cannot find nested catalog at %s",
1200 subtree_path.c_str());
1201 ✗ return 1;
1202 }
1203
1204
1205 ✗ catalog::DeltaCounters computed_counters;
1206 ✗ successful = InspectTree(subtree_path,
1207 root_hash,
1208 root_size,
1209 is_nested_catalog,
1210 NULL,
1211 &computed_counters)
1212 ✗ && successful;
1213
1214 ✗ if (!successful) {
1215 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "CATALOG PROBLEMS OR OTHER ERRORS FOUND");
1216 ✗ return 1;
1217 }
1218
1219 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "no problems found");
1220 ✗ delete inclusion_spec_;
1221 ✗ inclusion_spec_ = NULL;
1222 ✗ return 0;
1223 }
1224
1225 } // namespace swissknife
1226