| Directory: | cvmfs/ |
|---|---|
| File: | cvmfs/repository_tag.cc |
| Date: | 2026-06-28 02:36:10 |
| Exec | Total | Coverage | |
|---|---|---|---|
| Lines: | 53 | 54 | 98.1% |
| Branches: | 55 | 74 | 74.3% |
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * This file is part of the CernVM File System. | ||
| 3 | */ | ||
| 4 | |||
| 5 | #include "repository_tag.h" | ||
| 6 | |||
| 7 | #include <cctype> | ||
| 8 | #include <string> | ||
| 9 | |||
| 10 | #include "util/platform.h" | ||
| 11 | #include "util/string.h" | ||
| 12 | |||
| 13 | 314 | RepositoryTag::RepositoryTag(const std::string &name, | |
| 14 | 314 | const std::string &description) | |
| 15 | 314 | : name_(name) | |
| 16 |
1/2✓ Branch 1 taken 314 times.
✗ Branch 2 not taken.
|
314 | , description_(description) |
| 17 |
1/2✓ Branch 2 taken 314 times.
✗ Branch 3 not taken.
|
314 | , delete_tags_("") |
| 18 | 314 | , auto_tag_threshold_(0) { } | |
| 19 | |||
| 20 | /** | ||
| 21 | * Check if tag name is of the form "generic-*" | ||
| 22 | */ | ||
| 23 | 5 | bool RepositoryTag::HasGenericName() { | |
| 24 |
2/4✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 5 times.
✗ Branch 6 not taken.
|
5 | return HasPrefix(name_, "generic-", false); |
| 25 | } | ||
| 26 | |||
| 27 | namespace { | ||
| 28 | |||
| 29 | // True if `s` is exactly "YYYY-MM-DDTHH:MM:SS(.fff)?Z" (1-3 fractional digits), | ||
| 30 | // the timestamp shape produced by RepositoryTag::SetGenericName() and by the | ||
| 31 | // `date -u +%Y-%m-%dT%H:%M:%SZ` call in cvmfs_server_publish.sh. Only the digit | ||
| 32 | // and separator structure is checked, matching the regular expression in | ||
| 33 | // filter_auto_tags() (field ranges are not validated there either). | ||
| 34 | 16 | bool IsIso8601UtcTimestamp(const std::string &s) { | |
| 35 | // "YYYY-MM-DDTHH:MM:SSZ" without fractional seconds is 20 characters. | ||
| 36 |
2/2✓ Branch 1 taken 4 times.
✓ Branch 2 taken 12 times.
|
16 | if (s.size() < 20) { |
| 37 | 4 | return false; | |
| 38 | } | ||
| 39 |
7/10✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 11 times.
✓ Branch 8 taken 1 times.
✓ Branch 10 taken 11 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1 times.
✓ Branch 13 taken 11 times.
|
23 | if (s[4] != '-' || s[7] != '-' || s[10] != 'T' || s[13] != ':' || |
| 40 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
|
11 | s[16] != ':') { |
| 41 | 1 | return false; | |
| 42 | } | ||
| 43 | 11 | const size_t digit_pos[] = {0, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18}; | |
| 44 |
2/2✓ Branch 0 taken 144 times.
✓ Branch 1 taken 10 times.
|
154 | for (size_t k = 0; k < sizeof(digit_pos) / sizeof(digit_pos[0]); ++k) { |
| 45 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 143 times.
|
144 | if (!isdigit(static_cast<unsigned char>(s[digit_pos[k]]))) { |
| 46 | 1 | return false; | |
| 47 | } | ||
| 48 | } | ||
| 49 | // Optional ".fff" (1-3 digits) follows the seconds. | ||
| 50 | 10 | size_t pos = 19; | |
| 51 |
2/2✓ Branch 1 taken 6 times.
✓ Branch 2 taken 4 times.
|
10 | if (s[pos] == '.') { |
| 52 | 6 | ++pos; | |
| 53 | 6 | size_t fractional = 0; | |
| 54 |
6/6✓ Branch 1 taken 20 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 17 times.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 15 times.
✓ Branch 6 taken 6 times.
|
38 | while (pos < s.size() && fractional < 3 && |
| 55 |
2/2✓ Branch 1 taken 15 times.
✓ Branch 2 taken 2 times.
|
17 | isdigit(static_cast<unsigned char>(s[pos]))) { |
| 56 | 15 | ++pos; | |
| 57 | 15 | ++fractional; | |
| 58 | } | ||
| 59 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
|
6 | if (fractional == 0) { |
| 60 | ✗ | return false; // a '.' must be followed by at least one digit | |
| 61 | } | ||
| 62 | } | ||
| 63 | // The timestamp must end exactly with the 'Z' UTC designator. | ||
| 64 |
3/4✓ Branch 1 taken 7 times.
✓ Branch 2 taken 3 times.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
|
10 | return pos == s.size() - 1 && s[pos] == 'Z'; |
| 65 | } | ||
| 66 | |||
| 67 | } // anonymous namespace | ||
| 68 | |||
| 69 | /** | ||
| 70 | * True if `name` is a full auto-generated tag name: "generic-<ISO8601>" or, | ||
| 71 | * on name clashes, "generic_<n>-<ISO8601>", e.g. | ||
| 72 | * "generic-2024-01-01T00:00:00.000Z" (see SetGenericName() and the generic tag | ||
| 73 | * handling in cvmfs_server_publish.sh). The "<ISO8601>" part must be a complete | ||
| 74 | * "YYYY-MM-DDTHH:MM:SS(.mmm)?Z" timestamp, so user-created tags such as | ||
| 75 | * "generic-release" or "generic-2024-Z" are not matched and never deleted by | ||
| 76 | * the auto-tag cleanup. This mirrors the regular expression used by the | ||
| 77 | * non-gateway cleanup in filter_auto_tags(). | ||
| 78 | */ | ||
| 79 | 22 | bool RepositoryTag::IsAutoGeneratedName(const std::string &name) { | |
| 80 | 22 | std::string rest; | |
| 81 |
4/6✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 22 times.
✗ Branch 6 not taken.
✓ Branch 9 taken 14 times.
✓ Branch 10 taken 8 times.
|
22 | if (HasPrefix(name, "generic-", false)) { |
| 82 |
1/2✓ Branch 1 taken 14 times.
✗ Branch 2 not taken.
|
14 | rest = name.substr(8); // strlen("generic-") |
| 83 |
4/6✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 8 times.
✗ Branch 6 not taken.
✓ Branch 9 taken 5 times.
✓ Branch 10 taken 3 times.
|
8 | } else if (HasPrefix(name, "generic_", false)) { |
| 84 | // Skip the numeric disambiguation suffix and the following '-'. | ||
| 85 | 5 | size_t pos = 8; // strlen("generic_") | |
| 86 | 5 | const size_t start = pos; | |
| 87 |
4/4✓ Branch 1 taken 7 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 5 times.
|
15 | while (pos < name.size() && |
| 88 |
2/2✓ Branch 1 taken 3 times.
✓ Branch 2 taken 4 times.
|
7 | isdigit(static_cast<unsigned char>(name[pos]))) { |
| 89 | 3 | ++pos; | |
| 90 | } | ||
| 91 |
6/8✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2 times.
✓ Branch 8 taken 3 times.
✓ Branch 9 taken 2 times.
|
5 | if (pos == start || pos >= name.size() || name[pos] != '-') { |
| 92 | 3 | return false; | |
| 93 | } | ||
| 94 |
1/2✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
|
2 | rest = name.substr(pos + 1); |
| 95 | } else { | ||
| 96 | 3 | return false; | |
| 97 | } | ||
| 98 | |||
| 99 | 16 | return IsIso8601UtcTimestamp(rest); | |
| 100 | 22 | } | |
| 101 | |||
| 102 | /** | ||
| 103 | * Set a generic tag name of the form "generic-YYYY-MM-DDThh:mm:ss.sssZ" | ||
| 104 | */ | ||
| 105 | 1 | void RepositoryTag::SetGenericName() { | |
| 106 | 1 | const uint64_t nanoseconds = platform_realtime_ns(); | |
| 107 | |||
| 108 | // Use strftime() to format timestamp to one-second resolution | ||
| 109 | 1 | const time_t seconds = static_cast<time_t>(nanoseconds / 1000000000); | |
| 110 | struct tm timestamp; | ||
| 111 | 1 | gmtime_r(&seconds, ×tamp); | |
| 112 | char seconds_buffer[32]; | ||
| 113 | 1 | strftime(seconds_buffer, sizeof(seconds_buffer), "generic-%Y-%m-%dT%H:%M:%S", | |
| 114 | ×tamp); | ||
| 115 | |||
| 116 | // Append milliseconds | ||
| 117 | 1 | const unsigned offset_milliseconds = ((nanoseconds / 1000000) % 1000); | |
| 118 | char name_buffer[48]; | ||
| 119 | 1 | snprintf(name_buffer, sizeof(name_buffer), "%s.%03dZ", seconds_buffer, | |
| 120 | offset_milliseconds); | ||
| 121 | |||
| 122 |
1/2✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
1 | name_ = std::string(name_buffer); |
| 123 | 1 | } | |
| 124 |