Skip to content

Commit 6819f7a

Browse files
authored
On disk cache serialization (#6094)
### Context BinaryFormater serialization of on-disk RAR cache is slow and unsecure. Related issue: #6057 ### Changes Made Serialization changed to use custom binary format by using existing `ITranslatable` ### Testing Unit testing Manual testing - Roslyn repo rebuild + incremental build
1 parent 891b99c commit 6819f7a

13 files changed

+887
-72
lines changed

src/Build.UnitTests/BackEnd/BinaryTranslator_Tests.cs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Configuration.Assemblies;
7+
using System.Globalization;
68
using Microsoft.Build.BackEnd;
79
using System.IO;
10+
using System.Reflection;
11+
using Shouldly;
812
using Xunit;
913

1014
namespace Microsoft.Build.UnitTests.BackEnd
@@ -431,6 +435,163 @@ public void TestSerializeDictionaryStringTNoComparerNull()
431435
Assert.Equal(value, deserializedValue);
432436
}
433437

438+
[Theory]
439+
[InlineData("en")]
440+
[InlineData("en-US")]
441+
[InlineData("en-CA")]
442+
[InlineData("zh-HK")]
443+
[InlineData("sr-Cyrl-CS")]
444+
public void CultureInfo(string name)
445+
{
446+
CultureInfo value = new CultureInfo(name);
447+
TranslationHelpers.GetWriteTranslator().Translate(ref value);
448+
449+
CultureInfo deserializedValue = null;
450+
TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue);
451+
452+
deserializedValue.ShouldBe(value);
453+
}
454+
455+
[Fact]
456+
public void CultureInfoAsNull()
457+
{
458+
CultureInfo value = null;
459+
TranslationHelpers.GetWriteTranslator().Translate(ref value);
460+
461+
CultureInfo deserializedValue = null;
462+
TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue);
463+
464+
deserializedValue.ShouldBeNull();
465+
}
466+
467+
[Theory]
468+
[InlineData("1.2")]
469+
[InlineData("1.2.3")]
470+
[InlineData("1.2.3.4")]
471+
public void Version(string version)
472+
{
473+
Version value = new Version(version);
474+
TranslationHelpers.GetWriteTranslator().Translate(ref value);
475+
476+
Version deserializedValue = null;
477+
TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue);
478+
479+
deserializedValue.ShouldBe(value);
480+
}
481+
482+
[Fact]
483+
public void VersionAsNull()
484+
{
485+
Version value = null;
486+
TranslationHelpers.GetWriteTranslator().Translate(ref value);
487+
488+
Version deserializedValue = null;
489+
TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue);
490+
491+
deserializedValue.ShouldBeNull();
492+
}
493+
494+
[Fact]
495+
public void HashSetOfT()
496+
{
497+
HashSet<BaseClass> values = new()
498+
{
499+
new BaseClass(1),
500+
new BaseClass(2),
501+
null
502+
};
503+
TranslationHelpers.GetWriteTranslator().TranslateHashSet(ref values, BaseClass.FactoryForDeserialization, capacity => new());
504+
505+
HashSet<BaseClass> deserializedValues = null;
506+
TranslationHelpers.GetReadTranslator().TranslateHashSet(ref deserializedValues, BaseClass.FactoryForDeserialization, capacity => new());
507+
508+
deserializedValues.ShouldBe(values, ignoreOrder: true);
509+
}
510+
511+
[Fact]
512+
public void HashSetOfTAsNull()
513+
{
514+
HashSet<BaseClass> value = null;
515+
TranslationHelpers.GetWriteTranslator().TranslateHashSet(ref value, BaseClass.FactoryForDeserialization, capacity => new());
516+
517+
HashSet<BaseClass> deserializedValue = null;
518+
TranslationHelpers.GetReadTranslator().TranslateHashSet(ref deserializedValue, BaseClass.FactoryForDeserialization, capacity => new());
519+
520+
deserializedValue.ShouldBeNull();
521+
}
522+
523+
[Fact]
524+
public void AssemblyNameAsNull()
525+
{
526+
AssemblyName value = null;
527+
TranslationHelpers.GetWriteTranslator().Translate(ref value);
528+
529+
AssemblyName deserializedValue = null;
530+
TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue);
531+
532+
deserializedValue.ShouldBeNull();
533+
}
534+
535+
[Fact]
536+
public void AssemblyNameWithAllFields()
537+
{
538+
AssemblyName value = new()
539+
{
540+
Name = "a",
541+
Version = new Version(1, 2, 3),
542+
Flags = AssemblyNameFlags.PublicKey,
543+
ProcessorArchitecture = ProcessorArchitecture.X86,
544+
CultureInfo = new CultureInfo("zh-HK"),
545+
HashAlgorithm = System.Configuration.Assemblies.AssemblyHashAlgorithm.SHA256,
546+
VersionCompatibility = AssemblyVersionCompatibility.SameMachine,
547+
CodeBase = "C:\\src",
548+
KeyPair = new StrongNameKeyPair(new byte[] { 4, 3, 2, 1 }),
549+
ContentType = AssemblyContentType.WindowsRuntime,
550+
CultureName = "zh-HK",
551+
};
552+
value.SetPublicKey(new byte[]{ 3, 2, 1});
553+
value.SetPublicKeyToken(new byte[] { 8, 7, 6, 5, 4, 3, 2, 1 });
554+
555+
TranslationHelpers.GetWriteTranslator().Translate(ref value);
556+
557+
AssemblyName deserializedValue = null;
558+
TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue);
559+
560+
HelperAssertAssemblyNameEqual(value, deserializedValue);
561+
}
562+
563+
[Fact]
564+
public void AssemblyNameWithMinimalFields()
565+
{
566+
AssemblyName value = new();
567+
568+
TranslationHelpers.GetWriteTranslator().Translate(ref value);
569+
570+
AssemblyName deserializedValue = null;
571+
TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue);
572+
573+
HelperAssertAssemblyNameEqual(value, deserializedValue);
574+
}
575+
576+
/// <summary>
577+
/// Assert two AssemblyName objects values are same.
578+
/// Ignoring KeyPair, ContentType, CultureName as those are not serialized
579+
/// </summary>
580+
private static void HelperAssertAssemblyNameEqual(AssemblyName expected, AssemblyName actual)
581+
{
582+
actual.Name.ShouldBe(expected.Name);
583+
actual.Version.ShouldBe(expected.Version);
584+
actual.Flags.ShouldBe(expected.Flags);
585+
actual.ProcessorArchitecture.ShouldBe(expected.ProcessorArchitecture);
586+
actual.CultureInfo.ShouldBe(expected.CultureInfo);
587+
actual.HashAlgorithm.ShouldBe(expected.HashAlgorithm);
588+
actual.VersionCompatibility.ShouldBe(expected.VersionCompatibility);
589+
actual.CodeBase.ShouldBe(expected.CodeBase);
590+
591+
actual.GetPublicKey().ShouldBe(expected.GetPublicKey());
592+
actual.GetPublicKeyToken().ShouldBe(expected.GetPublicKeyToken());
593+
}
594+
434595
/// <summary>
435596
/// Helper for bool serialization.
436597
/// </summary>
@@ -610,6 +771,24 @@ protected BaseClass()
610771
{
611772
}
612773

774+
protected bool Equals(BaseClass other)
775+
{
776+
return _baseValue == other._baseValue;
777+
}
778+
779+
public override bool Equals(object obj)
780+
{
781+
if (ReferenceEquals(null, obj)) return false;
782+
if (ReferenceEquals(this, obj)) return true;
783+
if (obj.GetType() != this.GetType()) return false;
784+
return Equals((BaseClass) obj);
785+
}
786+
787+
public override int GetHashCode()
788+
{
789+
return _baseValue;
790+
}
791+
613792
/// <summary>
614793
/// Gets a comparer.
615794
/// </summary>

src/Shared/AssemblyNameExtension.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Configuration.Assemblies;
1010
using System.Runtime.Serialization;
1111
using System.IO;
12+
using Microsoft.Build.BackEnd;
1213
#if FEATURE_ASSEMBLYLOADCONTEXT
1314
using System.Reflection.PortableExecutable;
1415
using System.Reflection.Metadata;
@@ -54,7 +55,7 @@ internal enum PartialComparisonFlags : int
5455
/// between the two is done lazily on demand.
5556
/// </summary>
5657
[Serializable]
57-
internal sealed class AssemblyNameExtension : ISerializable, IEquatable<AssemblyNameExtension>
58+
internal sealed class AssemblyNameExtension : ISerializable, IEquatable<AssemblyNameExtension>, ITranslatable
5859
{
5960
private AssemblyName asAssemblyName = null;
6061
private string asString = null;
@@ -173,6 +174,14 @@ private AssemblyNameExtension(SerializationInfo info, StreamingContext context)
173174
remappedFrom = (HashSet<AssemblyNameExtension>) info.GetValue("remapped", typeof(HashSet<AssemblyNameExtension>));
174175
}
175176

177+
/// <summary>
178+
/// Ctor for deserializing from state file (custom binary serialization) using translator.
179+
/// </summary>
180+
internal AssemblyNameExtension(ITranslator translator) : this()
181+
{
182+
Translate(translator);
183+
}
184+
176185
/// <summary>
177186
/// To be used as a delegate. Gets the AssemblyName of the given file.
178187
/// </summary>
@@ -251,10 +260,18 @@ private void InitializeRemappedFrom()
251260
{
252261
if (remappedFrom == null)
253262
{
254-
remappedFrom = new HashSet<AssemblyNameExtension>(AssemblyNameComparer.GenericComparerConsiderRetargetable);
263+
remappedFrom = CreateRemappedFrom();
255264
}
256265
}
257266

267+
/// <summary>
268+
/// Create remappedFrom HashSet. Used by deserialization as well.
269+
/// </summary>
270+
private static HashSet<AssemblyNameExtension> CreateRemappedFrom()
271+
{
272+
return new HashSet<AssemblyNameExtension>(AssemblyNameComparer.GenericComparerConsiderRetargetable);
273+
}
274+
258275
/// <summary>
259276
/// Assume there is a string version, create the AssemblyName version.
260277
/// </summary>
@@ -993,5 +1010,23 @@ public void GetObjectData(SerializationInfo info, StreamingContext context)
9931010
info.AddValue("immutable", immutable);
9941011
info.AddValue("remapped", remappedFrom);
9951012
}
1013+
1014+
/// <summary>
1015+
/// Reads/writes this class
1016+
/// </summary>
1017+
/// <param name="translator"></param>
1018+
public void Translate(ITranslator translator)
1019+
{
1020+
translator.Translate(ref asAssemblyName);
1021+
translator.Translate(ref asString);
1022+
translator.Translate(ref isSimpleName);
1023+
translator.Translate(ref hasProcessorArchitectureInFusionName);
1024+
translator.Translate(ref immutable);
1025+
1026+
// TODO: consider some kind of protection against infinite loop during serialization, hint: pre serialize check for cycle in graph
1027+
translator.TranslateHashSet(ref remappedFrom,
1028+
(ITranslator t) => new AssemblyNameExtension(t),
1029+
(int capacity) => CreateRemappedFrom());
1030+
}
9961031
}
9971032
}

0 commit comments

Comments
 (0)