Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix handling of wildcard AssemblyVersion properties during markup compilation #2691

Merged
merged 15 commits into from
Apr 1, 2020
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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.

using System;

namespace MS.Internal
{
/// <summary>
/// A typed exception to allow parse errors on AssemblyVersions to flow to
/// the MarkupCompile task execution.
/// </summary>
internal class AssemblyVersionParseException : Exception
{
public AssemblyVersionParseException(string message) : base(message) { }
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,13 @@ static void ThrowCompilerExceptionImpl(string message)

internal void OnError(Exception e)
{
// Don't treat an AssemblyVersion parsing error as a XamlParseException.
// Throw it back to the task execution.
if(e is AssemblyVersionParseException)
{
throw e;
rladuca marked this conversation as resolved.
Show resolved Hide resolved
}

if (Error != null)
{
XamlParseException xe = e as XamlParseException;
Expand Down Expand Up @@ -2596,7 +2603,20 @@ private void GenerateInitializeComponent(bool isApp)

string uriPart = string.Empty;

string version = String.IsNullOrEmpty(AssemblyVersion) ? String.Empty : COMPONENT_DELIMITER + VER + AssemblyVersion;
bool hasWildcard = false;
rladuca marked this conversation as resolved.
Show resolved Hide resolved

// Attempt to parse out the AssemblyVersion if it exists. This validates that we can either use an empty version string (wildcards exist)
// or we can utilize the passed in string (valid parse).
if (!string.IsNullOrEmpty(AssemblyVersion)
&& !VersionHelper.TryParseAssemblyVersion(AssemblyVersion, allowWildcard: true, out Version _, out hasWildcard))
rladuca marked this conversation as resolved.
Show resolved Hide resolved
{
throw new AssemblyVersionParseException(SR.Get(SRID.InvalidAssemblyVersion, AssemblyVersion));
}

// If a developer explicitly sets the AssemblyVersion build property to a wildcard version string, we would use that as part of the URI here.
rladuca marked this conversation as resolved.
Show resolved Hide resolved
// This results in an error in Version.Parse during InitializeComponent's call tree. Instead, do as we would have when the developer sets a
// wildcard version string via AssemblyVersionAttribute and use an empty string.
string version = (hasWildcard || String.IsNullOrEmpty(AssemblyVersion)) ? String.Empty : COMPONENT_DELIMITER + VER + AssemblyVersion;
rladuca marked this conversation as resolved.
Show resolved Hide resolved
string token = String.IsNullOrEmpty(AssemblyPublicKeyToken) ? String.Empty : COMPONENT_DELIMITER + AssemblyPublicKeyToken;
uriPart = FORWARDSLASH + AssemblyName + version + token + COMPONENT_DELIMITER + COMPONENT + FORWARDSLASH + resourceID;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// 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.

// Modified from https://github.com/dotnet/roslyn/blob/master/src/Compilers/Core/Portable/VersionHelper.cs
rladuca marked this conversation as resolved.
Show resolved Hide resolved

#nullable enable

using System;
using System.Diagnostics;
using System.Globalization;

namespace MS.Internal
{
internal static class VersionHelper
{
static Version NullVersion = new Version(0, 0, 0, 0);
rladuca marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Parses a version string of the form "major [ '.' minor [ '.' build [ '.' revision ] ] ]".
/// </summary>
/// <param name="s">The version string to parse.</param>
/// <param name="version">If parsing succeeds, the parsed version. Otherwise a version that represents as much of the input as could be parsed successfully.</param>
/// <returns>True when parsing succeeds completely (i.e. every character in the string was consumed), false otherwise.</returns>
internal static bool TryParse(string s, out Version version)
{
return TryParse(s, allowWildcard: false, maxValue: ushort.MaxValue, allowPartialParse: true, version: out version, hasWildcard: out bool _);
}

/// <summary>
/// Parses a version string of the form "major [ '.' minor [ '.' ( '*' | ( build [ '.' ( '*' | revision ) ] ) ) ] ]"
/// as accepted by System.Reflection.AssemblyVersionAttribute.
/// </summary>
/// <param name="s">The version string to parse.</param>
/// <param name="allowWildcard">Indicates whether or not a wildcard is accepted as the terminal component.</param>
/// <param name="version">
/// If parsing succeeded, the parsed version. Otherwise a version instance with all parts set to zero.
/// If <paramref name="s"/> contains * the version build and/or revision numbers are set to <see cref="ushort.MaxValue"/>.
/// </param>
/// <param name="hasWildcard">If parsing succeeds, indicates if a wilcard character was found in the version.</param>
/// <returns>True when parsing succeeds completely (i.e. every character in the string was consumed), false otherwise.</returns>
internal static bool TryParseAssemblyVersion(string s, bool allowWildcard, out Version version, out bool hasWildcard)
{
return TryParse(s, allowWildcard: allowWildcard, maxValue: ushort.MaxValue - 1, allowPartialParse: false, version: out version, hasWildcard: out hasWildcard);
}

/// <summary>
/// Parses a version string of the form "major [ '.' minor [ '.' ( '*' | ( build [ '.' ( '*' | revision ) ] ) ) ] ]"
/// as accepted by System.Reflection.AssemblyVersionAttribute.
/// </summary>
/// <param name="s">The version string to parse.</param>
/// <param name="allowWildcard">Indicates whether or not we're parsing an assembly version string. If so, wildcards are accepted and each component must be less than 65535.</param>
/// <param name="maxValue">The maximum value that a version component may have.</param>
/// <param name="allowPartialParse">Allow the parsing of version elements where invalid characters exist. e.g. 1.2.2a.1</param>
/// <param name="version">
/// If parsing succeeded, the parsed version. When <paramref name="allowPartialParse"/> is true a version with values up to the first invalid character set. Otherwise a version with all parts set to zero.
/// If <paramref name="s"/> contains * and wildcard is allowed the version build and/or revision numbers are set to <see cref="ushort.MaxValue"/>.
/// </param>
/// <returns>True when parsing succeeds completely (i.e. every character in the string was consumed), false otherwise.</returns>
private static bool TryParse(string s, bool allowWildcard, ushort maxValue, bool allowPartialParse, out Version version, out bool hasWildcard)
{
Debug.Assert(!allowWildcard || maxValue < ushort.MaxValue);

if (string.IsNullOrWhiteSpace(s))
{
hasWildcard = false;
version = NullVersion;
return false;
}

string[] elements = s.Split('.');

// If the wildcard is being used, the first two elements must be specified explicitly, and
// the last must be a exactly single asterisk without whitespace.
hasWildcard = allowWildcard && elements[elements.Length - 1] == "*";

if ((hasWildcard && elements.Length < 3) || elements.Length > 4)
{
version = NullVersion;
return false;
}

ushort[] values = new ushort[4];
int lastExplicitValue = hasWildcard ? elements.Length - 1 : elements.Length;
bool parseError = false;
for (int i = 0; i < lastExplicitValue; i++)
{

if (!ushort.TryParse(elements[i], NumberStyles.None, CultureInfo.InvariantCulture, out values[i]) || values[i] > maxValue)
{
if (!allowPartialParse)
{
version = NullVersion;
return false;
}

parseError = true;

if (string.IsNullOrWhiteSpace(elements[i]))
{
values[i] = 0;
break;
}


if (values[i] > maxValue)
{
//The only way this can happen is if the value was 65536
//The old compiler would continue parsing from here
values[i] = 0;
continue;
}

bool invalidFormat = false;
System.Numerics.BigInteger number = 0;

//There could be an invalid character in the input so check for the presence of one and
//parse up to that point. examples of invalid characters are alphas and punctuation
for (var idx = 0; idx < elements[i].Length; idx++)
{
if (!char.IsDigit(elements[i][idx]))
{
invalidFormat = true;

TryGetValue(elements[i].Substring(0, idx), out values[i]);
break;
}
}

if (!invalidFormat)
{
//if we made it here then there weren't any alpha or punctuation chars in the input so the
//element is either greater than ushort.MaxValue or possibly a fullwidth unicode digit.
if (TryGetValue(elements[i], out values[i]))
{
//For this scenario the old compiler would continue processing the remaining version elements
//so continue processing
continue;
}
}

//Don't process any more of the version elements
break;
}
}




if (hasWildcard)
{
for (int i = lastExplicitValue; i < values.Length; i++)
{
values[i] = ushort.MaxValue;
}
}

version = new Version(values[0], values[1], values[2], values[3]);
return !parseError;
}

private static bool TryGetValue(string s, out ushort value)
{
System.Numerics.BigInteger number;
if (System.Numerics.BigInteger.TryParse(s, NumberStyles.None, CultureInfo.InvariantCulture, out number))
{
//The old compiler would take the 16 least significant bits and use their value as the output
//so we'll do that too.
value = (ushort)(number % 65536);
return true;
}

//One case that will cause us to end up here is when the input is a Fullwidth unicode digit
//so we'll always return zero
value = 0;
return false;
}

/// <summary>
/// If build and/or revision numbers are 65535 they are replaced with time-based values.
/// </summary>
public static Version? GenerateVersionFromPatternAndCurrentTime(DateTime time, Version pattern)
{
if (pattern == null || pattern.Revision != ushort.MaxValue)
{
return pattern;
}

// MSDN doc on the attribute:
// "The default build number increments daily. The default revision number is the number of seconds since midnight local time
// (without taking into account time zone adjustments for daylight saving time), divided by 2."
if (time == default(DateTime))
{
time = DateTime.Now;
}

int revision = (int)time.TimeOfDay.TotalSeconds / 2;

// 24 * 60 * 60 / 2 = 43200 < 65535
Debug.Assert(revision < ushort.MaxValue);

if (pattern.Build == ushort.MaxValue)
{
TimeSpan days = time.Date - new DateTime(2000, 1, 1);
int build = Math.Min(ushort.MaxValue, (int)days.TotalDays);

return new Version(pattern.Major, pattern.Minor, (ushort)build, (ushort)revision);
}
else
{
return new Version(pattern.Major, pattern.Minor, pattern.Build, (ushort)revision);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,12 @@

<Compile Include="MS\Internal\Localization\LocalizationDirectivesToLocFile.cs" />
<Compile Include="MS\Internal\Localization\LocalizationParserHooks.cs" />
<Compile Include="Ms\Internal\MarkupCompiler\CompilationUnit.cs" />
<Compile Include="Ms\Internal\MarkupCompiler\FileUnit.cs" />
<Compile Include="Ms\Internal\MarkupCompiler\MarkupCompiler.cs" />
<Compile Include="Ms\Internal\MarkupCompiler\ParserExtension.cs" />
<Compile Include="MS\Internal\MarkupCompiler\AssemblyVersionParseException.cs" />
<Compile Include="MS\Internal\MarkupCompiler\CompilationUnit.cs" />
<Compile Include="MS\Internal\MarkupCompiler\FileUnit.cs" />
<Compile Include="MS\Internal\MarkupCompiler\MarkupCompiler.cs" />
<Compile Include="MS\Internal\MarkupCompiler\ParserExtension.cs" />
<Compile Include="MS\Internal\MarkupCompiler\VersionHelper.cs" />
<Compile Include="MS\Internal\Shared\SourceFileInfo.cs" />
<Compile Include="MS\Internal\Tasks\Shared.cs" />
<Compile Include="MS\Internal\Tasks\TaskHelper.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@
<data name="MultipleSplashScreenImages" xml:space="preserve">
<value>MC1004: Project file cannot specify more than one SplashScreen element.</value>
</data>
<data name="InvalidAssemblyVersion" xml:space="preserve">
<value>MC1005: Invalid AssemblyVersion detected: {0}.</value>
</data>
<data name="InvalidCulture" xml:space="preserve">
<value>FC1001: The UICulture value '{0}' set in the project file is not valid.</value>
</data>
Expand Down Expand Up @@ -834,4 +837,4 @@
<data name="ParserMarkupExtensionMalformedBracketCharacers" xml:space="preserve">
<value>MC8002: BracketCharacter '{0}' at Line Number '{1}' and Line Position '{2}' does not have a corresponding opening/closing BracketCharacter.</value>
</data>
</root>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@
<target state="translated">Třída InternalTypeHelper není pro tento projekt požadována, vyprázdněte soubor {0}.</target>
<note />
</trans-unit>
<trans-unit id="InvalidAssemblyVersion">
rladuca marked this conversation as resolved.
Show resolved Hide resolved
<source>MC1005: Invalid AssemblyVersion detected: {0}.</source>
<target state="new">MC1005: Invalid AssemblyVersion detected: {0}.</target>
<note />
</trans-unit>
<trans-unit id="InvalidBaseClassName">
<source>MC6018: '{0}' class name is not valid for the locally defined XAML root element.</source>
<target state="translated">MC6018: Název třídy {0} je neplatný pro místně definovaný kořenový prvek XAML.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@
<target state="translated">Die InternalTypeHelper-Klasse ist für dieses Projekt nicht erforderlich, leeren Sie die Datei "{0}".</target>
<note />
</trans-unit>
<trans-unit id="InvalidAssemblyVersion">
<source>MC1005: Invalid AssemblyVersion detected: {0}.</source>
<target state="new">MC1005: Invalid AssemblyVersion detected: {0}.</target>
<note />
</trans-unit>
<trans-unit id="InvalidBaseClassName">
<source>MC6018: '{0}' class name is not valid for the locally defined XAML root element.</source>
<target state="translated">MC6018: Klassenname "{0}" ist für das lokal definierte XAML-Stammelement ungültig.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@
<target state="translated">No se requiere la clase InternalTypeHelper para este proyecto, vacíe el archivo '{0}'.</target>
<note />
</trans-unit>
<trans-unit id="InvalidAssemblyVersion">
<source>MC1005: Invalid AssemblyVersion detected: {0}.</source>
<target state="new">MC1005: Invalid AssemblyVersion detected: {0}.</target>
<note />
</trans-unit>
<trans-unit id="InvalidBaseClassName">
<source>MC6018: '{0}' class name is not valid for the locally defined XAML root element.</source>
<target state="translated">MC6018: el nombre de clase '{0}' no es válido para el elemento raíz XAML definido localmente.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@
<target state="translated">Classe InternalTypeHelper non obligatoire pour ce projet, fichier Make '{0}' vide.</target>
<note />
</trans-unit>
<trans-unit id="InvalidAssemblyVersion">
<source>MC1005: Invalid AssemblyVersion detected: {0}.</source>
<target state="new">MC1005: Invalid AssemblyVersion detected: {0}.</target>
<note />
</trans-unit>
<trans-unit id="InvalidBaseClassName">
<source>MC6018: '{0}' class name is not valid for the locally defined XAML root element.</source>
<target state="translated">MC6018 : nom de classe '{0}' non valide pour l'élément racine XAML défini localement.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@
<target state="translated">La classe InternalTypeHelper non è richiesta per questo progetto, file make '{0}' vuoto.</target>
<note />
</trans-unit>
<trans-unit id="InvalidAssemblyVersion">
<source>MC1005: Invalid AssemblyVersion detected: {0}.</source>
<target state="new">MC1005: Invalid AssemblyVersion detected: {0}.</target>
<note />
</trans-unit>
<trans-unit id="InvalidBaseClassName">
<source>MC6018: '{0}' class name is not valid for the locally defined XAML root element.</source>
<target state="translated">MC6018: il nome di classe '{0}' non è valido per l'elemento radice XAML definito in locale.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@
<target state="translated">このプロジェクトには、InternalTypeHelper クラスは不要です。ファイル '{0}' を空にしてください。</target>
<note />
</trans-unit>
<trans-unit id="InvalidAssemblyVersion">
<source>MC1005: Invalid AssemblyVersion detected: {0}.</source>
<target state="new">MC1005: Invalid AssemblyVersion detected: {0}.</target>
<note />
</trans-unit>
<trans-unit id="InvalidBaseClassName">
<source>MC6018: '{0}' class name is not valid for the locally defined XAML root element.</source>
<target state="translated">MC6018: '{0}' クラス名は、ローカルで定義された XAML ルート要素に対して無効です。</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@
<target state="translated">이 프로젝트에 InternalTypeHelper 클래스가 필요하지 않습니다. '{0}' 파일을 비우십시오.</target>
<note />
</trans-unit>
<trans-unit id="InvalidAssemblyVersion">
<source>MC1005: Invalid AssemblyVersion detected: {0}.</source>
<target state="new">MC1005: Invalid AssemblyVersion detected: {0}.</target>
<note />
</trans-unit>
<trans-unit id="InvalidBaseClassName">
<source>MC6018: '{0}' class name is not valid for the locally defined XAML root element.</source>
<target state="translated">MC6018: 로컬에 정의된 XAML 루트 요소에 '{0}' 클래스 이름을 사용할 수 없습니다.</target>
Expand Down
Loading