Apache Mesos
version.hpp
Go to the documentation of this file.
1 // Licensed under the Apache License, Version 2.0 (the "License");
2 // you may not use this file except in compliance with the License.
3 // You may obtain a copy of the License at
4 //
5 // http://www.apache.org/licenses/LICENSE-2.0
6 //
7 // Unless required by applicable law or agreed to in writing, software
8 // distributed under the License is distributed on an "AS IS" BASIS,
9 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 // See the License for the specific language governing permissions and
11 // limitations under the License.
12 
13 #ifndef __STOUT_VERSION_HPP__
14 #define __STOUT_VERSION_HPP__
15 
16 #include <algorithm>
17 #include <cctype>
18 #include <cstdint>
19 #include <ostream>
20 #include <string>
21 #include <vector>
22 
23 #include <stout/check.hpp>
24 #include <stout/error.hpp>
25 #include <stout/numify.hpp>
26 #include <stout/option.hpp>
27 #include <stout/stringify.hpp>
28 #include <stout/strings.hpp>
29 #include <stout/try.hpp>
30 
31 // This class provides convenience routines for working with version
32 // numbers. We support the SemVer 2.0.0 (http://semver.org) format,
33 // with two differences:
34 //
35 // (1) Numeric components with leading zeros are allowed.
36 //
37 // (2) Missing version components are allowed and treated as zero.
38 //
39 // TODO(neilc): Consider providing a "strict" variant that does not
40 // allow these extensions.
41 struct Version
42 {
43  // Expect the string in the following format:
44  // <major>[.<minor>[.<patch>]][-prerelease][+build]
45  //
46  // Missing `minor` or `patch` components are treated as zero.
47  //
48  // `prerelease` is a prerelease label (e.g., "beta", "rc.1");
49  // `build` is a build metadata label. Both `prerelease` and `build`
50  // consist of one or more dot-separated identifiers. An identifier
51  // is a non-empty string containing ASCII alphanumeric characters or
52  // hyphens.
53  //
54  // Ideally, the version components would be called simply "major",
55  // "minor", and "patch". However, GNU libstdc++ already defines
56  // these as macros for compatibility reasons (man 3 makedev for more
57  // information) implicitly included in every compilation.
58  static Try<Version> parse(const std::string& input)
59  {
60  // The input string consists of the numeric components, optionally
61  // followed by the prerelease label (prefixed with '-') and/or the
62  // build label (prefixed with '+'). We parse the string from right
63  // to left: build label (if any), prerelease label (if any), and
64  // finally numeric components.
65 
66  std::vector<std::string> buildLabel;
67 
68  std::vector<std::string> buildParts = strings::split(input, "+", 2);
69  CHECK(buildParts.size() == 1 || buildParts.size() == 2);
70 
71  if (buildParts.size() == 2) {
72  const std::string& buildString = buildParts.back();
73 
74  Try<std::vector<std::string>> parsed = parseLabel(buildString);
75  if (parsed.isError()) {
76  return Error("Invalid build label: " + parsed.error());
77  }
78 
79  buildLabel = parsed.get();
80  }
81 
82  std::string remainder = buildParts.front();
83 
84  // Parse the prerelease label, if any. Note that the prerelease
85  // label might itself contain hyphens.
86  std::vector<std::string> prereleaseLabel;
87 
88  std::vector<std::string> prereleaseParts =
89  strings::split(remainder, "-", 2);
90  CHECK(prereleaseParts.size() == 1 || prereleaseParts.size() == 2);
91 
92  if (prereleaseParts.size() == 2) {
93  const std::string& prereleaseString = prereleaseParts.back();
94 
95  Try<std::vector<std::string>> parsed = parseLabel(prereleaseString);
96  if (parsed.isError()) {
97  return Error("Invalid prerelease label: " + parsed.error());
98  }
99 
100  prereleaseLabel = parsed.get();
101  }
102 
103  remainder = prereleaseParts.front();
104 
105  constexpr size_t maxNumericComponents = 3;
106  std::vector<std::string> numericComponents = strings::split(remainder, ".");
107 
108  if (numericComponents.size() > maxNumericComponents) {
109  return Error("Version has " + stringify(numericComponents.size()) +
110  " components; maximum " + stringify(maxNumericComponents) +
111  " components allowed");
112  }
113 
114  uint32_t versionNumbers[maxNumericComponents] = {0};
115 
116  for (size_t i = 0; i < numericComponents.size(); i++) {
117  Try<uint32_t> result = parseNumericIdentifier(numericComponents[i]);
118  if (result.isError()) {
119  return Error("Invalid version component '" + numericComponents[i] + "'"
120  ": " + result.error());
121  }
122 
123  versionNumbers[i] = result.get();
124  }
125 
126  return Version(versionNumbers[0],
127  versionNumbers[1],
128  versionNumbers[2],
129  prereleaseLabel,
130  buildLabel);
131  }
132 
133  // Construct a new Version. The `_prerelease` and `_build` arguments
134  // contain lists of prerelease and build identifiers, respectively.
135  Version(uint32_t _majorVersion,
136  uint32_t _minorVersion,
137  uint32_t _patchVersion,
138  const std::vector<std::string>& _prerelease = {},
139  const std::vector<std::string>& _build = {})
140  : majorVersion(_majorVersion),
141  minorVersion(_minorVersion),
142  patchVersion(_patchVersion),
143  prerelease(_prerelease),
144  build(_build)
145  {
146  // As a sanity check, ensure that the caller has provided
147  // valid prerelease and build identifiers.
148 
149  foreach (const std::string& identifier, prerelease) {
150  CHECK_NONE(validateIdentifier(identifier));
151  }
152 
153  foreach (const std::string& identifier, build) {
154  CHECK_NONE(validateIdentifier(identifier));
155  }
156  }
157 
158  bool operator==(const Version& other) const
159  {
160  // NOTE: The `build` field is ignored when comparing two versions
161  // for equality, per SemVer spec.
162  return majorVersion == other.majorVersion &&
163  minorVersion == other.minorVersion &&
164  patchVersion == other.patchVersion &&
165  prerelease == other.prerelease;
166  }
167 
168  bool operator!=(const Version& other) const
169  {
170  return !(*this == other);
171  }
172 
173  // SemVer 2.0.0 defines version precedence (ordering) like so:
174  //
175  // Precedence MUST be calculated by separating the version into
176  // major, minor, patch and pre-release identifiers in that order
177  // (Build metadata does not figure into precedence). Precedence is
178  // determined by the first difference when comparing each of these
179  // identifiers from left to right as follows: Major, minor, and
180  // patch versions are always compared numerically. Example: 1.0.0
181  // < 2.0.0 < 2.1.0 < 2.1.1. When major, minor, and patch are
182  // equal, a pre-release version has lower precedence than a normal
183  // version. Example: 1.0.0-alpha < 1.0.0. Precedence for two
184  // pre-release versions with the same major, minor, and patch
185  // version MUST be determined by comparing each dot separated
186  // identifier from left to right until a difference is found as
187  // follows: identifiers consisting of only digits are compared
188  // numerically and identifiers with letters or hyphens are
189  // compared lexically in ASCII sort order. Numeric identifiers
190  // always have lower precedence than non-numeric identifiers. A
191  // larger set of pre-release fields has a higher precedence than a
192  // smaller set, if all of the preceding identifiers are equal.
193  // Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta <
194  // 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.
195  //
196  // NOTE: The `build` field is ignored when comparing two versions
197  // for precedence, per the SemVer spec text above.
198  bool operator<(const Version& other) const
199  {
200  // Compare version numbers numerically.
201  if (majorVersion != other.majorVersion) {
202  return majorVersion < other.majorVersion;
203  }
204  if (minorVersion != other.minorVersion) {
205  return minorVersion < other.minorVersion;
206  }
207  if (patchVersion != other.patchVersion) {
208  return patchVersion < other.patchVersion;
209  }
210 
211  // If one version has a prerelease label and the other does not,
212  // the prerelease version has lower precedence.
213  if (!prerelease.empty() && other.prerelease.empty()) {
214  return true;
215  }
216  if (prerelease.empty() && !other.prerelease.empty()) {
217  return false;
218  }
219 
220  // Compare two versions with prerelease labels by proceeding from
221  // left to right.
222  size_t minPrereleaseSize = std::min(
223  prerelease.size(), other.prerelease.size());
224 
225  for (size_t i = 0; i < minPrereleaseSize; i++) {
226  // Check whether the two prerelease identifiers can be converted
227  // to numbers.
228  Try<uint32_t> identifier = parseNumericIdentifier(prerelease.at(i));
229  Try<uint32_t> otherIdentifier =
230  parseNumericIdentifier(other.prerelease.at(i));
231 
232  if (identifier.isSome() && otherIdentifier.isSome()) {
233  // Both identifiers are numeric.
234  if (identifier.get() != otherIdentifier.get()) {
235  return identifier.get() < otherIdentifier.get();
236  }
237  } else if (identifier.isSome()) {
238  // `identifier` is numeric but `otherIdentifier` is not, so
239  // `identifier` comes first.
240  return true;
241  } else if (otherIdentifier.isSome()) {
242  // `otherIdentifier` is numeric but `identifier` is not, so
243  // `otherIdentifier` comes first.
244  return false;
245  } else {
246  // Neither identifier is numeric, so compare via ASCII sort.
247  if (prerelease.at(i) != other.prerelease.at(i)) {
248  return prerelease.at(i) < other.prerelease.at(i);
249  }
250  }
251  }
252 
253  // If two versions have different numbers of prerelease labels but
254  // they match on the common prefix, the version with the smaller
255  // set of labels comes first.
256  return prerelease.size() < other.prerelease.size();
257  }
258 
259  bool operator>(const Version& other) const
260  {
261  return other < *this;
262  }
263 
264  bool operator<=(const Version& other) const
265  {
266  return *this < other || *this == other;
267  }
268 
269  bool operator>=(const Version& other) const
270  {
271  return *this > other || *this == other;
272  }
273 
274  friend inline std::ostream& operator<<(
275  std::ostream& stream,
276  const Version& version);
277 
278  const uint32_t majorVersion;
279  const uint32_t minorVersion;
280  const uint32_t patchVersion;
281  const std::vector<std::string> prerelease;
282  const std::vector<std::string> build;
283 
284 private:
285  // Check that a string contains a valid identifier. An identifier is
286  // a non-empty string; each character must be an ASCII alphanumeric
287  // or hyphen. We allow leading zeros in numeric identifiers, which
288  // inconsistent with the SemVer spec.
289  static Option<Error> validateIdentifier(const std::string& identifier)
290  {
291  if (identifier.empty()) {
292  return Error("Empty identifier");
293  }
294 
295  auto alphaNumericOrHyphen = [](unsigned char c) -> bool {
296  return std::isalnum(c) || c == '-';
297  };
298 
299  auto firstInvalid = std::find_if_not(
300  identifier.begin(), identifier.end(), alphaNumericOrHyphen);
301 
302  if (firstInvalid != identifier.end()) {
303  return Error("Identifier contains illegal character: "
304  "'" + stringify(*firstInvalid) + "'");
305  }
306 
307  return None();
308  }
309 
310  // Parse a string containing a series of dot-separated identifiers
311  // into a vector of strings; each element of the vector contains a
312  // single identifier.
313  static Try<std::vector<std::string>> parseLabel(const std::string& label)
314  {
315  if (label.empty()) {
316  return Error("Empty label");
317  }
318 
319  std::vector<std::string> identifiers = strings::split(label, ".");
320 
321  foreach (const std::string& identifier, identifiers) {
322  Option<Error> error = validateIdentifier(identifier);
323  if (error.isSome()) {
324  return error.get();
325  }
326  }
327 
328  return identifiers;
329  }
330 
331  // Try to parse the given string as a numeric identifier. According
332  // to the SemVer spec, identifiers that begin with hyphens are
333  // considered non-numeric.
334  //
335  // TODO(neilc): Consider adding a variant of `numify<T>` that only
336  // supports non-negative inputs.
337  static Try<uint32_t> parseNumericIdentifier(const std::string& identifier) {
338  if (strings::startsWith(identifier, '-')) {
339  return Error("Contains leading hyphen");
340  }
341 
342  return numify<uint32_t>(identifier);
343  }
344 };
345 
346 
347 inline std::ostream& operator<<(
348  std::ostream& stream,
349  const Version& version)
350 {
351  stream << version.majorVersion << "."
352  << version.minorVersion << "."
353  << version.patchVersion;
354 
355  if (!version.prerelease.empty()) {
356  stream << "-" << strings::join(".", version.prerelease);
357  }
358 
359  if (!version.build.empty()) {
360  stream << "+" << strings::join(".", version.build);
361  }
362 
363  return stream;
364 }
365 
366 #endif // __STOUT_VERSION_HPP__
const std::vector< std::string > prerelease
Definition: version.hpp:281
Definition: errorbase.hpp:36
T & get()&
Definition: try.hpp:80
bool operator>(const Version &other) const
Definition: version.hpp:259
const uint32_t majorVersion
Definition: version.hpp:278
std::stringstream & join(std::stringstream &stream, const std::string &separator, T &&...args)
Definition: strings.hpp:307
Definition: check.hpp:33
const uint32_t patchVersion
Definition: version.hpp:280
Version(uint32_t _majorVersion, uint32_t _minorVersion, uint32_t _patchVersion, const std::vector< std::string > &_prerelease={}, const std::vector< std::string > &_build={})
Definition: version.hpp:135
#define CHECK_NONE(expression)
Definition: check.hpp:54
JSON::Object version()
Definition: version.hpp:32
bool operator>=(const Version &other) const
Definition: version.hpp:269
static Try< Version > parse(const std::string &input)
Definition: version.hpp:58
bool isSome() const
Definition: option.hpp:116
bool operator<(const Version &other) const
Definition: version.hpp:198
Option< T > min(const Option< T > &left, const Option< T > &right)
Definition: option.hpp:185
bool isSome() const
Definition: try.hpp:77
const T & get() const &
Definition: option.hpp:119
bool operator<=(const Version &other) const
Definition: version.hpp:264
static Try error(const E &e)
Definition: try.hpp:43
friend std::ostream & operator<<(std::ostream &stream, const Version &version)
Definition: version.hpp:347
Definition: none.hpp:27
bool isError() const
Definition: try.hpp:78
const uint32_t minorVersion
Definition: version.hpp:279
std::string error(const std::string &msg, uint32_t code)
bool operator==(const Version &other) const
Definition: version.hpp:158
Definition: version.hpp:41
std::vector< std::string > split(const std::string &s, const std::string &delims, const Option< size_t > &maxTokens=None())
Definition: strings.hpp:184
bool operator!=(const Version &other) const
Definition: version.hpp:168
std::string stringify(int flags)
bool startsWith(const std::string &s, const std::string &prefix)
Definition: strings.hpp:381
const std::vector< std::string > build
Definition: version.hpp:282