未验证 提交 5a9976ca 编写于 作者: R Rob LaDuca 提交者: GitHub

Fix handling of wildcard AssemblyVersion properties during markup compilation (#2691)

Use empty version strings for generation of URIs in InitializeComponent when a wildcard string is detected in AssemblyVersion.  Also add validation of versions strings passed in via the AssemblyVersion build property and an associated MarkupCompilation error when validation fails.
上级 9b995a60
// 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) { }
}
}
......@@ -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)
{
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(e).Throw();
}
if (Error != null)
{
XamlParseException xe = e as XamlParseException;
......@@ -2596,8 +2603,31 @@ private void GenerateInitializeComponent(bool isApp)
string uriPart = string.Empty;
string version = String.IsNullOrEmpty(AssemblyVersion) ? String.Empty : COMPONENT_DELIMITER + VER + AssemblyVersion;
string token = String.IsNullOrEmpty(AssemblyPublicKeyToken) ? String.Empty : COMPONENT_DELIMITER + AssemblyPublicKeyToken;
// 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 (!VersionHelper.TryParseAssemblyVersion(AssemblyVersion, allowWildcard: true, version: out _, out bool hasWildcard)
&& !string.IsNullOrWhiteSpace(AssemblyVersion))
{
throw new AssemblyVersionParseException(SR.Get(SRID.InvalidAssemblyVersion, AssemblyVersion));
}
// In .NET Framework (non-SDK-style projects), the process to use a wildcard AssemblyVersion is to do the following:
// - Modify the AssemblyVersionAttribute to a wildcard string (e.g. "1.2.*")
// - Set Deterministic to false in the build
// During MarkupCompilation, the AssemblyVersion property would not be set and WPF would correctly generate a resource URI without a version.
// In .NET Core/5 (or .NET Framework SDK-style projects), the same process can be used if GenerateAssemblyVersionAttribute is set to false in
// the build. However, this isn't really the idiomatic way to set the version for an assembly. Instead, developers are more likely to use the
// AssemblyVersion build property. If a developer explicitly sets the AssemblyVersion build property to a wildcard version string, we would use
// that as part of the URI here. 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;
string token = String.IsNullOrEmpty(AssemblyPublicKeyToken)
? String.Empty
: COMPONENT_DELIMITER + AssemblyPublicKeyToken;
uriPart = FORWARDSLASH + AssemblyName + version + token + COMPONENT_DELIMITER + COMPONENT + FORWARDSLASH + resourceID;
//
......
// 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/91571a3bb038e05e7bf2ab87510273a1017faed0/src/Compilers/Core/Portable/VersionHelper.cs
#nullable enable
using System;
using System.Diagnostics;
using System.Globalization;
namespace MS.Internal
{
internal static class VersionHelper
{
static readonly Version NullVersion = new Version(0, 0, 0, 0);
/// <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);
}
}
}
}
......@@ -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" />
......
......@@ -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>
......@@ -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>
\ No newline at end of file
......@@ -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">
<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>
......
......@@ -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>
......
......@@ -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>
......
......@@ -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>
......
......@@ -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>
......
......@@ -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>
......
......@@ -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>
......
......@@ -152,6 +152,11 @@
<target state="translated">Klasa InternalTypeHelper nie jest wymagana dla tego projektu. Plik make „{0}” jest pusty.</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: Nazwa klasy „{0}” jest nieprawidłowa dla zdefiniowanego lokalnie elementu głównego XAML.</target>
......
......@@ -152,6 +152,11 @@
<target state="translated">A classe InternalTypeHelper não é necessária para este projeto; esvazie o arquivo '{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: o nome da classe '{0}' não é válido para o elemento raiz XAML definido localmente.</target>
......
......@@ -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>
......
......@@ -152,6 +152,11 @@
<target state="translated">InternalTypeHelper sınıfı bu proje için gerekli değil, '{0}' dosyasını boşaltın.</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}' sınıf adı yerel olarak tanımlanmış XAML kök öğesi için geçerli değil.</target>
......
......@@ -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>
......
......@@ -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>
......
......@@ -69,7 +69,7 @@ internal enum XamlParseMode
internal class XamlParser
{
#if PBTCOMPILER
#region Constructors
#region Constructors
/// <summary>
/// Constructor that takes a stream and creates an XmlCompatibilityReader on it.
......@@ -96,7 +96,7 @@ internal class XamlParser
// entity expansion technique. In System.Xml V2.0, in order to provide protection against DTD DoS attacks there
// is the capability of turning off DTD parsing through the use of the ProhibitDtd property.
#pragma warning disable 0618
#pragma warning disable 0618
// CS0618: A class member was marked with the Obsolete attribute, such that a warning
// will be issued when the class member is referenced.
textReader.ProhibitDtd = true;
......@@ -123,9 +123,9 @@ protected XamlParser()
{
}
#endregion Constructors
#endregion Constructors
#region PublicMethods
#region PublicMethods
/// <summary>
......@@ -203,6 +203,13 @@ public bool ReadXaml(bool singleRecordMode)
}
else
{
// Don't treat an AssemblyVersion parsing error as a XamlParseException.
// Throw it back to the task execution.
if(e is AssemblyVersionParseException)
{
throw;
}
if (e is XamlParseException)
{
throw;
......@@ -531,9 +538,9 @@ public bool ReadXaml(bool singleRecordMode)
}
#endregion // PublicMethods
#endregion // PublicMethods
#region Virtuals
#region Virtuals
/// <summary>
/// Called when parsing begins
......@@ -1099,7 +1106,7 @@ public virtual void WriteConstructorParametersEnd(XamlConstructorParametersEndNo
}
#endregion Virtuals
#endregion Virtuals
/// <summary>
......@@ -1156,7 +1163,7 @@ void WriteDefAttributeCore(XamlDefAttributeNode xamlDefAttributeNode)
WriteDefAttribute(xamlDefAttributeNode);
}
#region Methods
#region Methods
// virtuals to override the default implementation. used by the compiler
// for internal virtuals review why not public as the others?
......@@ -1272,9 +1279,9 @@ internal bool IsXmlNamespaceSupported(string xmlNamespace, out string newXmlName
TokenReader.IsXmlDataIsland();
}
}
#endregion Methods
#endregion Methods
#region Properties
#region Properties
/// <summary>
/// TokenReader that is being used.
......@@ -1380,9 +1387,9 @@ internal virtual bool StrictParsing
}
#endregion Properties
#endregion Properties
#region Data
#region Data
......@@ -1407,7 +1414,7 @@ internal virtual bool StrictParsing
XamlReaderHelper.DefaultNamespaceURI,
XamlReaderHelper.DefinitionMetroNamespaceURI
};
#endregion Data
#endregion Data
#endif
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册