diff --git a/docker/Tests/legacy.tests.ps1 b/docker/Tests/legacy.tests.ps1 index 7629a37..c8229b1 100644 --- a/docker/Tests/legacy.tests.ps1 +++ b/docker/Tests/legacy.tests.ps1 @@ -85,9 +85,11 @@ Describe 'vswhere -legacy' { $instances[0].installationPath | Should Be 'C:\VisualStudio\14.0' } - It '-version is not supported' { - C:\bin\vswhere.exe -legacy -version 14.0 - $LASTEXITCODE | Should Be 87 + It '-version is supported' { + $instances = C:\bin\vswhere.exe -legacy -latest -format json | ConvertFrom-Json + $instances.Count | Should Be 1 + $instances[0].instanceId | Should Be 'VisualStudio.14.0' + $instances[0].installationPath | Should Be 'C:\VisualStudio\14.0' } } diff --git a/src/vswhere.lib/VersionRange.cpp b/src/vswhere.lib/VersionRange.cpp new file mode 100644 index 0000000..9d3b8c1 --- /dev/null +++ b/src/vswhere.lib/VersionRange.cpp @@ -0,0 +1,284 @@ +// +// Copyright (C) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt in the project root for license information. +// + +#include "stdafx.h" + +#define _nameof(x) #x +#define ExitOnFailure(x, s, ...) if (FAILED(x)) { goto Exit; } +#define ExitOnNull(p, x, e, s, ...) if (NULL == p) { x = e; goto Exit; } + +using namespace std; + +static locale neutral_locale("C", LC_ALL); + +STDMETHODIMP VersionRange::ParseVersion( + _In_ LPCOLESTR pwszVersion, + _Out_ PULONGLONG pullVersion +) noexcept +{ + HRESULT hr = S_OK; + LPWSTR pwszVersionEnd = NULL; + + ExitOnNull(pwszVersion, hr, E_INVALIDARG, "Missing required parameter: %hs", _nameof(pwszVersion)); + ExitOnNull(pullVersion, hr, E_POINTER, "Output parameter is NULL."); + + *pullVersion = 0; + + LPWSTR pwszVersionRangeEnd = NULL; + pwszVersion = Trim(pwszVersion, &pwszVersionEnd); + + if (pwszVersion > pwszVersionEnd) + { + // Should not be possible but check anyway. + ExitOnFailure(hr = E_UNEXPECTED, "Invalid string."); + } + + const size_t cchVersion = pwszVersionEnd - pwszVersion; + ExitOnNull(cchVersion, hr, E_INVALIDARG, "Version required."); + + hr = ParseVersionString(pwszVersion, pwszVersionEnd, pullVersion); + ExitOnFailure(hr, "Failed to parse version: %ls", pwszVersion); + +Exit: + return hr; +} + +STDMETHODIMP VersionRange::ParseVersionRange( + _In_ LPCOLESTR pwszVersionRange, + _Out_ PULONGLONG pullMinVersion, + _Out_ PULONGLONG pullMaxVersion +) +{ + HRESULT hr = S_OK; + ULONGLONG ullMinVersion = MinVersion; + ULONGLONG ullMaxVersion = MaxVersion; + LPCWSTR pwszVersionStart = NULL; + LPCWSTR pwszVersionEnd = NULL; + bool sep = false; + bool single = false; + bool isMinInclusive = true; + bool isMaxInclusive = true; + + ExitOnNull(pwszVersionRange, hr, E_INVALIDARG, "Missing required parameter: %hs", _nameof(pwszVersionRange)); + ExitOnNull(pullMinVersion, hr, E_POINTER, "Output parameter is NULL."); + ExitOnNull(pullMaxVersion, hr, E_POINTER, "Output parameter is NULL."); + + *pullMinVersion = 0; + *pullMaxVersion = 0; + + LPWSTR pwszVersionRangeEnd = NULL; + pwszVersionRange = Trim(pwszVersionRange, &pwszVersionRangeEnd); + + if (pwszVersionRange > pwszVersionRangeEnd) + { + // Should not be possible but check anyway. + ExitOnFailure(hr = E_UNEXPECTED, "Invalid string."); + } + + const size_t cchVersionRange = pwszVersionRangeEnd - pwszVersionRange; + ExitOnNull(cchVersionRange, hr, E_INVALIDARG, "Version range required."); + + for (size_t i = 0; i < cchVersionRange; ++i) + { + const auto pwsz = &pwszVersionRange[i]; + const auto ch = *pwsz; + + if (L'(' == ch || L'[' == ch) + { + if (0 != i) + { + ExitOnFailure(hr = E_INVALIDARG, "Invalid format."); + } + else if (L'(' == ch) + { + isMinInclusive = false; + } + } + else if (L')' == ch || L']' == ch) + { + if (cchVersionRange - 1 != i) + { + ExitOnFailure(hr = E_INVALIDARG, "Invalid format."); + } + else if (L')' == ch) + { + isMaxInclusive = false; + } + + // Only a single version if the separator was not yet parsed. + single = !sep; + } + else if (L',' == ch) + { + if (sep) + { + // Already parsed a separator. + ExitOnFailure(hr = E_INVALIDARG, "Invalid format."); + } + + sep = true; + + if (pwszVersionEnd > pwszVersionStart) + { + hr = ParseVersionString(pwszVersionStart, pwszVersionEnd, &ullMinVersion); + ExitOnFailure(hr, "Failed to parse version."); + } + + pwszVersionStart = NULL; + pwszVersionEnd = NULL; + } + else if (isspace(ch, neutral_locale)) + { + continue; + } + else if (!pwszVersionStart) + { + pwszVersionStart = pwsz; + pwszVersionEnd = pwsz + 1; + } + else + { + pwszVersionEnd = pwsz + 1; + } + } + + if (pwszVersionEnd > pwszVersionStart) + { + auto pullVersion = sep ? &ullMaxVersion : &ullMinVersion; + + hr = ParseVersionString(pwszVersionStart, pwszVersionEnd, pullVersion); + ExitOnFailure(hr, "Failed to parse version."); + + if (single) + { + ullMaxVersion = ullMinVersion; + } + } + + if (!isMinInclusive) + { + if (_UI64_MAX == ullMinVersion) + { + ExitOnFailure(hr = E_INVALIDARG, "Arithmetic overflow."); + } + + ullMinVersion++; + } + + if (!isMaxInclusive) + { + if (0 == ullMaxVersion) + { + ExitOnFailure(hr = E_INVALIDARG, "Arithmetic underflow."); + } + + ullMaxVersion--; + } + + if (ullMinVersion > ullMaxVersion) + { + ExitOnFailure(hr = E_INVALIDARG, "Min greater than max."); + } + + *pullMinVersion = ullMinVersion; + *pullMaxVersion = ullMaxVersion; + +Exit: + return hr; +} + +HRESULT VersionRange::ParseVersionString( + _In_ const LPCWSTR pwszVersionBegin, + _In_ const LPCWSTR pwszVersionEnd, + _Out_ PULONGLONG pullVersion +) +{ + _ASSERT(pwszVersionBegin); + _ASSERT(pwszVersionEnd); + _ASSERT(pullVersion); + + HRESULT hr = S_OK; + LPWSTR pwszFieldBegin = const_cast(pwszVersionBegin); + LPWSTR pwszFieldEnd = const_cast(pwszVersionBegin); + WORD cFields = 0; + ULONG ulField = 0; + ULONGLONG ullVersion = 0; + + *pullVersion = 0; + + for (;;) + { + if (4 <= cFields) + { + ExitOnFailure(hr = E_INVALIDARG, "Too many version fields."); + } + + while (pwszFieldEnd < pwszVersionEnd && L'.' != *pwszFieldEnd) + { + if (isspace(*pwszFieldEnd, neutral_locale)) + { + ExitOnFailure(hr = E_INVALIDARG, "Space in version field."); + } + + ++pwszFieldEnd; + } + + if (pwszFieldBegin == pwszFieldEnd) + { + ExitOnFailure(hr = E_INVALIDARG, "Empty version field."); + } + + LPWSTR pwszEnd = NULL; + ulField = ::wcstoul(pwszFieldBegin, &pwszEnd, 10); + + if (USHRT_MAX < ulField) + { + ExitOnFailure(hr = E_INVALIDARG, "Version field too large."); + } + else if (0 == ulField && pwszEnd != pwszFieldEnd) + { + ExitOnFailure(hr = E_INVALIDARG, "NaN"); + } + + pwszFieldEnd = pwszEnd; + + ullVersion |= (ULONGLONG)ulField << ((3 - cFields) * 16); + ++cFields; + + if (pwszFieldEnd >= pwszVersionEnd) + { + break; + } + + pwszFieldBegin = ++pwszFieldEnd; + } + + *pullVersion = ullVersion; + +Exit: + return hr; +} + +LPWSTR VersionRange::Trim( + _In_ const LPCWSTR pwszValue, + _Out_ LPWSTR* ppwszEnd +) noexcept +{ + auto pwszResult = const_cast(pwszValue); + + // Trim left. + while (isspace(*pwszResult, neutral_locale)) + { + ++pwszResult; + } + + // Trim right. + auto cchResult = ::wcslen(pwszResult); + for (; cchResult > 0 && isspace(pwszResult[cchResult - 1], neutral_locale); --cchResult); + + // Set end to where terminating null is or would be. + *ppwszEnd = &pwszResult[cchResult]; + return pwszResult; +} diff --git a/src/vswhere.lib/VersionRange.h b/src/vswhere.lib/VersionRange.h new file mode 100644 index 0000000..b3e236c --- /dev/null +++ b/src/vswhere.lib/VersionRange.h @@ -0,0 +1,105 @@ +// +// Copyright (C) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt in the project root for license information. +// + +#pragma once + +class VersionRange : + public ISetupHelper +{ +public: + VersionRange() noexcept : + m_ulRef(1) + { + } + +private: + ~VersionRange() + { + } + +public: + // IUnknown + STDMETHODIMP QueryInterface( + _In_ REFIID iid, + _Out_ LPVOID* ppUnk + ) + { + HRESULT hr = S_OK; + IUnknown* pUnk = NULL; + + if (!ppUnk) + { + return E_POINTER; + } + + *ppUnk = NULL; + if (iid == __uuidof(ISetupHelper)) + { + pUnk = static_cast(this); + } + else if (iid == IID_IUnknown) + { + pUnk = static_cast(this); + } + else + { + hr = E_NOINTERFACE; + } + + if (pUnk) + { + pUnk->AddRef(); + *ppUnk = pUnk; + } + + return hr; + } + + STDMETHODIMP_(ULONG) AddRef() + { + return ::InterlockedIncrement(&m_ulRef); + } + + STDMETHODIMP_(ULONG) Release() + { + ULONG ulRef = ::InterlockedDecrement(&m_ulRef); + if (ulRef == 0) + { + delete this; + } + + return ulRef; + } + +public: + // ISetupHelper + STDMETHOD(ParseVersion)( + _In_ LPCOLESTR pwszVersion, + _Out_ PULONGLONG pullVersion + ) noexcept; + + STDMETHOD(ParseVersionRange)( + _In_ LPCOLESTR pwszVersionRange, + _Out_ PULONGLONG pullMinVersion, + _Out_ PULONGLONG pullMaxVersion + ); + + static const ULONGLONG MinVersion = 0; + static const ULONGLONG MaxVersion = 18446744073709551615; + +private: + static HRESULT ParseVersionString( + _In_ const LPCWSTR pwszVersionBegin, + _In_ const LPCWSTR pwszVersionEnd, + _Out_ PULONGLONG pullVersion + ); + + static LPWSTR Trim( + _In_ const LPCWSTR pwszValue, + _Out_ LPWSTR* ppwszEnd + ) noexcept; + + ULONG m_ulRef; +}; diff --git a/src/vswhere.lib/resource.h b/src/vswhere.lib/resource.h index 644095f..8b50a12 100644 Binary files a/src/vswhere.lib/resource.h and b/src/vswhere.lib/resource.h differ diff --git a/src/vswhere.lib/stdafx.h b/src/vswhere.lib/stdafx.h index 041c8a4..5dc19d7 100644 --- a/src/vswhere.lib/stdafx.h +++ b/src/vswhere.lib/stdafx.h @@ -28,6 +28,7 @@ // STL headers #include +#include #include #include #include @@ -69,5 +70,6 @@ _COM_SMARTPTR_TYPEDEF(ISetupInstanceCatalog, __uuidof(ISetupInstanceCatalog)); #include "SafeArray.h" #include "TextFormatter.h" #include "ValueFormatter.h" +#include "VersionRange.h" #include "XmlScope.h" #include "XmlFormatter.h" diff --git a/src/vswhere.lib/vswhere.lib.rc b/src/vswhere.lib/vswhere.lib.rc index f474f09..3448dd2 100644 Binary files a/src/vswhere.lib/vswhere.lib.rc and b/src/vswhere.lib/vswhere.lib.rc differ diff --git a/src/vswhere.lib/vswhere.lib.vcxproj b/src/vswhere.lib/vswhere.lib.vcxproj index 34f6b4f..544d291 100644 --- a/src/vswhere.lib/vswhere.lib.vcxproj +++ b/src/vswhere.lib/vswhere.lib.vcxproj @@ -120,6 +120,7 @@ + @@ -143,6 +144,7 @@ + diff --git a/src/vswhere.lib/vswhere.lib.vcxproj.filters b/src/vswhere.lib/vswhere.lib.vcxproj.filters index af05dde..cc144cb 100644 --- a/src/vswhere.lib/vswhere.lib.vcxproj.filters +++ b/src/vswhere.lib/vswhere.lib.vcxproj.filters @@ -87,6 +87,9 @@ Header Files + + Header Files + @@ -140,6 +143,9 @@ Source Files + + Source Files + diff --git a/src/vswhere/Program.cpp b/src/vswhere/Program.cpp index 92616fd..0afec6b 100644 --- a/src/vswhere/Program.cpp +++ b/src/vswhere/Program.cpp @@ -50,11 +50,10 @@ int wmain(_In_ int argc, _In_ LPCWSTR argv[]) { query->QueryInterface(&helper); } - - if (!args.get_Version().empty() && !query) + // Fall back to a copy of the current implementation. + else { - auto message = ResourceManager::GetString(IDS_E_VERSION); - throw win32_error(ERROR_INVALID_PARAMETER, message); + helper.Attach(new VersionRange); } IEnumSetupInstancesPtr e; diff --git a/test/vswhere.test/VersionRangeTests.cpp b/test/vswhere.test/VersionRangeTests.cpp new file mode 100644 index 0000000..9a34765 --- /dev/null +++ b/test/vswhere.test/VersionRangeTests.cpp @@ -0,0 +1,206 @@ +// +// Copyright (C) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt in the project root for license information. +// + +#include "stdafx.h" + +using namespace std; +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +TEST_CLASS(VersionRangeTests) +{ +public: + + TEST_METHOD(ParseVersion_pwszVersion_Null) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + Assert::AreEqual(E_INVALIDARG, sut->ParseVersion(NULL, NULL)); + } + + TEST_METHOD(ParseVersion_pullVersion_Null) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + Assert::AreEqual(E_POINTER, sut->ParseVersion(L"1.0", NULL)); + } + + TEST_METHOD(ParseVersion_Major) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + ULONGLONG ullVersion = 0; + Assert::AreEqual(S_OK, sut->ParseVersion(L"1", &ullVersion)); + Assert::AreEqual(281474976710656ULL, ullVersion); + } + + TEST_METHOD(ParseVersion_MajorMinor) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + ULONGLONG ullVersion = 0; + Assert::AreEqual(S_OK, sut->ParseVersion(L"1.2", &ullVersion)); + Assert::AreEqual(281483566645248ULL, ullVersion); + } + + TEST_METHOD(ParseVersion_MajorMinorBuild) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + ULONGLONG ullVersion = 0; + Assert::AreEqual(S_OK, sut->ParseVersion(L"1.2.3", &ullVersion)); + Assert::AreEqual(281483566841856ULL, ullVersion); + } + + TEST_METHOD(ParseVersion_MajorMinorBuildRevision) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + ULONGLONG ullVersion = 0; + Assert::AreEqual(S_OK, sut->ParseVersion(L"1.2.3.4", &ullVersion)); + Assert::AreEqual(281483566841860ULL, ullVersion); + } + + TEST_METHOD(ParseVersion_Too_Many_Fields) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + ULONGLONG ullVersion = 0; + Assert::AreEqual(E_INVALIDARG, sut->ParseVersion(L"1.2.3.4.5", &ullVersion)); + Assert::AreEqual(0ULL, ullVersion); + } + + TEST_METHOD(ParseVersion_Empty_Field) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + ULONGLONG ullVersion = 0; + Assert::AreEqual(E_INVALIDARG, sut->ParseVersion(L"1..0", &ullVersion)); + Assert::AreEqual(0ULL, ullVersion); + } + + TEST_METHOD(ParseVersion_Overflow) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + ULONGLONG ullVersion = 0; + Assert::AreEqual(E_INVALIDARG, sut->ParseVersion(L"65536", &ullVersion)); + } + + TEST_METHOD(ParseVersion_NaN) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + ULONGLONG ullVersion = 0; + Assert::AreEqual(E_INVALIDARG, sut->ParseVersion(L"NaN", &ullVersion)); + } + + TEST_METHOD(ParseVersion_only_Spaces) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + ULONGLONG ullVersion = 0; + Assert::AreEqual(E_INVALIDARG, sut->ParseVersion(L" ", &ullVersion)); + } + + TEST_METHOD(ParseVersion_with_Spaces) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + ULONGLONG ullVersion = 0; + Assert::AreEqual(S_OK, sut->ParseVersion(L" 1.0 ", &ullVersion)); + Assert::AreEqual(281474976710656ULL, ullVersion); + } + + TEST_METHOD(ParseVersionRange_pwszVersionRange_Null) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + Assert::AreEqual(E_INVALIDARG, sut->ParseVersionRange(NULL, NULL, NULL)); + } + + TEST_METHOD(ParseVersionRange_pullMinVersion_Null) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + Assert::AreEqual(E_POINTER, sut->ParseVersionRange(L"[1.0,)", NULL, NULL)); + } + + TEST_METHOD(ParseVersionRange_pullMaxVersion_Null) + { + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + ULONGLONG ullMinVersion = 0; + Assert::AreEqual(E_POINTER, sut->ParseVersionRange(L"[1.0,)", &ullMinVersion, NULL)); + } + + TEST_METHOD(ParseVersionRange) + { + typedef tuple theory_datum; + typedef vector theory_data; + + auto pHelper = new VersionRange; + ISetupHelperPtr sut(pHelper, false); + + theory_data data = + { + make_tuple(__LINE__, S_OK, L"1.0", 281474976710656, 18446744073709551615), + make_tuple(__LINE__, S_OK, L"[1.0]", 281474976710656, 281474976710656), + make_tuple(__LINE__, E_INVALIDARG, L"[1.0)", 0, 0), + make_tuple(__LINE__, S_OK, L"[1.0,)", 281474976710656, 18446744073709551614), + make_tuple(__LINE__, S_OK, L"[1.0,2.0)", 281474976710656, 562949953421311), + make_tuple(__LINE__, S_OK, L"[1,2)", 281474976710656, 562949953421311), + make_tuple(__LINE__, S_OK, L"[1,2]", 281474976710656, 562949953421312), + make_tuple(__LINE__, S_OK, L"(1.0,2.0]", 281474976710657, 562949953421312), + make_tuple(__LINE__, S_OK, L"[1.0", 281474976710656, 18446744073709551615), + make_tuple(__LINE__, S_OK, L",2.0)", 0, 562949953421311), + make_tuple(__LINE__, E_INVALIDARG, L"NaN", 0, 0), + make_tuple(__LINE__, E_INVALIDARG, L"[1,2,3)", 0, 0), + make_tuple(__LINE__, E_INVALIDARG, L"", 0, 0), + make_tuple(__LINE__, E_INVALIDARG, L"1[,2)", 0, 0), + make_tuple(__LINE__, E_INVALIDARG, L"[1,)2", 0, 0), + make_tuple(__LINE__, E_INVALIDARG, L"(65535.65535.65535.65535]", 0, 0), + make_tuple(__LINE__, E_INVALIDARG, L"[0,0)", 0, 0), + make_tuple(__LINE__, E_INVALIDARG, L"[2,1)", 0, 0), + make_tuple(__LINE__, E_INVALIDARG, L"(1.65535.65535.65535,2.0)", 0, 0), + make_tuple(__LINE__, S_OK, L"[1.0, )", 281474976710656, 18446744073709551614), + make_tuple(__LINE__, S_OK, L" [ 1.0 , ) ", 281474976710656, 18446744073709551614), + make_tuple(__LINE__, E_INVALIDARG, L" [ 1. 0 , ) ", 0, 0), + }; + + UINT uiLine; + HRESULT hrActual; + HRESULT hrExpected; + LPCWSTR pwszVersionRange; + ULONGLONG ullMinVersionActual; + ULONGLONG ullMinVersionExpected; + ULONGLONG ullMaxVersionActual; + ULONGLONG ullMaxVersionExpected; + + for (auto datum : data) + { + tie(uiLine, hrExpected, pwszVersionRange, ullMinVersionExpected, ullMaxVersionExpected) = datum; + + hrActual = sut->ParseVersionRange(pwszVersionRange, &ullMinVersionActual, &ullMaxVersionActual); + Assert::AreEqual(hrExpected, hrActual, format(L"Unexpected result parsing '%ls' from line %d, expected: 0x%08x, actual: 0x%08x", pwszVersionRange, uiLine, hrExpected, hrActual).c_str()); + Assert::AreEqual(ullMinVersionExpected, ullMinVersionActual, format(L"Unexpected minimum version parsing '%ls'", pwszVersionRange).c_str()); + Assert::AreEqual(ullMaxVersionExpected, ullMaxVersionActual, format(L"Unexpected maximum version parsing '%ls'", pwszVersionRange).c_str()); + } + } +}; diff --git a/test/vswhere.test/vswhere.test.vcxproj b/test/vswhere.test/vswhere.test.vcxproj index e1aab34..b6a973e 100644 --- a/test/vswhere.test/vswhere.test.vcxproj +++ b/test/vswhere.test/vswhere.test.vcxproj @@ -124,6 +124,7 @@ + diff --git a/test/vswhere.test/vswhere.test.vcxproj.filters b/test/vswhere.test/vswhere.test.vcxproj.filters index a5fcf0a..7429f1a 100644 --- a/test/vswhere.test/vswhere.test.vcxproj.filters +++ b/test/vswhere.test/vswhere.test.vcxproj.filters @@ -92,6 +92,9 @@ Source Files + + Source Files +