SemVer.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. /*
  2. * Copyright (c) 2023, Gurkirat Singh <tbhaxor@gmail.com>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <AK/CharacterTypes.h>
  7. #include <AK/Find.h>
  8. #include <AK/GenericLexer.h>
  9. #include <AK/ReverseIterator.h>
  10. #include <AK/StringBuilder.h>
  11. #include <LibSemVer/SemVer.h>
  12. namespace SemVer {
  13. String SemVer::suffix() const
  14. {
  15. StringBuilder sb;
  16. if (!m_prerelease_identifiers.is_empty())
  17. sb.appendff("-{}", prerelease());
  18. if (!m_build_metadata_identifiers.is_empty())
  19. sb.appendff("+{}", build_metadata());
  20. return sb.to_string().release_value_but_fixme_should_propagate_errors();
  21. }
  22. String SemVer::to_string() const
  23. {
  24. return String::formatted("{}{}{}{}{}{}", m_major, m_number_separator, m_minor, m_number_separator, m_patch, suffix()).release_value_but_fixme_should_propagate_errors();
  25. }
  26. SemVer SemVer::bump(BumpType type) const
  27. {
  28. switch (type) {
  29. case BumpType::Major:
  30. return SemVer(m_major + 1, 0, 0, m_number_separator);
  31. case BumpType::Minor:
  32. return SemVer(m_major, m_minor + 1, 0, m_number_separator);
  33. case BumpType::Patch:
  34. return SemVer(m_major, m_minor, m_patch + 1, m_number_separator);
  35. case BumpType::Prerelease: {
  36. Vector<String> prerelease_identifiers = m_prerelease_identifiers;
  37. bool is_found = false;
  38. // Unlike comparision, prerelease bumps take from RTL.
  39. for (auto& identifier : AK::ReverseWrapper::in_reverse(prerelease_identifiers)) {
  40. auto numeric_identifier = identifier.to_number<u32>();
  41. if (numeric_identifier.has_value()) {
  42. is_found = true;
  43. identifier = String::formatted("{}", numeric_identifier.value() + 1).release_value_but_fixme_should_propagate_errors();
  44. break;
  45. }
  46. }
  47. // Append 0 identifier if there is no numeric found to be bumped.
  48. if (!is_found)
  49. prerelease_identifiers.append("0"_string);
  50. return SemVer(m_major, m_minor, m_patch, m_number_separator, prerelease_identifiers, {});
  51. }
  52. default:
  53. VERIFY_NOT_REACHED();
  54. }
  55. }
  56. bool SemVer::is_same(SemVer const& other, CompareType compare_type) const
  57. {
  58. switch (compare_type) {
  59. case CompareType::Major:
  60. return m_major == other.m_major;
  61. case CompareType::Minor:
  62. return m_major == other.m_major && m_minor == other.m_minor;
  63. case CompareType::Patch:
  64. return m_major == other.m_major && m_minor == other.m_minor && m_patch == other.m_patch;
  65. default:
  66. // Build metadata MUST be ignored when determining version precedence.
  67. return m_major == other.m_major && m_minor == other.m_minor && m_patch == other.m_patch && prerelease() == other.prerelease();
  68. }
  69. }
  70. bool SemVer::is_greater_than(SemVer const& other) const
  71. {
  72. // Priortize the normal version string.
  73. // Precedence is determined by the first difference when comparing them from left to right.
  74. // Major > Minor > Patch
  75. if (m_major > other.m_major || m_minor > other.m_minor || m_patch > other.m_patch)
  76. return true;
  77. // When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version.
  78. // Example: 1.0.0-alpha < 1.0.0
  79. if (prerelease() == other.prerelease() || other.prerelease().is_empty())
  80. return false;
  81. if (prerelease().is_empty())
  82. return true;
  83. // Both the versions have non-zero length of pre-release identifiers.
  84. for (size_t i = 0; i < min(prerelease_identifiers().size(), other.prerelease_identifiers().size()); ++i) {
  85. auto const this_numerical_identifier = m_prerelease_identifiers[i].to_number<u32>();
  86. auto const other_numerical_identifier = other.m_prerelease_identifiers[i].to_number<u32>();
  87. // 1. Identifiers consisting of only digits are compared numerically.
  88. if (this_numerical_identifier.has_value() && other_numerical_identifier.has_value()) {
  89. auto const this_value = this_numerical_identifier.value();
  90. auto const other_value = other_numerical_identifier.value();
  91. if (this_value == other_value) {
  92. continue;
  93. }
  94. return this_value > other_value;
  95. }
  96. // 2. Identifiers with letters or hyphens are compared lexically in ASCII sort order.
  97. if (!this_numerical_identifier.has_value() && !other_numerical_identifier.has_value()) {
  98. if (m_prerelease_identifiers[i] == other.m_prerelease_identifiers[i]) {
  99. continue;
  100. }
  101. return m_prerelease_identifiers[i] > other.m_prerelease_identifiers[i];
  102. }
  103. // 3. Numeric identifiers always have lower precedence than non-numeric identifiers.
  104. if (this_numerical_identifier.has_value() && !other_numerical_identifier.has_value())
  105. return false;
  106. if (!this_numerical_identifier.has_value() && other_numerical_identifier.has_value())
  107. return true;
  108. }
  109. // 4. If all of the preceding identifiers are equal, larger set of pre-release fields has a higher precedence than a smaller set.
  110. return m_prerelease_identifiers.size() > other.m_prerelease_identifiers.size();
  111. }
  112. bool SemVer::satisfies(StringView const& semver_spec) const
  113. {
  114. GenericLexer lexer(semver_spec.trim_whitespace());
  115. if (lexer.tell_remaining() == 0)
  116. return false;
  117. auto compare_op = lexer.consume_until([](auto const& ch) { return ch >= '0' && ch <= '9'; });
  118. auto spec_version = MUST(from_string_view(lexer.consume_all()));
  119. // Lenient compare, tolerance for any patch and pre-release.
  120. if (compare_op.is_empty())
  121. return is_same(spec_version, CompareType::Minor);
  122. if (compare_op == "!="sv)
  123. return !is_same(spec_version);
  124. // Adds strictness based on number of equal sign.
  125. if (compare_op == "="sv)
  126. return is_same(spec_version, CompareType::Patch);
  127. // Exact version string match.
  128. if (compare_op == "=="sv)
  129. return is_same(spec_version);
  130. // Current version is greater than spec.
  131. if (compare_op == ">"sv)
  132. return is_greater_than(spec_version);
  133. if (compare_op == "<"sv)
  134. return is_lesser_than(spec_version);
  135. if (compare_op == ">="sv)
  136. return is_same(spec_version) || is_greater_than(spec_version);
  137. if (compare_op == "<="sv)
  138. return is_same(spec_version) || !is_greater_than(spec_version);
  139. return false;
  140. }
  141. ErrorOr<SemVer> from_string_view(StringView const& version, char normal_version_separator)
  142. {
  143. if (is_ascii_space(normal_version_separator) || is_ascii_digit(normal_version_separator)) {
  144. return Error::from_string_view("Version separator can't be a space or digit character"sv);
  145. }
  146. if (version.count(normal_version_separator) < 2)
  147. return Error::from_string_view("Insufficient occurrences of version separator"sv);
  148. if (version.count('+') > 1)
  149. return Error::from_string_view("Build metadata must be defined at most once"sv);
  150. // Checks for the bad charaters
  151. // Spec: https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions
  152. auto trimmed_version = version.trim_whitespace();
  153. for (auto const& code_point : trimmed_version.bytes()) {
  154. if (is_ascii_space(code_point) || code_point == '_') {
  155. return Error::from_string_view("Bad characters found in the version string"sv);
  156. }
  157. }
  158. GenericLexer lexer(trimmed_version);
  159. if (lexer.tell_remaining() == 0)
  160. return Error::from_string_view("Version string is empty"sv);
  161. // Parse the normal version parts.
  162. // https://semver.org/#spec-item-2
  163. auto version_part = lexer.consume_until(normal_version_separator).to_number<u64>();
  164. if (!version_part.has_value())
  165. return Error::from_string_view("Major version is not numeric"sv);
  166. auto version_major = version_part.value();
  167. lexer.consume();
  168. version_part = lexer.consume_until(normal_version_separator).to_number<u64>();
  169. if (!version_part.has_value())
  170. return Error::from_string_view("Minor version is not numeric"sv);
  171. auto version_minor = version_part.value();
  172. lexer.consume();
  173. version_part = lexer.consume_while([](char ch) { return ch >= '0' && ch <= '9'; }).to_number<u64>();
  174. if (!version_part.has_value())
  175. return Error::from_string_view("Patch version is not numeric"sv);
  176. auto version_patch = version_part.value();
  177. if (lexer.is_eof())
  178. return SemVer(version_major, version_minor, version_patch, normal_version_separator);
  179. Vector<String> build_metadata_identifiers;
  180. Vector<String> prerelease_identifiers;
  181. auto process_build_metadata = [&lexer, &build_metadata_identifiers]() -> ErrorOr<void> {
  182. // Function body strictly adheres to the spec
  183. // Spec: https://semver.org/#spec-item-10
  184. if (lexer.is_eof()) {
  185. return Error::from_string_view("Build metadata can't be empty"sv);
  186. }
  187. auto build_metadata = TRY(String::from_utf8(lexer.consume_all()));
  188. build_metadata_identifiers = TRY(build_metadata.split('.'));
  189. // Because there is no mention about leading zero in the spec, only empty check is used
  190. for (auto& identifier : build_metadata_identifiers) {
  191. if (identifier.is_empty()) {
  192. return Error::from_string_view("Build metadata identifier must be non empty string"sv);
  193. }
  194. }
  195. return {};
  196. };
  197. switch (lexer.consume()) {
  198. case '+': {
  199. // Build metadata always starts with the + symbol after normal version string.
  200. TRY(process_build_metadata());
  201. break;
  202. }
  203. case '-': {
  204. // Pre-releases always start with the - symbol after normal version string.
  205. // Spec: https://semver.org/#spec-item-9
  206. if (lexer.is_eof())
  207. return Error::from_string_view("Pre-release can't be empty"sv);
  208. auto prerelease = TRY(String::from_utf8(lexer.consume_until('+')));
  209. constexpr auto is_valid_identifier = [](String const& identifier) {
  210. for (auto const& code_point : identifier.code_points()) {
  211. if (!is_ascii_alphanumeric(code_point) && code_point != '-') {
  212. return false;
  213. }
  214. }
  215. return true;
  216. };
  217. // Parts of prerelease (identitifers) are separated by dot (.)
  218. prerelease_identifiers = TRY(prerelease.split('.'));
  219. for (auto const& prerelease_identifier : prerelease_identifiers) {
  220. // Empty identifiers are not allowed.
  221. if (prerelease_identifier.is_empty())
  222. return Error::from_string_view("Prerelease identifier can't be empty"sv);
  223. // If there are multiple digits, it can't start with 0 digit.
  224. // 1.2.3-0 or 1.2.3-0is.legal are valid, but not 1.2.3-00 or 1.2.3-01
  225. auto identifier_bytes = prerelease_identifier.bytes();
  226. if (identifier_bytes.size() > 1 && prerelease_identifier.starts_with('0') && is_ascii_digit(identifier_bytes[1]))
  227. return Error::from_string_view("Prerelease identifier has leading redundant zeroes"sv);
  228. // Validate identifier against charset
  229. if (!is_valid_identifier(prerelease_identifier))
  230. return Error::from_string_view("Characters in prerelease identifier must be either hyphen (-), dot (.) or alphanumeric"sv);
  231. }
  232. if (!lexer.is_eof()) {
  233. // This would invalidate the following versions.
  234. // 1.2.3-pre$ss 1.2.3-pre.1.0*build-meta
  235. if (lexer.consume() != '+') {
  236. return Error::from_string_view("After processing pre-release, only + character is allowed for build metadata information"sv);
  237. }
  238. // Process the pending build metadata information, ignoring invalids like following.
  239. // 1.2.3-pre+ is not a valid version.
  240. TRY(process_build_metadata());
  241. }
  242. break;
  243. }
  244. default:
  245. // TODO: Add context information like actual character (peek) and its index, use the following format.
  246. // "Expected prerelease (-) or build metadata (+) character at {}. Found {}"
  247. return Error::from_string_view("Malformed version syntax. Expected + or - characters"sv);
  248. }
  249. return SemVer(version_major, version_minor, version_patch, normal_version_separator, prerelease_identifiers, build_metadata_identifiers);
  250. }
  251. bool is_valid(StringView const& version, char normal_version_separator)
  252. {
  253. auto result = from_string_view(version, normal_version_separator);
  254. return !result.is_error() && result.release_value().to_string() == version;
  255. }
  256. }