From d1d31466bb9f2e887a277807d60378afef9cc57d Mon Sep 17 00:00:00 2001 From: Sean Hall Date: Sat, 29 Aug 2020 21:29:28 -0500 Subject: WIXFEAT:6210 Parse and compare bundle versions kind of like SemVer. --- src/dutil/dutil.vcxproj | 2 + src/dutil/dutil.vcxproj.filters | 6 + src/dutil/inc/dutilsources.h | 1 + src/dutil/inc/verutil.h | 93 ++++++ src/dutil/precomp.h | 1 + src/dutil/verutil.cpp | 631 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 734 insertions(+) create mode 100644 src/dutil/inc/verutil.h create mode 100644 src/dutil/verutil.cpp (limited to 'src/dutil') diff --git a/src/dutil/dutil.vcxproj b/src/dutil/dutil.vcxproj index e9bbb98b..017f7a6f 100644 --- a/src/dutil/dutil.vcxproj +++ b/src/dutil/dutil.vcxproj @@ -111,6 +111,7 @@ + @@ -167,6 +168,7 @@ + diff --git a/src/dutil/dutil.vcxproj.filters b/src/dutil/dutil.vcxproj.filters index 01dd6661..b93d166b 100644 --- a/src/dutil/dutil.vcxproj.filters +++ b/src/dutil/dutil.vcxproj.filters @@ -159,6 +159,9 @@ Source Files + + Source Files + Source Files @@ -329,6 +332,9 @@ Header Files + + Header Files + Header Files diff --git a/src/dutil/inc/dutilsources.h b/src/dutil/inc/dutilsources.h index b03013ca..7d512cb3 100644 --- a/src/dutil/inc/dutilsources.h +++ b/src/dutil/inc/dutilsources.h @@ -60,6 +60,7 @@ typedef enum DUTIL_SOURCE DUTIL_SOURCE_WIUTIL, DUTIL_SOURCE_WUAUTIL, DUTIL_SOURCE_XMLUTIL, + DUTIL_SOURCE_VERUTIL, DUTIL_SOURCE_EXTERNAL = 256, } DUTIL_SOURCE; diff --git a/src/dutil/inc/verutil.h b/src/dutil/inc/verutil.h new file mode 100644 index 00000000..d3715049 --- /dev/null +++ b/src/dutil/inc/verutil.h @@ -0,0 +1,93 @@ +#pragma once +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + + +#ifdef __cplusplus +extern "C" { +#endif + +#define ReleaseVerutilVersion(p) if (p) { VerFreeVersion(p); p = NULL; } + +typedef struct _VERUTIL_VERSION_RELEASE_LABEL +{ + BOOL fNumeric; + DWORD dwValue; + DWORD_PTR cchLabelOffset; + int cchLabel; +} VERUTIL_VERSION_RELEASE_LABEL; + +typedef struct _VERUTIL_VERSION +{ + LPWSTR sczVersion; + DWORD dwMajor; + DWORD dwMinor; + DWORD dwPatch; + DWORD dwRevision; + DWORD cReleaseLabels; + VERUTIL_VERSION_RELEASE_LABEL* rgReleaseLabels; + DWORD_PTR cchMetadataOffset; + BOOL fInvalid; +} VERUTIL_VERSION; + +/******************************************************************* + VerCompareParsedVersions - compares the Verutil versions. + +*******************************************************************/ +HRESULT DAPI VerCompareParsedVersions( + __in VERUTIL_VERSION* pVersion1, + __in VERUTIL_VERSION* pVersion2, + __out int* pnResult + ); + +/******************************************************************* + VerCompareStringVersions - parses the strings with VerParseVersion and then + compares the Verutil versions with VerCompareParsedVersions. + +*******************************************************************/ +HRESULT DAPI VerCompareStringVersions( + __in_z LPCWSTR wzVersion1, + __in_z LPCWSTR wzVersion2, + __in BOOL fStrict, + __out int* pnResult + ); + +/******************************************************************** + VerCopyVersion - copies the given Verutil version. + +*******************************************************************/ +HRESULT DAPI VerCopyVersion( + __in VERUTIL_VERSION* pSource, + __out VERUTIL_VERSION** ppVersion + ); + +/******************************************************************** + VerFreeVersion - frees any memory associated with a Verutil version. + +*******************************************************************/ +void DAPI VerFreeVersion( + __in VERUTIL_VERSION* pVersion + ); + +/******************************************************************* + VerParseVersion - parses the string into a Verutil version. + +*******************************************************************/ +HRESULT DAPI VerParseVersion( + __in_z LPCWSTR wzVersion, + __in DWORD cchVersion, + __in BOOL fStrict, + __out VERUTIL_VERSION** ppVersion + ); + +/******************************************************************* + VerParseVersion - parses the QWORD into a Verutil version. + +*******************************************************************/ +HRESULT DAPI VerVersionFromQword( + __in DWORD64 qwVersion, + __out VERUTIL_VERSION** ppVersion + ); + +#ifdef __cplusplus +} +#endif diff --git a/src/dutil/precomp.h b/src/dutil/precomp.h index 7fdc83ae..be58860c 100644 --- a/src/dutil/precomp.h +++ b/src/dutil/precomp.h @@ -89,6 +89,7 @@ #include "uncutil.h" #include "uriutil.h" #include "userutil.h" +#include "verutil.h" #include "wiutil.h" #include "wuautil.h" #include // This header is needed for msxml2.h to compile correctly diff --git a/src/dutil/verutil.cpp b/src/dutil/verutil.cpp new file mode 100644 index 00000000..f362f413 --- /dev/null +++ b/src/dutil/verutil.cpp @@ -0,0 +1,631 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +#include "precomp.h" + +// Exit macros +#define VerExitOnLastError(x, s, ...) ExitOnLastErrorSource(DUTIL_SOURCE_VERUTIL, x, s, __VA_ARGS__) +#define VerExitOnLastErrorDebugTrace(x, s, ...) ExitOnLastErrorDebugTraceSource(DUTIL_SOURCE_VERUTIL, x, s, __VA_ARGS__) +#define VerExitWithLastError(x, s, ...) ExitWithLastErrorSource(DUTIL_SOURCE_VERUTIL, x, s, __VA_ARGS__) +#define VerExitOnFailure(x, s, ...) ExitOnFailureSource(DUTIL_SOURCE_VERUTIL, x, s, __VA_ARGS__) +#define VerExitOnRootFailure(x, s, ...) ExitOnRootFailureSource(DUTIL_SOURCE_VERUTIL, x, s, __VA_ARGS__) +#define VerExitOnFailureDebugTrace(x, s, ...) ExitOnFailureDebugTraceSource(DUTIL_SOURCE_VERUTIL, x, s, __VA_ARGS__) +#define VerExitOnNull(p, x, e, s, ...) ExitOnNullSource(DUTIL_SOURCE_VERUTIL, p, x, e, s, __VA_ARGS__) +#define VerExitOnNullWithLastError(p, x, s, ...) ExitOnNullWithLastErrorSource(DUTIL_SOURCE_VERUTIL, p, x, s, __VA_ARGS__) +#define VerExitOnNullDebugTrace(p, x, e, s, ...) ExitOnNullDebugTraceSource(DUTIL_SOURCE_VERUTIL, p, x, e, s, __VA_ARGS__) +#define VerExitOnInvalidHandleWithLastError(p, x, s, ...) ExitOnInvalidHandleWithLastErrorSource(DUTIL_SOURCE_VERUTIL, p, x, s, __VA_ARGS__) +#define VerExitOnWin32Error(e, x, s, ...) ExitOnWin32ErrorSource(DUTIL_SOURCE_VERUTIL, e, x, s, __VA_ARGS__) + +// constants +const DWORD GROW_RELEASE_LABELS = 3; + +// Forward declarations. +static int CompareDword( + __in const DWORD& dw1, + __in const DWORD& dw2 + ); +static HRESULT CompareReleaseLabel( + __in const VERUTIL_VERSION_RELEASE_LABEL* p1, + __in LPCWSTR wzVersion1, + __in const VERUTIL_VERSION_RELEASE_LABEL* p2, + __in LPCWSTR wzVersion2, + __out int* pnResult + ); +static HRESULT CompareVersionSubstring( + __in LPCWSTR wzString1, + __in int cchCount1, + __in LPCWSTR wzString2, + __in int cchCount2, + __out int* pnResult + ); + + +DAPI_(HRESULT) VerCompareParsedVersions( + __in VERUTIL_VERSION* pVersion1, + __in VERUTIL_VERSION* pVersion2, + __out int* pnResult + ) +{ + HRESULT hr = S_OK; + int nResult = 0; + DWORD cMaxReleaseLabels = 0; + BOOL fCompareMetadata = FALSE; + + if (!pVersion1 || !pVersion1->sczVersion || + !pVersion2 || !pVersion2->sczVersion) + { + ExitFunction1(hr = E_INVALIDARG); + } + + if (pVersion1 == pVersion2) + { + ExitFunction1(nResult = 0); + } + + nResult = CompareDword(pVersion1->dwMajor, pVersion2->dwMajor); + if (0 != nResult) + { + ExitFunction(); + } + + nResult = CompareDword(pVersion1->dwMinor, pVersion2->dwMinor); + if (0 != nResult) + { + ExitFunction(); + } + + nResult = CompareDword(pVersion1->dwPatch, pVersion2->dwPatch); + if (0 != nResult) + { + ExitFunction(); + } + + nResult = CompareDword(pVersion1->dwRevision, pVersion2->dwRevision); + if (0 != nResult) + { + ExitFunction(); + } + + if (pVersion1->fInvalid) + { + if (!pVersion2->fInvalid) + { + ExitFunction1(nResult = -1); + } + else + { + fCompareMetadata = TRUE; + } + } + else if (pVersion2->fInvalid) + { + ExitFunction1(nResult = 1); + } + + if (pVersion1->cReleaseLabels) + { + if (pVersion2->cReleaseLabels) + { + cMaxReleaseLabels = max(pVersion1->cReleaseLabels, pVersion2->cReleaseLabels); + } + else + { + ExitFunction1(nResult = -1); + } + } + else if (pVersion2->cReleaseLabels) + { + ExitFunction1(nResult = 1); + } + + if (cMaxReleaseLabels) + { + for (DWORD i = 0; i < cMaxReleaseLabels; ++i) + { + VERUTIL_VERSION_RELEASE_LABEL* pReleaseLabel1 = pVersion1->cReleaseLabels > i ? pVersion1->rgReleaseLabels + i : NULL; + VERUTIL_VERSION_RELEASE_LABEL* pReleaseLabel2 = pVersion2->cReleaseLabels > i ? pVersion2->rgReleaseLabels + i : NULL; + + hr = CompareReleaseLabel(pReleaseLabel1, pVersion1->sczVersion, pReleaseLabel2, pVersion2->sczVersion, &nResult); + if (FAILED(hr) || 0 != nResult) + { + ExitFunction(); + } + } + } + + if (fCompareMetadata) + { + hr = CompareVersionSubstring(pVersion1->sczVersion + pVersion1->cchMetadataOffset, -1, pVersion2->sczVersion + pVersion2->cchMetadataOffset, -1, &nResult); + } + +LExit: + *pnResult = nResult; + return hr; +} + +DAPI_(HRESULT) VerCompareStringVersions( + __in_z LPCWSTR wzVersion1, + __in_z LPCWSTR wzVersion2, + __in BOOL fStrict, + __out int* pnResult + ) +{ + HRESULT hr = S_OK; + VERUTIL_VERSION* pVersion1 = NULL; + VERUTIL_VERSION* pVersion2 = NULL; + int nResult = 0; + + hr = VerParseVersion(wzVersion1, 0, fStrict, &pVersion1); + VerExitOnFailure(hr, "Failed to parse Verutil version '%ls'", wzVersion1); + + hr = VerParseVersion(wzVersion2, 0, fStrict, &pVersion2); + VerExitOnFailure(hr, "Failed to parse Verutil version '%ls'", wzVersion2); + + hr = VerCompareParsedVersions(pVersion1, pVersion2, &nResult); + VerExitOnFailure(hr, "Failed to compare parsed Verutil versions '%ls' and '%ls'.", wzVersion1, wzVersion2); + +LExit: + *pnResult = nResult; + + ReleaseVerutilVersion(pVersion1); + ReleaseVerutilVersion(pVersion2); + + return hr; +} + +DAPI_(HRESULT) VerCopyVersion( + __in VERUTIL_VERSION* pSource, + __out VERUTIL_VERSION** ppVersion + ) +{ + HRESULT hr = S_OK; + VERUTIL_VERSION* pCopy = NULL; + + pCopy = reinterpret_cast(MemAlloc(sizeof(VERUTIL_VERSION), TRUE)); + VerExitOnNull(pCopy, hr, E_OUTOFMEMORY, "Failed to allocate memory for Verutil version copy."); + + hr = StrAllocString(&pCopy->sczVersion, pSource->sczVersion, 0); + VerExitOnFailure(hr, "Failed to copy Verutil version string '%ls'.", pSource->sczVersion); + + pCopy->dwMajor = pSource->dwMajor; + pCopy->dwMinor = pSource->dwMinor; + pCopy->dwPatch = pSource->dwPatch; + pCopy->dwRevision = pSource->dwRevision; + + hr = MemEnsureArraySize(reinterpret_cast(&pCopy->rgReleaseLabels), 0, sizeof(VERUTIL_VERSION_RELEASE_LABEL), pSource->cReleaseLabels); + VerExitOnFailure(hr, "Failed to allocate memory for Verutil version release labels copies."); + + pCopy->cReleaseLabels = pSource->cReleaseLabels; + + for (DWORD i = 0; i < pCopy->cReleaseLabels; ++i) + { + VERUTIL_VERSION_RELEASE_LABEL* pSourceLabel = pSource->rgReleaseLabels + i; + VERUTIL_VERSION_RELEASE_LABEL* pCopyLabel = pCopy->rgReleaseLabels + i; + + pCopyLabel->cchLabelOffset = pSourceLabel->cchLabelOffset; + pCopyLabel->cchLabel = pSourceLabel->cchLabel; + pCopyLabel->fNumeric = pSourceLabel->fNumeric; + pCopyLabel->dwValue = pSourceLabel->dwValue; + } + + pCopy->cchMetadataOffset = pSource->cchMetadataOffset; + pCopy->fInvalid = pSource->fInvalid; + + *ppVersion = pCopy; + pCopy = NULL; + +LExit: + ReleaseVerutilVersion(pCopy); + + return hr; +} + +DAPI_(void) VerFreeVersion( + __in VERUTIL_VERSION* pVersion + ) +{ + if (pVersion) + { + ReleaseStr(pVersion->sczVersion); + ReleaseMem(pVersion->rgReleaseLabels); + ReleaseMem(pVersion); + } +} + +DAPI_(HRESULT) VerParseVersion( + __in_z LPCWSTR wzVersion, + __in DWORD cchVersion, + __in BOOL fStrict, + __out VERUTIL_VERSION** ppVersion + ) +{ + HRESULT hr = S_OK; + VERUTIL_VERSION* pVersion = NULL; + LPCWSTR wzEnd = NULL; + LPCWSTR wzPartBegin = NULL; + LPCWSTR wzPartEnd = NULL; + BOOL fInvalid = FALSE; + BOOL fLastPart = FALSE; + BOOL fTrailingDot = FALSE; + BOOL fParsedVersionNumber = FALSE; + BOOL fExpectedReleaseLabels = FALSE; + DWORD iPart = 0; + + if (!wzVersion || !ppVersion) + { + ExitFunction1(hr = E_INVALIDARG); + } + + // Get string length if not provided. + if (0 == cchVersion) + { + cchVersion = lstrlenW(wzVersion); + } + + if (L'v' == *wzVersion || L'V' == *wzVersion) + { + ++wzVersion; + --cchVersion; + } + + pVersion = reinterpret_cast(MemAlloc(sizeof(VERUTIL_VERSION), TRUE)); + VerExitOnNull(pVersion, hr, E_OUTOFMEMORY, "Failed to allocate memory for Verutil version '%ls'.", wzVersion); + + hr = StrAllocString(&pVersion->sczVersion, wzVersion, cchVersion); + VerExitOnFailure(hr, "Failed to copy Verutil version string '%ls'.", wzVersion); + + wzVersion = wzPartBegin = wzPartEnd = pVersion->sczVersion; + + // Save end pointer. + wzEnd = wzVersion + cchVersion; + + // Parse version number + while (wzPartBegin < wzEnd) + { + fTrailingDot = FALSE; + + // Find end of part. + for (;;) + { + if (wzPartEnd >= wzEnd) + { + fLastPart = TRUE; + break; + } + + switch (*wzPartEnd) + { + case L'0': + case L'1': + case L'2': + case L'3': + case L'4': + case L'5': + case L'6': + case L'7': + case L'8': + case L'9': + ++wzPartEnd; + continue; + case L'.': + fTrailingDot = TRUE; + break; + case L'-': + case L'+': + fLastPart = TRUE; + break; + default: + fInvalid = TRUE; + break; + } + + break; + } + + if (wzPartBegin == wzPartEnd) + { + fInvalid = TRUE; + } + + if (fInvalid) + { + break; + } + + DWORD cchPart = 0; + hr = ::PtrdiffTToDWord(wzPartEnd - wzPartBegin, &cchPart); + if (FAILED(hr)) + { + fInvalid = TRUE; + break; + } + + // Parse version part. + UINT uPart = 0; + hr = StrStringToUInt32(wzPartBegin, cchPart, &uPart); + if (FAILED(hr)) + { + fInvalid = TRUE; + break; + } + + switch (iPart) + { + case 0: + pVersion->dwMajor = uPart; + break; + case 1: + pVersion->dwMinor = uPart; + break; + case 2: + pVersion->dwPatch = uPart; + break; + case 3: + pVersion->dwRevision = uPart; + break; + } + + if (fTrailingDot) + { + ++wzPartEnd; + } + wzPartBegin = wzPartEnd; + ++iPart; + + if (4 <= iPart || fLastPart) + { + fParsedVersionNumber = TRUE; + break; + } + } + + fInvalid |= !fParsedVersionNumber || fTrailingDot; + + if (!fInvalid && wzPartBegin < wzEnd && *wzPartBegin == L'-') + { + wzPartBegin = wzPartEnd = wzPartBegin + 1; + fExpectedReleaseLabels = TRUE; + fLastPart = FALSE; + } + + while (fExpectedReleaseLabels && wzPartBegin < wzEnd) + { + fTrailingDot = FALSE; + + // Find end of part. + for (;;) + { + if (wzPartEnd >= wzEnd) + { + fLastPart = TRUE; + break; + } + + if (*wzPartEnd >= L'0' && *wzPartEnd <= L'9' || + *wzPartEnd >= L'A' && *wzPartEnd <= L'Z' || + *wzPartEnd >= L'a' && *wzPartEnd <= L'z' || + *wzPartEnd == L'-') + { + ++wzPartEnd; + continue; + } + else if (*wzPartEnd == L'+') + { + fLastPart = TRUE; + } + else if (*wzPartEnd == L'.') + { + fTrailingDot = TRUE; + } + else + { + fInvalid = TRUE; + } + + break; + } + + if (wzPartBegin == wzPartEnd) + { + fInvalid = TRUE; + } + + if (fInvalid) + { + break; + } + + int cchLabel = 0; + hr = ::PtrdiffTToInt32(wzPartEnd - wzPartBegin, &cchLabel); + if (FAILED(hr) || 0 > cchLabel) + { + fInvalid = TRUE; + break; + } + + hr = MemReAllocArray(reinterpret_cast(&pVersion->rgReleaseLabels), pVersion->cReleaseLabels, sizeof(VERUTIL_VERSION_RELEASE_LABEL), GROW_RELEASE_LABELS - (pVersion->cReleaseLabels % GROW_RELEASE_LABELS)); + VerExitOnFailure(hr, "Failed to allocate memory for Verutil version release labels '%ls'", wzVersion); + + VERUTIL_VERSION_RELEASE_LABEL* pReleaseLabel = pVersion->rgReleaseLabels + pVersion->cReleaseLabels; + ++pVersion->cReleaseLabels; + + // Try to parse as number. + UINT uLabel = 0; + hr = StrStringToUInt32(wzPartBegin, cchLabel, &uLabel); + if (SUCCEEDED(hr)) + { + pReleaseLabel->fNumeric = TRUE; + pReleaseLabel->dwValue = uLabel; + } + + pReleaseLabel->cchLabelOffset = wzPartBegin - pVersion->sczVersion; + pReleaseLabel->cchLabel = cchLabel; + + if (fTrailingDot) + { + ++wzPartEnd; + } + wzPartBegin = wzPartEnd; + + if (fLastPart) + { + break; + } + } + + fInvalid |= fExpectedReleaseLabels && (!pVersion->cReleaseLabels || fTrailingDot); + + if (!fInvalid && wzPartBegin < wzEnd) + { + if (*wzPartBegin == L'+') + { + wzPartBegin = wzPartEnd = wzPartBegin + 1; + } + else + { + fInvalid = TRUE; + } + } + + if (fInvalid && fStrict) + { + ExitFunction1(hr = E_INVALIDARG); + } + + pVersion->cchMetadataOffset = min(wzPartBegin, wzEnd) - pVersion->sczVersion; + pVersion->fInvalid = fInvalid; + + *ppVersion = pVersion; + pVersion = NULL; + hr = S_OK; + +LExit: + ReleaseVerutilVersion(pVersion); + + return hr; +} + +DAPI_(HRESULT) VerVersionFromQword( + __in DWORD64 qwVersion, + __out VERUTIL_VERSION** ppVersion + ) +{ + HRESULT hr = S_OK; + VERUTIL_VERSION* pVersion = NULL; + + pVersion = reinterpret_cast(MemAlloc(sizeof(VERUTIL_VERSION), TRUE)); + VerExitOnNull(pVersion, hr, E_OUTOFMEMORY, "Failed to allocate memory for Verutil version from QWORD."); + + pVersion->dwMajor = (WORD)(qwVersion >> 48 & 0xffff); + pVersion->dwMinor = (WORD)(qwVersion >> 32 & 0xffff); + pVersion->dwPatch = (WORD)(qwVersion >> 16 & 0xffff); + pVersion->dwRevision = (WORD)(qwVersion & 0xffff); + + hr = StrAllocFormatted(&pVersion->sczVersion, L"%lu.%lu.%lu.%lu", pVersion->dwMajor, pVersion->dwMinor, pVersion->dwPatch, pVersion->dwRevision); + ExitOnFailure(hr, "Failed to allocate and format the version string."); + + pVersion->cchMetadataOffset = lstrlenW(pVersion->sczVersion); + + *ppVersion = pVersion; + pVersion = NULL; + +LExit: + ReleaseVerutilVersion(pVersion); + + return hr; +} + + +static int CompareDword( + __in const DWORD& dw1, + __in const DWORD& dw2 + ) +{ + int nResult = 0; + + if (dw1 > dw2) + { + nResult = 1; + } + else if (dw1 < dw2) + { + nResult = -1; + } + + return nResult; +} + +static HRESULT CompareReleaseLabel( + __in const VERUTIL_VERSION_RELEASE_LABEL* p1, + __in LPCWSTR wzVersion1, + __in const VERUTIL_VERSION_RELEASE_LABEL* p2, + __in LPCWSTR wzVersion2, + __out int* pnResult + ) +{ + HRESULT hr = S_OK; + int nResult = 0; + + if (p1 == p2) + { + ExitFunction(); + } + else if (p1 && !p2) + { + ExitFunction1(nResult = 1); + } + else if (!p1 && p2) + { + ExitFunction1(nResult = -1); + } + + if (p1->fNumeric) + { + if (p2->fNumeric) + { + nResult = CompareDword(p1->dwValue, p2->dwValue); + } + else + { + nResult = -1; + } + } + else + { + if (p2->fNumeric) + { + nResult = 1; + } + else + { + hr = CompareVersionSubstring(wzVersion1 + p1->cchLabelOffset, p1->cchLabel, wzVersion2 + p2->cchLabelOffset, p2->cchLabel, &nResult); + } + } + +LExit: + *pnResult = nResult; + + return hr; +} + +static HRESULT CompareVersionSubstring( + __in LPCWSTR wzString1, + __in int cchCount1, + __in LPCWSTR wzString2, + __in int cchCount2, + __out int* pnResult + ) +{ + HRESULT hr = S_OK; + int nResult = 0; + + nResult = ::CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, wzString1, cchCount1, wzString2, cchCount2); + if (!nResult) + { + VerExitOnLastError(hr, "Failed to compare version substrings"); + } + +LExit: + *pnResult = nResult - 2; + + return hr; +} -- cgit v1.2.3-55-g6feb