diff --git a/src/Build.UnitTests/BackEnd/BinaryTranslator_Tests.cs b/src/Build.UnitTests/BackEnd/BinaryTranslator_Tests.cs index 87c18c4b5b9..e6ac8089ea8 100644 --- a/src/Build.UnitTests/BackEnd/BinaryTranslator_Tests.cs +++ b/src/Build.UnitTests/BackEnd/BinaryTranslator_Tests.cs @@ -3,8 +3,12 @@ using System; using System.Collections.Generic; +using System.Configuration.Assemblies; +using System.Globalization; using Microsoft.Build.BackEnd; using System.IO; +using System.Reflection; +using Shouldly; using Xunit; namespace Microsoft.Build.UnitTests.BackEnd @@ -431,6 +435,163 @@ public void TestSerializeDictionaryStringTNoComparerNull() Assert.Equal(value, deserializedValue); } + [Theory] + [InlineData("en")] + [InlineData("en-US")] + [InlineData("en-CA")] + [InlineData("zh-HK")] + [InlineData("sr-Cyrl-CS")] + public void CultureInfo(string name) + { + CultureInfo value = new CultureInfo(name); + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + CultureInfo deserializedValue = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + deserializedValue.ShouldBe(value); + } + + [Fact] + public void CultureInfoAsNull() + { + CultureInfo value = null; + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + CultureInfo deserializedValue = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + deserializedValue.ShouldBeNull(); + } + + [Theory] + [InlineData("1.2")] + [InlineData("1.2.3")] + [InlineData("1.2.3.4")] + public void Version(string version) + { + Version value = new Version(version); + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + Version deserializedValue = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + deserializedValue.ShouldBe(value); + } + + [Fact] + public void VersionAsNull() + { + Version value = null; + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + Version deserializedValue = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + deserializedValue.ShouldBeNull(); + } + + [Fact] + public void HashSetOfT() + { + HashSet values = new() + { + new BaseClass(1), + new BaseClass(2), + null + }; + TranslationHelpers.GetWriteTranslator().TranslateHashSet(ref values, BaseClass.FactoryForDeserialization, capacity => new()); + + HashSet deserializedValues = null; + TranslationHelpers.GetReadTranslator().TranslateHashSet(ref deserializedValues, BaseClass.FactoryForDeserialization, capacity => new()); + + deserializedValues.ShouldBe(values, ignoreOrder: true); + } + + [Fact] + public void HashSetOfTAsNull() + { + HashSet value = null; + TranslationHelpers.GetWriteTranslator().TranslateHashSet(ref value, BaseClass.FactoryForDeserialization, capacity => new()); + + HashSet deserializedValue = null; + TranslationHelpers.GetReadTranslator().TranslateHashSet(ref deserializedValue, BaseClass.FactoryForDeserialization, capacity => new()); + + deserializedValue.ShouldBeNull(); + } + + [Fact] + public void AssemblyNameAsNull() + { + AssemblyName value = null; + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + AssemblyName deserializedValue = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + deserializedValue.ShouldBeNull(); + } + + [Fact] + public void AssemblyNameWithAllFields() + { + AssemblyName value = new() + { + Name = "a", + Version = new Version(1, 2, 3), + Flags = AssemblyNameFlags.PublicKey, + ProcessorArchitecture = ProcessorArchitecture.X86, + CultureInfo = new CultureInfo("zh-HK"), + HashAlgorithm = System.Configuration.Assemblies.AssemblyHashAlgorithm.SHA256, + VersionCompatibility = AssemblyVersionCompatibility.SameMachine, + CodeBase = "C:\\src", + KeyPair = new StrongNameKeyPair(new byte[] { 4, 3, 2, 1 }), + ContentType = AssemblyContentType.WindowsRuntime, + CultureName = "zh-HK", + }; + value.SetPublicKey(new byte[]{ 3, 2, 1}); + value.SetPublicKeyToken(new byte[] { 8, 7, 6, 5, 4, 3, 2, 1 }); + + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + AssemblyName deserializedValue = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + HelperAssertAssemblyNameEqual(value, deserializedValue); + } + + [Fact] + public void AssemblyNameWithMinimalFields() + { + AssemblyName value = new(); + + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + AssemblyName deserializedValue = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + HelperAssertAssemblyNameEqual(value, deserializedValue); + } + + /// + /// Assert two AssemblyName objects values are same. + /// Ignoring KeyPair, ContentType, CultureName as those are not serialized + /// + private static void HelperAssertAssemblyNameEqual(AssemblyName expected, AssemblyName actual) + { + actual.Name.ShouldBe(expected.Name); + actual.Version.ShouldBe(expected.Version); + actual.Flags.ShouldBe(expected.Flags); + actual.ProcessorArchitecture.ShouldBe(expected.ProcessorArchitecture); + actual.CultureInfo.ShouldBe(expected.CultureInfo); + actual.HashAlgorithm.ShouldBe(expected.HashAlgorithm); + actual.VersionCompatibility.ShouldBe(expected.VersionCompatibility); + actual.CodeBase.ShouldBe(expected.CodeBase); + + actual.GetPublicKey().ShouldBe(expected.GetPublicKey()); + actual.GetPublicKeyToken().ShouldBe(expected.GetPublicKeyToken()); + } + /// /// Helper for bool serialization. /// @@ -610,6 +771,24 @@ protected BaseClass() { } + protected bool Equals(BaseClass other) + { + return _baseValue == other._baseValue; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((BaseClass) obj); + } + + public override int GetHashCode() + { + return _baseValue; + } + /// /// Gets a comparer. /// diff --git a/src/Shared/AssemblyNameExtension.cs b/src/Shared/AssemblyNameExtension.cs index 1d4f2a4bf71..8c62a178e05 100644 --- a/src/Shared/AssemblyNameExtension.cs +++ b/src/Shared/AssemblyNameExtension.cs @@ -9,6 +9,7 @@ using System.Configuration.Assemblies; using System.Runtime.Serialization; using System.IO; +using Microsoft.Build.BackEnd; #if FEATURE_ASSEMBLYLOADCONTEXT using System.Reflection.PortableExecutable; using System.Reflection.Metadata; @@ -54,7 +55,7 @@ internal enum PartialComparisonFlags : int /// between the two is done lazily on demand. /// [Serializable] - internal sealed class AssemblyNameExtension : ISerializable, IEquatable + internal sealed class AssemblyNameExtension : ISerializable, IEquatable, ITranslatable { private AssemblyName asAssemblyName = null; private string asString = null; @@ -173,6 +174,14 @@ private AssemblyNameExtension(SerializationInfo info, StreamingContext context) remappedFrom = (HashSet) info.GetValue("remapped", typeof(HashSet)); } + /// + /// Ctor for deserializing from state file (custom binary serialization) using translator. + /// + internal AssemblyNameExtension(ITranslator translator) : this() + { + Translate(translator); + } + /// /// To be used as a delegate. Gets the AssemblyName of the given file. /// @@ -251,10 +260,18 @@ private void InitializeRemappedFrom() { if (remappedFrom == null) { - remappedFrom = new HashSet(AssemblyNameComparer.GenericComparerConsiderRetargetable); + remappedFrom = CreateRemappedFrom(); } } + /// + /// Create remappedFrom HashSet. Used by deserialization as well. + /// + private static HashSet CreateRemappedFrom() + { + return new HashSet(AssemblyNameComparer.GenericComparerConsiderRetargetable); + } + /// /// Assume there is a string version, create the AssemblyName version. /// @@ -993,5 +1010,23 @@ public void GetObjectData(SerializationInfo info, StreamingContext context) info.AddValue("immutable", immutable); info.AddValue("remapped", remappedFrom); } + + /// + /// Reads/writes this class + /// + /// + public void Translate(ITranslator translator) + { + translator.Translate(ref asAssemblyName); + translator.Translate(ref asString); + translator.Translate(ref isSimpleName); + translator.Translate(ref hasProcessorArchitectureInFusionName); + translator.Translate(ref immutable); + + // TODO: consider some kind of protection against infinite loop during serialization, hint: pre serialize check for cycle in graph + translator.TranslateHashSet(ref remappedFrom, + (ITranslator t) => new AssemblyNameExtension(t), + (int capacity) => CreateRemappedFrom()); + } } } diff --git a/src/Shared/TranslatorHelpers.cs b/src/Shared/TranslatorHelpers.cs index 130ad05d9cd..9cab3485c97 100644 --- a/src/Shared/TranslatorHelpers.cs +++ b/src/Shared/TranslatorHelpers.cs @@ -1,7 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using System.Collections.Generic; +using System.Configuration.Assemblies; +using System.Globalization; +using System.Reflection; +using AssemblyHashAlgorithm = System.Configuration.Assemblies.AssemblyHashAlgorithm; namespace Microsoft.Build.BackEnd { @@ -40,12 +45,12 @@ public static void Translate( static ObjectTranslator AdaptFactory(NodePacketValueFactory valueFactory) where T : ITranslatable { - void Translate(ITranslator translator, ref T objectToTranslate) + void TranslateUsingValueFactory(ITranslator translator, ref T objectToTranslate) { - TranslatorHelpers.Translate(translator, ref objectToTranslate, valueFactory); + translator.Translate(ref objectToTranslate, valueFactory); } - return Translate; + return TranslateUsingValueFactory; } public static void Translate( @@ -102,5 +107,168 @@ public static void TranslateDictionary( { translator.TranslateDictionary(ref dictionary, AdaptFactory(valueFactory), collectionCreator); } + + public static void TranslateHashSet( + this ITranslator translator, + ref HashSet hashSet, + NodePacketValueFactory valueFactory, + NodePacketCollectionCreator> collectionFactory) where T : class, ITranslatable + { + if (!translator.TranslateNullable(hashSet)) + return; + + int count = default; + if (translator.Mode == TranslationDirection.WriteToStream) + { + count = hashSet.Count; + } + translator.Translate(ref count); + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + hashSet = collectionFactory(count); + for (int i = 0; i < count; i++) + { + T value = default; + translator.Translate(ref value, valueFactory); + hashSet.Add(value); + } + } + + if (translator.Mode == TranslationDirection.WriteToStream) + { + foreach (T item in hashSet) + { + T value = item; + translator.Translate(ref value, valueFactory); + } + } + } + + public static void Translate(this ITranslator translator, ref CultureInfo cultureInfo) + { + if (!translator.TranslateNullable(cultureInfo)) + return; + + int lcid = default; + + if (translator.Mode == TranslationDirection.WriteToStream) + { + lcid = cultureInfo.LCID; + } + + translator.Translate(ref lcid); + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + cultureInfo = new CultureInfo(lcid); + } + } + + public static void Translate(this ITranslator translator, ref Version version) + { + if (!translator.TranslateNullable(version)) + return; + + int major = 0; + int minor = 0; + int build = 0; + int revision = 0; + + if (translator.Mode == TranslationDirection.WriteToStream) + { + major = version.Major; + minor = version.Minor; + build = version.Build; + revision = version.Revision; + } + + translator.Translate(ref major); + translator.Translate(ref minor); + translator.Translate(ref build); + translator.Translate(ref revision); + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + if (build < 0) + { + version = new Version(major, minor); + } + else if (revision < 0) + { + version = new Version(major, minor, build); + } + else + { + version = new Version(major, minor, build, revision); + } + } + } + + public static void Translate(this ITranslator translator, ref AssemblyName assemblyName) + { + if (!translator.TranslateNullable(assemblyName)) + return; + + string name = null; + Version version = null; + AssemblyNameFlags flags = default; + ProcessorArchitecture processorArchitecture = default; + CultureInfo cultureInfo = null; + AssemblyHashAlgorithm hashAlgorithm = default; + AssemblyVersionCompatibility versionCompatibility = default; + string codeBase = null; + + byte[] publicKey = null; + byte[] publicKeyToken = null; + + if (translator.Mode == TranslationDirection.WriteToStream) + { + name = assemblyName.Name; + version = assemblyName.Version; + flags = assemblyName.Flags; + processorArchitecture = assemblyName.ProcessorArchitecture; + cultureInfo = assemblyName.CultureInfo; + hashAlgorithm = assemblyName.HashAlgorithm; + versionCompatibility = assemblyName.VersionCompatibility; + codeBase = assemblyName.CodeBase; + + publicKey = assemblyName.GetPublicKey(); // TODO: no need to serialize, public key is not used anywhere in context of RAR, only public key token + publicKeyToken = assemblyName.GetPublicKeyToken(); + } + + translator.Translate(ref name); + translator.Translate(ref version); + translator.TranslateEnum(ref flags, (int)flags); + translator.TranslateEnum(ref processorArchitecture, (int)processorArchitecture); + translator.Translate(ref cultureInfo); + translator.TranslateEnum(ref hashAlgorithm, (int)hashAlgorithm); + translator.TranslateEnum(ref versionCompatibility, (int)versionCompatibility); + translator.Translate(ref codeBase); + + translator.Translate(ref publicKey); + translator.Translate(ref publicKeyToken); + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + assemblyName = new AssemblyName + { + Name = name, + Version = version, + Flags = flags, + ProcessorArchitecture = processorArchitecture, + CultureInfo = cultureInfo, + HashAlgorithm = hashAlgorithm, + VersionCompatibility = versionCompatibility, + CodeBase = codeBase, + // AssemblyName.KeyPair is not used anywhere, additionally StrongNameKeyPair is not supported in .net core 5- + // and throws platform not supported exception when serialized or deserialized + KeyPair = null, + }; + + assemblyName.SetPublicKey(publicKey); + assemblyName.SetPublicKeyToken(publicKeyToken); + } + } } } diff --git a/src/Shared/UnitTests/AssemblyNameEx_Tests.cs b/src/Shared/UnitTests/AssemblyNameEx_Tests.cs index 2c804fa0320..120adbb9ed4 100644 --- a/src/Shared/UnitTests/AssemblyNameEx_Tests.cs +++ b/src/Shared/UnitTests/AssemblyNameEx_Tests.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Reflection; using System.Runtime.Serialization.Formatters.Binary; +using Microsoft.Build.BackEnd; using Microsoft.Build.Shared; using Shouldly; using Xunit; @@ -740,9 +741,55 @@ public void VerifyAssemblyNameExSerializationWithRemappedFrom() assemblyNameDeserialized.RemappedFromEnumerator.Count().ShouldBe(1); assemblyNameDeserialized.RemappedFromEnumerator.First().ShouldBe(assemblyRemappedFrom); } - } -} + [Theory] + [InlineData("System.Xml")] + [InlineData("System.XML, Version=2.0.0.0")] + [InlineData("System.Xml, Culture=de-DE")] + [InlineData("System.Xml, Version=10.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a, Retargetable=Yes")] + [InlineData("System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")] + public void VerifyAssemblyNameExSerializationByTranslator(string assemblyName) + { + AssemblyNameExtension assemblyNameOriginal = new AssemblyNameExtension(assemblyName); + AssemblyNameExtension assemblyNameDeserialized = null; + + MemoryStream serializationStream = new MemoryStream(); + ITranslator writeTranslator = BinaryTranslator.GetWriteTranslator(serializationStream); + + writeTranslator.Translate(ref assemblyNameOriginal, (ITranslator t) => new AssemblyNameExtension(t)); + + serializationStream.Seek(0, SeekOrigin.Begin); + ITranslator readTranslator = BinaryTranslator.GetReadTranslator(serializationStream, null); + + readTranslator.Translate(ref assemblyNameDeserialized, (ITranslator t) => new AssemblyNameExtension(t)); + + assemblyNameDeserialized.ShouldBe(assemblyNameOriginal); + } + + [Fact] + public void VerifyAssemblyNameExSerializationWithRemappedFromByTranslator() + { + AssemblyNameExtension assemblyNameOriginal = new AssemblyNameExtension("System.Xml, Version=10.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension assemblyRemappedFrom = new AssemblyNameExtension("System.Xml, Version=9.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a"); + assemblyRemappedFrom.MarkImmutable(); + assemblyNameOriginal.AddRemappedAssemblyName(assemblyRemappedFrom); + assemblyNameOriginal.RemappedFromEnumerator.Count().ShouldBe(1); + + AssemblyNameExtension assemblyNameDeserialized = null; + MemoryStream serializationStream = new MemoryStream(); + ITranslator writeTranslator = BinaryTranslator.GetWriteTranslator(serializationStream); + writeTranslator.Translate(ref assemblyNameOriginal, (ITranslator t) => new AssemblyNameExtension(t)); + serializationStream.Seek(0, SeekOrigin.Begin); + ITranslator readTranslator = BinaryTranslator.GetReadTranslator(serializationStream, null); + + readTranslator.Translate(ref assemblyNameDeserialized, (ITranslator t) => new AssemblyNameExtension(t)); + + assemblyNameDeserialized.Equals(assemblyNameOriginal).ShouldBeTrue(); + assemblyNameDeserialized.RemappedFromEnumerator.Count().ShouldBe(1); + assemblyNameDeserialized.RemappedFromEnumerator.First().ShouldBe(assemblyRemappedFrom); + } + } +} diff --git a/src/Tasks.UnitTests/AssemblyDependency/CacheFileSamples/Microsoft.VisualStudio.LanguageServices.Implementation.csprojAssemblyReference.cache b/src/Tasks.UnitTests/AssemblyDependency/CacheFileSamples/Microsoft.VisualStudio.LanguageServices.Implementation.csprojAssemblyReference.cache new file mode 100644 index 00000000000..b9afa33ceb9 Binary files /dev/null and b/src/Tasks.UnitTests/AssemblyDependency/CacheFileSamples/Microsoft.VisualStudio.LanguageServices.Implementation.csprojAssemblyReference.cache differ diff --git a/src/Tasks.UnitTests/AssemblyDependency/ResolveAssemblyReferenceCacheSerialization.cs b/src/Tasks.UnitTests/AssemblyDependency/ResolveAssemblyReferenceCacheSerialization.cs new file mode 100644 index 00000000000..77a9bf0452a --- /dev/null +++ b/src/Tasks.UnitTests/AssemblyDependency/ResolveAssemblyReferenceCacheSerialization.cs @@ -0,0 +1,219 @@ +using System; +using System.IO; +using System.Reflection; +using System.Runtime.Versioning; +using Microsoft.Build.Shared; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.ResolveAssemblyReference_Tests +{ + public class ResolveAssemblyReferenceCacheSerialization : IDisposable + { + // Maintain this two in sync with the constant in SystemState + private static readonly byte[] TranslateContractSignature = { (byte)'M', (byte)'B', (byte)'R', (byte)'S', (byte)'C' }; // Microsoft Build RAR State Cache + private static readonly byte TranslateContractVersion = 0x01; + + private readonly string _rarCacheFile; + private readonly TaskLoggingHelper _taskLoggingHelper; + + public ResolveAssemblyReferenceCacheSerialization() + { + var tempPath = Path.GetTempPath(); + _rarCacheFile = Path.Combine(tempPath, Guid.NewGuid() + ".UnitTest.RarCache"); + _taskLoggingHelper = new TaskLoggingHelper(new MockEngine(), "TaskA") + { + TaskResources = AssemblyResources.PrimaryResources + }; + } + + public void Dispose() + { + if (File.Exists(_rarCacheFile)) + { + FileUtilities.DeleteNoThrow(_rarCacheFile); + } + } + + [Fact] + public void RoundTripEmptyState() + { + SystemState systemState = new(); + + systemState.SerializeCacheByTranslator(_rarCacheFile, _taskLoggingHelper); + + var deserialized = SystemState.DeserializeCacheByTranslator(_rarCacheFile, _taskLoggingHelper); + + deserialized.ShouldNotBeNull(); + } + + [Fact] + public void WrongFileSignature() + { + SystemState systemState = new(); + + for (int i = 0; i < TranslateContractSignature.Length; i++) + { + systemState.SerializeCacheByTranslator(_rarCacheFile, _taskLoggingHelper); + using (var cacheStream = new FileStream(_rarCacheFile, FileMode.Open, FileAccess.ReadWrite)) + { + cacheStream.Seek(i, SeekOrigin.Begin); + cacheStream.WriteByte(0); + cacheStream.Close(); + } + + var deserialized = SystemState.DeserializeCacheByTranslator(_rarCacheFile, _taskLoggingHelper); + + deserialized.ShouldBeNull(); + } + } + + [Fact] + public void WrongFileVersion() + { + SystemState systemState = new(); + + systemState.SerializeCacheByTranslator(_rarCacheFile, _taskLoggingHelper); + using (var cacheStream = new FileStream(_rarCacheFile, FileMode.Open, FileAccess.ReadWrite)) + { + cacheStream.Seek(TranslateContractSignature.Length, SeekOrigin.Begin); + cacheStream.WriteByte((byte) (TranslateContractVersion + 1)); + cacheStream.Close(); + } + + var deserialized = SystemState.DeserializeCacheByTranslator(_rarCacheFile, _taskLoggingHelper); + + deserialized.ShouldBeNull(); + } + + [Fact] + public void CorrectFileSignature() + { + SystemState systemState = new(); + + for (int i = 0; i < TranslateContractSignature.Length; i++) + { + systemState.SerializeCacheByTranslator(_rarCacheFile, _taskLoggingHelper); + using (var cacheStream = new FileStream(_rarCacheFile, FileMode.Open, FileAccess.ReadWrite)) + { + cacheStream.Seek(i, SeekOrigin.Begin); + cacheStream.WriteByte(TranslateContractSignature[i]); + cacheStream.Close(); + } + + var deserialized = SystemState.DeserializeCacheByTranslator(_rarCacheFile, _taskLoggingHelper); + + deserialized.ShouldNotBeNull(); + } + } + + [Fact] + public void CorrectFileVersion() + { + SystemState systemState = new(); + + systemState.SerializeCacheByTranslator(_rarCacheFile, _taskLoggingHelper); + using (var cacheStream = new FileStream(_rarCacheFile, FileMode.Open, FileAccess.ReadWrite)) + { + cacheStream.Seek(TranslateContractSignature.Length, SeekOrigin.Begin); + cacheStream.WriteByte(TranslateContractVersion); + cacheStream.Close(); + } + + var deserialized = SystemState.DeserializeCacheByTranslator(_rarCacheFile, _taskLoggingHelper); + + deserialized.ShouldNotBeNull(); + } + + [Fact] + public void VerifySampleStateDeserialization() + { + // This test might also fail when binary format is modified. + // Any change in SystemState and child class ITranslatable implementation will most probably make this fail. + // To fix it, file referred by 'sampleName' needs to be recaptured and constant bellow modified to reflect + // the content of that cache. + // This sample was captured by compiling https://github.com/dotnet/roslyn/commit/f8107de2a94a01e96ac3d7c1f225acbb61e18830 + const string sampleName = "Microsoft.VisualStudio.LanguageServices.Implementation.csprojAssemblyReference.cache"; + const string expectedAssemblyPath = @"C:\Users\rokon\.nuget\packages\microsoft.visualstudio.codeanalysis.sdk.ui\15.8.27812-alpha\lib\net46\Microsoft.VisualStudio.CodeAnalysis.Sdk.UI.dll"; + const long expectedAssemblyLastWriteTimeTicks = 636644382480000000; + const string expectedAssemblyName = "Microsoft.VisualStudio.CodeAnalysis.Sdk.UI, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"; + const string expectedFrameworkName = ".NETFramework,Version=v4.5"; + var expectedDependencies = new[] + { + "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.CodeAnalysis, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.DeveloperTools, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "Microsoft.VisualStudio.Shell.Interop, Version=7.1.40304.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "EnvDTE, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.CodeAnalysis.Sdk, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.Build.Framework, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.Text.Logic, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.Text.UI, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.Text.Data, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.Text.UI.Wpf, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.ComponentModelHost, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.VSHelp, Version=7.0.3300.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.Shell.Interop.11.0, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.VCProjectEngine, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.Shell.15.0, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.OLE.Interop, Version=7.1.40304.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "Microsoft.VisualStudio.TextManager.Interop, Version=7.1.40304.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "EnvDTE80, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "Microsoft.VisualStudio.VirtualTreeGrid, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.Shell.Interop.8.0, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "Microsoft.VisualStudio.Editor, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + }; + + + CopyResourceSampleFileIntoRarCacheFile($@"AssemblyDependency\CacheFileSamples\{sampleName}"); + + var deserializedByTranslator = SystemState.DeserializeCacheByTranslator(_rarCacheFile, _taskLoggingHelper); + deserializedByTranslator.ShouldNotBeNull(); + + deserializedByTranslator.SetGetLastWriteTime(path => + { + if (path != expectedAssemblyPath) + throw new InvalidOperationException("Unexpected file name for this test case"); + + return new DateTime(expectedAssemblyLastWriteTimeTicks, DateTimeKind.Utc); + }); + + GetAssemblyName getAssemblyName = deserializedByTranslator.CacheDelegate((GetAssemblyName)null); + GetAssemblyMetadata getAssemblyMetadata = deserializedByTranslator.CacheDelegate((GetAssemblyMetadata)null); + + var assemblyName = getAssemblyName(expectedAssemblyPath); + getAssemblyMetadata(expectedAssemblyPath, null, + out AssemblyNameExtension[] dependencies, + out string[] scatterFiles, + out FrameworkName frameworkNameAttribute); + + + assemblyName.ShouldNotBeNull(); + assemblyName.ShouldBe(new AssemblyNameExtension(expectedAssemblyName, false)); + scatterFiles.ShouldBeEmpty(); + frameworkNameAttribute.ShouldBe(new FrameworkName(expectedFrameworkName)); + dependencies.ShouldNotBeNull(); + expectedDependencies.ShouldBe(expectedDependencies, ignoreOrder: true); + } + + private void CopyResourceSampleFileIntoRarCacheFile(string name) + { + Assembly asm = this.GetType().Assembly; + var resource = string.Format($"{asm.GetName().Name}.{name.Replace("\\", ".")}"); + using Stream resourceStream = asm.GetManifestResourceStream(resource); + if (resourceStream == null) + throw new InvalidOperationException($"Resource '{resource}' has not been found."); + + using FileStream rarCacheFile = new FileStream(_rarCacheFile, FileMode.CreateNew); + + resourceStream.CopyTo(rarCacheFile); + } + } +} diff --git a/src/Tasks.UnitTests/AssemblyDependency/TaskTranslatorHelpers.cs b/src/Tasks.UnitTests/AssemblyDependency/TaskTranslatorHelpers.cs new file mode 100644 index 00000000000..bfb9dd55cb6 --- /dev/null +++ b/src/Tasks.UnitTests/AssemblyDependency/TaskTranslatorHelpers.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Versioning; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Tasks; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.ResolveAssemblyReference_Tests +{ + public class TaskTranslatorHelpers + { + MemoryStream _serializationStream; + + [Fact] + public void NullFrameworkName() + { + FrameworkName value = null; + + GetWriteTranslator().Translate(ref value); + GetReadTranslator().Translate(ref value); + + value.ShouldBeNull(); + } + + [Theory] + [MemberData(nameof(SampleFrameworkNames))] + public void ValidFrameworkName(FrameworkName value) + { + FrameworkName deserialized = null; + + GetWriteTranslator().Translate(ref value); + GetReadTranslator().Translate(ref deserialized); + + deserialized.ShouldNotBeNull(); + deserialized.ShouldBe(value); + } + + public static IEnumerable SampleFrameworkNames => + new List + { + new object[] { new FrameworkName("X, Version=3.4.5") }, + new object[] { new FrameworkName("X, Version=3.4, Profile=Compact") }, + new object[] { new FrameworkName("Y", new Version(1, 2, 3)) }, + new object[] { new FrameworkName("Z", new Version(1, 2, 3), "P") }, + }; + + private ITranslator GetReadTranslator() + { + if (_serializationStream == null) + throw new InvalidOperationException("GetWriteTranslator has to be called before GetReadTranslator"); + + _serializationStream.Seek(0, SeekOrigin.Begin); + return BinaryTranslator.GetReadTranslator(_serializationStream, null); + } + + private ITranslator GetWriteTranslator() + { + _serializationStream = new MemoryStream(); + return BinaryTranslator.GetWriteTranslator(_serializationStream); + } + } +} diff --git a/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj b/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj index cd00af8329e..1c0521195bb 100644 --- a/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj +++ b/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj @@ -11,6 +11,10 @@ $(DefineConstants);MICROSOFT_BUILD_TASKS_UNITTESTS + + + + @@ -35,7 +39,6 @@ - true @@ -60,6 +63,7 @@ + App.config diff --git a/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs b/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs index d03366aa29a..4844d757611 100644 --- a/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs +++ b/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs @@ -1886,7 +1886,7 @@ private void LogConflict(Reference reference, string fusionName, StringBuilder l /// private void ReadStateFile() { - _cache = (SystemState)StateFileBase.DeserializeCache(_stateFile, Log, typeof(SystemState)); + _cache = SystemState.DeserializeCacheByTranslator(_stateFile, Log); // Construct the cache if necessary. if (_cache == null) @@ -1902,7 +1902,7 @@ private void WriteStateFile() { if (!string.IsNullOrEmpty(_stateFile) && _cache.IsDirty) { - _cache.SerializeCache(_stateFile, Log); + _cache.SerializeCacheByTranslator(_stateFile, Log); } } #endregion diff --git a/src/Tasks/Microsoft.Build.Tasks.csproj b/src/Tasks/Microsoft.Build.Tasks.csproj index e4cc9c84179..6e05beb2f75 100644 --- a/src/Tasks/Microsoft.Build.Tasks.csproj +++ b/src/Tasks/Microsoft.Build.Tasks.csproj @@ -27,6 +27,14 @@ System.Design.resources + + + + + + + + @@ -517,6 +525,7 @@ true + true diff --git a/src/Tasks/StateFileBase.cs b/src/Tasks/StateFileBase.cs index 3d17a4967a4..cbeac2a38cf 100644 --- a/src/Tasks/StateFileBase.cs +++ b/src/Tasks/StateFileBase.cs @@ -68,8 +68,7 @@ internal static StateFileBase DeserializeCache(string stateFile, TaskLoggingHelp { StateFileBase retVal = null; - // First, we read the cache from disk if one exists, or if one does not exist - // then we create one. + // First, we read the cache from disk if one exists, or if one does not exist, we create one. try { if (!string.IsNullOrEmpty(stateFile) && FileSystems.Default.FileExists(stateFile)) diff --git a/src/Tasks/SystemState.cs b/src/Tasks/SystemState.cs index 1dd51c92c86..b4f422959a2 100644 --- a/src/Tasks/SystemState.cs +++ b/src/Tasks/SystemState.cs @@ -2,17 +2,18 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Runtime.Serialization; +using System.Linq; using System.Runtime.Versioning; -using System.Security.Permissions; +using Microsoft.Build.BackEnd; using Microsoft.Build.Shared; +using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Tasks.AssemblyDependency; +using Microsoft.Build.Utilities; namespace Microsoft.Build.Tasks { @@ -20,8 +21,11 @@ namespace Microsoft.Build.Tasks /// Class is used to cache system state. /// [Serializable] - internal sealed class SystemState : StateFileBase, ISerializable + internal sealed class SystemState : StateFileBase, ITranslatable { + private static readonly byte[] TranslateContractSignature = { (byte) 'M', (byte) 'B', (byte) 'R', (byte) 'S', (byte) 'C'}; // Microsoft Build RAR State Cache + private static readonly byte TranslateContractVersion = 0x01; + /// /// Cache at the SystemState instance level. Has the same contents as . /// It acts as a flag to enforce that an entry has been checked for staleness only once. @@ -31,7 +35,7 @@ internal sealed class SystemState : StateFileBase, ISerializable /// /// Cache at the SystemState instance level. It is serialized and reused between instances. /// - private Hashtable instanceLocalFileStateCache = new Hashtable(StringComparer.OrdinalIgnoreCase); + private Dictionary instanceLocalFileStateCache = new Dictionary(StringComparer.OrdinalIgnoreCase); /// /// LastModified information is purely instance-local. It doesn't make sense to @@ -108,7 +112,7 @@ internal sealed class SystemState : StateFileBase, ISerializable /// Class that holds the current file state. /// [Serializable] - private sealed class FileState : ISerializable + private sealed class FileState : ITranslatable { /// /// The last modified time for this file. @@ -149,47 +153,28 @@ internal FileState(DateTime lastModified) } /// - /// Deserializing constuctor. + /// Ctor for translator deserialization /// - internal FileState(SerializationInfo info, StreamingContext context) + internal FileState(ITranslator translator) { - ErrorUtilities.VerifyThrowArgumentNull(info, nameof(info)); - - lastModified = new DateTime(info.GetInt64("mod"), (DateTimeKind)info.GetInt32("modk")); - assemblyName = (AssemblyNameExtension)info.GetValue("an", typeof(AssemblyNameExtension)); - dependencies = (AssemblyNameExtension[])info.GetValue("deps", typeof(AssemblyNameExtension[])); - scatterFiles = (string[])info.GetValue("sfiles", typeof(string[])); - runtimeVersion = (string)info.GetValue("rtver", typeof(string)); - if (info.GetBoolean("fn")) - { - var frameworkNameVersion = (Version) info.GetValue("fnVer", typeof(Version)); - var frameworkIdentifier = info.GetString("fnId"); - var frameworkProfile = info.GetString("fmProf"); - frameworkName = new FrameworkName(frameworkIdentifier, frameworkNameVersion, frameworkProfile); - } + Translate(translator); } /// - /// Serialize the contents of the class. + /// Reads/writes this class /// - [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] - public void GetObjectData(SerializationInfo info, StreamingContext context) + public void Translate(ITranslator translator) { - ErrorUtilities.VerifyThrowArgumentNull(info, nameof(info)); - - info.AddValue("mod", lastModified.Ticks); - info.AddValue("modk", (int)lastModified.Kind); - info.AddValue("an", assemblyName); - info.AddValue("deps", dependencies); - info.AddValue("sfiles", scatterFiles); - info.AddValue("rtver", runtimeVersion); - info.AddValue("fn", frameworkName != null); - if (frameworkName != null) - { - info.AddValue("fnVer", frameworkName.Version); - info.AddValue("fnId", frameworkName.Identifier); - info.AddValue("fmProf", frameworkName.Profile); - } + ErrorUtilities.VerifyThrowArgumentNull(translator, nameof(translator)); + + translator.Translate(ref lastModified); + translator.Translate(ref assemblyName, + (ITranslator t) => new AssemblyNameExtension(t)); + translator.TranslateArray(ref dependencies, + (ITranslator t) => new AssemblyNameExtension(t)); + translator.Translate(ref scatterFiles); + translator.Translate(ref runtimeVersion); + translator.Translate(ref frameworkName); } /// @@ -239,17 +224,6 @@ internal SystemState() { } - /// - /// Deserialize the contents of the class. - /// - internal SystemState(SerializationInfo info, StreamingContext context) - { - ErrorUtilities.VerifyThrowArgumentNull(info, nameof(info)); - - instanceLocalFileStateCache = (Hashtable)info.GetValue("fileState", typeof(Hashtable)); - isDirty = false; - } - /// /// Set the target framework paths. /// This is used to optimize IO in the case of files requested from one @@ -265,14 +239,95 @@ AssemblyTableInfo[] installedAssemblyTableInfos } /// - /// Serialize the contents of the class. + /// Writes the contents of this object out to the specified file. + /// TODO: once all derived classes from StateFileBase adopt new serialization, we shall consider to mode this into base class + /// + internal void SerializeCacheByTranslator(string stateFile, TaskLoggingHelper log) + { + try + { + if (!string.IsNullOrEmpty(stateFile)) + { + if (FileSystems.Default.FileExists(stateFile)) + { + File.Delete(stateFile); + } + + using var s = new FileStream(stateFile, FileMode.CreateNew); + var translator = BinaryTranslator.GetWriteTranslator(s); + + // write file signature + translator.Writer.Write(TranslateContractSignature); + translator.Writer.Write(TranslateContractVersion); + + Translate(translator); + isDirty = false; + } + } + catch (Exception e) when (!ExceptionHandling.NotExpectedSerializationException(e)) + { + // Not being able to serialize the cache is not an error, but we let the user know anyway. + // Don't want to hold up processing just because we couldn't read the file. + log.LogWarningWithCodeFromResources("General.CouldNotWriteStateFile", stateFile, e.Message); + } + } + + /// + /// Read the contents of this object out to the specified file. + /// TODO: once all classes derived from StateFileBase adopt the new serialization, we should consider moving this into the base class + /// + internal static SystemState DeserializeCacheByTranslator(string stateFile, TaskLoggingHelper log) + { + // First, we read the cache from disk if one exists, or if one does not exist, we create one. + try + { + if (!string.IsNullOrEmpty(stateFile) && FileSystems.Default.FileExists(stateFile)) + { + using FileStream s = new FileStream(stateFile, FileMode.Open); + var translator = BinaryTranslator.GetReadTranslator(s, buffer:null); // TODO: shared buffering? + + // verify file signature + var contractSignature = translator.Reader.ReadBytes(TranslateContractSignature.Length); + var contractVersion = translator.Reader.ReadByte(); + + if (!contractSignature.SequenceEqual(TranslateContractSignature) || contractVersion != TranslateContractVersion) + { + log.LogMessageFromResources("General.CouldNotReadStateFileMessage", stateFile, log.FormatResourceString("General.IncompatibleStateFileType")); + return null; + } + + SystemState systemState = new SystemState(); + systemState.Translate(translator); + systemState.isDirty = false; + + return systemState; + } + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + // The deserialization process seems like it can throw just about + // any exception imaginable. Catch them all here. + // Not being able to deserialize the cache is not an error, but we let the user know anyway. + // Don't want to hold up processing just because we couldn't read the file. + log.LogWarningWithCodeFromResources("General.CouldNotReadStateFile", stateFile, e.Message); + } + + return null; + } + + /// + /// Reads/writes this class. + /// Used for serialization and deserialization of this class persistent cache. /// - [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] - public void GetObjectData(SerializationInfo info, StreamingContext context) + public void Translate(ITranslator translator) { - ErrorUtilities.VerifyThrowArgumentNull(info, nameof(info)); + if (instanceLocalFileStateCache is null) + throw new NullReferenceException(nameof(instanceLocalFileStateCache)); - info.AddValue("fileState", instanceLocalFileStateCache); + translator.TranslateDictionary( + ref instanceLocalFileStateCache, + StringComparer.OrdinalIgnoreCase, + (ITranslator t) => new FileState(t)); } /// @@ -378,10 +433,8 @@ private FileState GetFileState(string path) private FileState ComputeFileStateFromCachesAndDisk(string path) { DateTime lastModified = GetAndCacheLastModified(path); - FileState cachedInstanceFileState = (FileState)instanceLocalFileStateCache[path]; - bool isCachedInInstance = cachedInstanceFileState != null; - bool isCachedInProcess = - s_processWideFileStateCache.TryGetValue(path, out FileState cachedProcessFileState); + bool isCachedInInstance = instanceLocalFileStateCache.TryGetValue(path, out FileState cachedInstanceFileState); + bool isCachedInProcess = s_processWideFileStateCache.TryGetValue(path, out FileState cachedProcessFileState); bool isInstanceFileStateUpToDate = isCachedInInstance && lastModified == cachedInstanceFileState.LastModified; bool isProcessFileStateUpToDate = isCachedInProcess && lastModified == cachedProcessFileState.LastModified; diff --git a/src/Tasks/TaskTranslatorHelpers.cs b/src/Tasks/TaskTranslatorHelpers.cs new file mode 100644 index 00000000000..7db48cf61ae --- /dev/null +++ b/src/Tasks/TaskTranslatorHelpers.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.Versioning; +using Microsoft.Build.BackEnd; + +namespace Microsoft.Build.Tasks +{ + internal static class TaskTranslatorHelpers + { + public static void Translate(this ITranslator translator, ref FrameworkName frameworkName) + { + if (!translator.TranslateNullable(frameworkName)) + return; + + string identifier = null; + Version version = null; + string profile = null; + + if (translator.Mode == TranslationDirection.WriteToStream) + { + identifier = frameworkName.Identifier; + version = frameworkName.Version; + profile = frameworkName.Profile; + } + + translator.Translate(ref identifier); + translator.Translate(ref version); + translator.Translate(ref profile); + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + frameworkName = new FrameworkName(identifier, version, profile); + } + } + } +}