diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/fx_ver.cpp b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/fx_ver.cpp index 63183adf01..65ae380c27 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/fx_ver.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/fx_ver.cpp @@ -1,11 +1,31 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +#include +#include #include "fx_ver.h" -#include -#include -#include +static bool validIdentifiers(const std::wstring& ids); + +size_t index_of_non_numeric(const std::wstring& str, size_t i) +{ + return str.find_first_not_of(TEXT("0123456789"), i); +} + +bool try_stou(const std::wstring& str, unsigned* num) +{ + if (str.empty()) + { + return false; + } + if (index_of_non_numeric(str, 0) != std::wstring::npos) + { + return false; + } + *num = (unsigned)std::stoul(str); + return true; +} fx_ver_t::fx_ver_t(int major, int minor, int patch, const std::wstring& pre, const std::wstring& build) : m_major(major) @@ -14,9 +34,15 @@ fx_ver_t::fx_ver_t(int major, int minor, int patch, const std::wstring& pre, con , m_pre(pre) , m_build(build) { + // verify preconditions + assert(is_empty() || m_major >= 0); + assert(is_empty() || m_minor >= 0); + assert(is_empty() || m_patch >= 0); + assert(m_pre[0] == 0 || validIdentifiers(m_pre)); + assert(m_build[0] == 0 || validIdentifiers(m_build)); } -fx_ver_t::fx_ver_t(int major, int minor, int patch, const std::wstring& pre) +fx_ver_t::fx_ver_t(int major, int minor, int patch, const std::wstring & pre) : fx_ver_t(major, minor, patch, pre, TEXT("")) { } @@ -26,32 +52,37 @@ fx_ver_t::fx_ver_t(int major, int minor, int patch) { } -bool fx_ver_t::operator ==(const fx_ver_t& b) const +fx_ver_t::fx_ver_t() + : fx_ver_t(-1, -1, -1, TEXT(""), TEXT("")) +{ +} + +bool fx_ver_t::operator ==(const fx_ver_t & b) const { return compare(*this, b) == 0; } -bool fx_ver_t::operator !=(const fx_ver_t& b) const +bool fx_ver_t::operator !=(const fx_ver_t & b) const { return !operator ==(b); } -bool fx_ver_t::operator <(const fx_ver_t& b) const +bool fx_ver_t::operator <(const fx_ver_t & b) const { return compare(*this, b) < 0; } -bool fx_ver_t::operator >(const fx_ver_t& b) const +bool fx_ver_t::operator >(const fx_ver_t & b) const { return compare(*this, b) > 0; } -bool fx_ver_t::operator <=(const fx_ver_t& b) const +bool fx_ver_t::operator <=(const fx_ver_t & b) const { return compare(*this, b) <= 0; } -bool fx_ver_t::operator >=(const fx_ver_t& b) const +bool fx_ver_t::operator >=(const fx_ver_t & b) const { return compare(*this, b) >= 0; } @@ -66,13 +97,34 @@ std::wstring fx_ver_t::as_str() const } if (!m_build.empty()) { - stream << TEXT("+") << m_build; + stream << m_build; } return stream.str(); } +std::wstring fx_ver_t::prerelease_glob() const +{ + std::wstringstream stream; + stream << m_major << TEXT(".") << m_minor << TEXT(".") << m_patch << TEXT("-*"); + return stream.str(); +} + +std::wstring fx_ver_t::patch_glob() const +{ + std::wstringstream stream; + stream << m_major << TEXT(".") << m_minor << TEXT(".*"); + return stream.str(); +} + +static std::wstring getId(const std::wstring & ids, size_t idStart) +{ + size_t next = ids.find(TEXT('.'), idStart); + + return next == std::wstring::npos ? ids.substr(idStart) : ids.substr(idStart, next - idStart); +} + /* static */ -int fx_ver_t::compare(const fx_ver_t&a, const fx_ver_t& b) +int fx_ver_t::compare(const fx_ver_t & a, const fx_ver_t & b) { // compare(u.v.w-p+b, x.y.z-q+c) if (a.m_major != b.m_major) @@ -90,37 +142,166 @@ int fx_ver_t::compare(const fx_ver_t&a, const fx_ver_t& b) return (a.m_patch > b.m_patch) ? 1 : -1; } - if (a.m_pre.empty() != b.m_pre.empty()) + if (a.m_pre.empty() || b.m_pre.empty()) { - // Either a is empty or b is empty - return a.m_pre.empty() ? 1 : -1; + // Either a is empty or b is empty or both are empty + return a.m_pre.empty() ? !b.m_pre.empty() : -1; } - // Either both are empty or both are non-empty (may be equal) - int pre_cmp = a.m_pre.compare(b.m_pre); - if (pre_cmp != 0) + // Both are non-empty (may be equal) + + // First character of pre is '-' when it is not empty + assert(a.m_pre[0] == TEXT('-')); + assert(b.m_pre[0] == TEXT('-')); + + // First idenitifier starts at position 1 + size_t idStart = 1; + for (size_t i = idStart; true; ++i) { - return pre_cmp; + if (a.m_pre[i] != b.m_pre[i]) + { + // Found first character with a difference + if (a.m_pre[i] == 0 && b.m_pre[i] == TEXT('.')) + { + // identifiers both complete, b has an additional idenitifier + return -1; + } + + if (b.m_pre[i] == 0 && a.m_pre[i] == TEXT('.')) + { + // identifiers both complete, a has an additional idenitifier + return 1; + } + + // identifiers must not be empty + std::wstring ida = getId(a.m_pre, idStart); + std::wstring idb = getId(b.m_pre, idStart); + + unsigned idanum = 0; + bool idaIsNum = try_stou(ida, &idanum); + unsigned idbnum = 0; + bool idbIsNum = try_stou(idb, &idbnum); + + if (idaIsNum && idbIsNum) + { + // Numeric comparison + return (idanum > idbnum) ? 1 : -1; + } + else if (idaIsNum || idbIsNum) + { + // Mixed compare. Spec: Number < Text + return idbIsNum ? 1 : -1; + } + // Ascii compare + return ida.compare(idb); + } + else + { + // a.m_pre[i] == b.m_pre[i] + if (a.m_pre[i] == 0) + { + break; + } + if (a.m_pre[i] == TEXT('.')) + { + idStart = i + 1; + } + } } - return a.m_build.compare(b.m_build); + return 0; } -bool try_stou(const std::wstring& str, unsigned* num) +static bool validIdentifierCharSet(const std::wstring & id) { - if (str.empty()) + // ids must be of the set [0-9a-zA-Z-] + + // ASCII and Unicode ordering + static_assert(TEXT('-') < TEXT('0'), "Code assumes ordering - < 0 < 9 < A < Z < a < z"); + static_assert(TEXT('0') < TEXT('9'), "Code assumes ordering - < 0 < 9 < A < Z < a < z"); + static_assert(TEXT('9') < TEXT('A'), "Code assumes ordering - < 0 < 9 < A < Z < a < z"); + static_assert(TEXT('A') < TEXT('Z'), "Code assumes ordering - < 0 < 9 < A < Z < a < z"); + static_assert(TEXT('Z') < TEXT('a'), "Code assumes ordering - < 0 < 9 < A < Z < a < z"); + static_assert(TEXT('a') < TEXT('z'), "Code assumes ordering - < 0 < 9 < A < Z < a < z"); + + for (size_t i = 0; id[i] != 0; ++i) { - return false; + if (id[i] >= TEXT('A')) + { + if ((id[i] > TEXT('Z') && id[i] < TEXT('a')) || id[i] > TEXT('z')) + { + return false; + } + } + else + { + if ((id[i] < TEXT('0') && id[i] != TEXT('-')) || id[i] > TEXT('9')) + { + return false; + } + } } - if (str.find_first_not_of(TEXT("0123456789")) != std::wstring::npos) - { - return false; - } - *num = (unsigned)std::stoul(str); return true; } -bool parse_internal(const std::wstring& ver, fx_ver_t* fx_ver, bool parse_only_production) +static bool validIdentifier(const std::wstring & id, bool buildMeta) +{ + if (id.empty()) + { + // Identifier must not be empty + return false; + } + + if (!validIdentifierCharSet(id)) + { + // ids must be of the set [0-9a-zA-Z-] + return false; + } + + if (!buildMeta && id[0] == TEXT('0') && id[1] != 0 && index_of_non_numeric(id, 1) == std::wstring::npos) + { + // numeric identifiers must not be padded with 0s + return false; + } + return true; +} + +static bool validIdentifiers(const std::wstring & ids) +{ + if (ids.empty()) + { + return true; + } + + bool prerelease = ids[0] == TEXT('-'); + bool buildMeta = ids[0] == TEXT('+'); + + if (!(prerelease || buildMeta)) + { + // ids must start with '-' or '+' for prerelease & build respectively + return false; + } + + size_t idStart = 1; + size_t nextId; + while ((nextId = ids.find(TEXT('.'), idStart)) != std::wstring::npos) + { + if (!validIdentifier(ids.substr(idStart, nextId - idStart), buildMeta)) + { + return false; + } + idStart = nextId + 1; + } + + if (!validIdentifier(ids.substr(idStart), buildMeta)) + { + return false; + } + + return true; +} + +bool parse_internal(const std::wstring & ver, fx_ver_t * fx_ver, bool parse_only_production) { size_t maj_start = 0; size_t maj_sep = ver.find(TEXT('.')); @@ -133,6 +314,12 @@ bool parse_internal(const std::wstring& ver, fx_ver_t* fx_ver, bool parse_only_p { return false; } + if (maj_sep > 1 && ver[maj_start] == TEXT('0')) + { + // if leading character is 0, and strlen > 1 + // then the numeric substring has leading zeroes which is prohibited by the specification. + return false; + } size_t min_start = maj_sep + 1; size_t min_sep = ver.find(TEXT('.'), min_start); @@ -146,16 +333,28 @@ bool parse_internal(const std::wstring& ver, fx_ver_t* fx_ver, bool parse_only_p { return false; } + if (min_sep - min_start > 1 && ver[min_start] == TEXT('0')) + { + // if leading character is 0, and strlen > 1 + // then the numeric substring has leading zeroes which is prohibited by the specification. + return false; + } unsigned patch = 0; size_t pat_start = min_sep + 1; - size_t pat_sep = ver.find_first_not_of(TEXT("0123456789"), pat_start); + size_t pat_sep = index_of_non_numeric(ver, pat_start); if (pat_sep == std::wstring::npos) { if (!try_stou(ver.substr(pat_start), &patch)) { return false; } + if (ver[pat_start + 1] != 0 && ver[pat_start] == TEXT('0')) + { + // if leading character is 0, and strlen != 1 + // then the numeric substring has leading zeroes which is prohibited by the specification. + return false; + } *fx_ver = fx_ver_t(major, minor, patch); return true; @@ -171,24 +370,39 @@ bool parse_internal(const std::wstring& ver, fx_ver_t* fx_ver, bool parse_only_p { return false; } + if (pat_sep - pat_start > 1 && ver[pat_start] == TEXT('0')) + { + return false; + } size_t pre_start = pat_sep; - size_t pre_sep = ver.find(TEXT('+'), pre_start); - if (pre_sep == std::wstring::npos) + size_t pre_sep = ver.find(TEXT('+'), pat_sep); + + std::wstring pre = (pre_sep == std::wstring::npos) ? ver.substr(pre_start) : ver.substr(pre_start, pre_sep - pre_start); + + if (!validIdentifiers(pre)) { - *fx_ver = fx_ver_t(major, minor, patch, ver.substr(pre_start)); - return true; + return false; } - else + + std::wstring build; + + if (pre_sep != std::wstring::npos) { - size_t build_start = pre_sep + 1; - *fx_ver = fx_ver_t(major, minor, patch, ver.substr(pre_start, pre_sep - pre_start), ver.substr(build_start)); - return true; + build = ver.substr(pre_sep); + + if (!validIdentifiers(build)) + { + return false; + } } + + *fx_ver = fx_ver_t(major, minor, patch, pre, build); + return true; } /* static */ -bool fx_ver_t::parse(const std::wstring& ver, fx_ver_t* fx_ver, bool parse_only_production) +bool fx_ver_t::parse(const std::wstring & ver, fx_ver_t * fx_ver, bool parse_only_production) { bool valid = parse_internal(ver, fx_ver, parse_only_production); assert(!valid || fx_ver->as_str() == ver); diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/fx_ver.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/fx_ver.h index 723a0da360..9c5a0af980 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/fx_ver.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/fx_ver.h @@ -1,29 +1,38 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. -#pragma once +#ifndef __FX_VER_H__ +#define __FX_VER_H__ #include -// Note: This is not SemVer (esp., in comparing pre-release part, fx_ver_t does not -// compare multiple dot separated identifiers individually.) ex: 1.0.0-beta.2 vs. 1.0.0-beta.11 +// Note: This is intended to implement SemVer 2.0 struct fx_ver_t { + fx_ver_t(); fx_ver_t(int major, int minor, int patch); + // if not empty pre contains valid prerelease label with leading '-' fx_ver_t(int major, int minor, int patch, const std::wstring& pre); + // if not empty pre contains valid prerelease label with leading '-' + // if not empty build contains valid build label with leading '+' fx_ver_t(int major, int minor, int patch, const std::wstring& pre, const std::wstring& build); - int get_major() const noexcept { return m_major; } - int get_minor() const noexcept { return m_minor; } - int get_patch() const noexcept { return m_patch; } + int get_major() const { return m_major; } + int get_minor() const { return m_minor; } + int get_patch() const { return m_patch; } - void set_major(int m) noexcept { m_major = m; } - void set_minor(int m) noexcept { m_minor = m; } - void set_patch(int p) noexcept { m_patch = p; } + void set_major(int m) { m_major = m; } + void set_minor(int m) { m_minor = m; } + void set_patch(int p) { m_patch = p; } - bool is_prerelease() const noexcept { return !m_pre.empty(); } + bool is_prerelease() const { return !m_pre.empty(); } + + bool is_empty() const { return m_major == -1; } std::wstring as_str() const; + std::wstring prerelease_glob() const; + std::wstring patch_glob() const; bool operator ==(const fx_ver_t& b) const; bool operator !=(const fx_ver_t& b) const; @@ -41,5 +50,7 @@ private: std::wstring m_pre; std::wstring m_build; - static int compare(const fx_ver_t&a, const fx_ver_t& b); + static int compare(const fx_ver_t& a, const fx_ver_t& b); }; + +#endif // __FX_VER_H__ diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLibTests/GlobalVersionTests.cpp b/src/Servers/IIS/AspNetCoreModuleV2/CommonLibTests/GlobalVersionTests.cpp index f38c9361d2..299cfd4f84 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLibTests/GlobalVersionTests.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLibTests/GlobalVersionTests.cpp @@ -83,6 +83,18 @@ namespace GlobalVersionTests EXPECT_STREQ(res.c_str(), L"2.1.0"); } + // Sem version 2.0 will not be used with ANCM out of process handler, but it's the most convenient way to test it. + TEST(FindHighestGlobalVersion, HighestVersionWithSemVersion20) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0-preview")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0-preview.1.1")); + + auto res = GlobalVersionUtility::FindHighestGlobalVersion(tempPath.path().c_str()); + + EXPECT_STREQ(res.c_str(), L"2.1.0-preview.1.1"); + } + TEST(FindHighestGlobalVersion, HighestVersionWithMultipleVersionsPreview) { auto tempPath = TempDirectory();