diff --git a/src/NuGet.Server.Core/Infrastructure/JsonNetPackagesSerializer.cs b/src/NuGet.Server.Core/Infrastructure/JsonNetPackagesSerializer.cs index cf82826c..487b3ac8 100644 --- a/src/NuGet.Server.Core/Infrastructure/JsonNetPackagesSerializer.cs +++ b/src/NuGet.Server.Core/Infrastructure/JsonNetPackagesSerializer.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -18,7 +19,8 @@ public class JsonNetPackagesSerializer private readonly JsonSerializer _serializer = new JsonSerializer { Formatting = Formatting.None, - NullValueHandling = NullValueHandling.Ignore + NullValueHandling = NullValueHandling.Ignore, + Converters = { new AbsoluteUriConverter() }, }; public void Serialize(IEnumerable packages, Stream stream) @@ -51,5 +53,54 @@ public IEnumerable Deserialize(Stream stream) return packages.Packages; } } + + /// + /// This is necessary because Newtonsoft.Json creates instances with + /// which treats UNC paths as relative. NuGet.Core uses + /// which treats UNC paths as absolute. For more details, see: + /// https://github.com/JamesNK/Newtonsoft.Json/issues/2128 + /// + private class AbsoluteUriConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Uri); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + else if (reader.TokenType == JsonToken.String) + { + return new Uri((string)reader.Value, UriKind.Absolute); + } + + throw new JsonSerializationException("The JSON value must be a string."); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + if (!(value is Uri uriValue)) + { + throw new JsonSerializationException("The value must be a URI."); + } + + if (!uriValue.IsAbsoluteUri) + { + throw new JsonSerializationException("The URI value must be an absolute Uri. Relative URI instances are not allowed."); + } + + writer.WriteValue(uriValue.OriginalString); + } + } } } \ No newline at end of file diff --git a/test/NuGet.Server.Core.Tests/JsonNetPackagesSerializerTests.cs b/test/NuGet.Server.Core.Tests/JsonNetPackagesSerializerTests.cs index 0f0a9f1a..42c0cea8 100644 --- a/test/NuGet.Server.Core.Tests/JsonNetPackagesSerializerTests.cs +++ b/test/NuGet.Server.Core.Tests/JsonNetPackagesSerializerTests.cs @@ -14,6 +14,37 @@ namespace NuGet.Server.Core.Tests { public class JsonNetPackagesSerializerTests { + [Fact] + public void RoundTripsUncPaths() + { + var originalPackages = GenerateServerPackages(1); + var originalPackage = originalPackages.Single(); + originalPackage.IconUrl = new Uri("//testunc/test/a", UriKind.Absolute); + originalPackage.LicenseUrl = new Uri("//testunc/test/b", UriKind.Absolute); + originalPackage.ProjectUrl = new Uri("//testunc/test/c", UriKind.Absolute); + originalPackage.ReportAbuseUrl = new Uri("//testunc/test/d", UriKind.Absolute); + var serializer = new JsonNetPackagesSerializer(); + + // Act + var deserializedPackages = new List(); + using (var memoryStream = new MemoryStream()) + { + serializer.Serialize(originalPackages, memoryStream); + + memoryStream.Position = 0; + + deserializedPackages.AddRange(serializer.Deserialize(memoryStream)); + } + + // Assert + AssertPackagesAreEqual(originalPackages, deserializedPackages); + var deserializedPackage = deserializedPackages.Single(); + Assert.True(deserializedPackage.IconUrl.IsAbsoluteUri, "The icon URL should still be absolute."); + Assert.True(deserializedPackage.LicenseUrl.IsAbsoluteUri, "The license URL should still be absolute."); + Assert.True(deserializedPackage.ProjectUrl.IsAbsoluteUri, "The project URL should still be absolute."); + Assert.True(deserializedPackage.ReportAbuseUrl.IsAbsoluteUri, "The report abuse URL should still be absolute."); + } + [Fact] public void TestSerializationRoundTrip() { @@ -33,14 +64,25 @@ public void TestSerializationRoundTrip() } // Assert + AssertPackagesAreEqual(originalPackages, deserializedPackages); + } + + private static void AssertPackagesAreEqual(List originalPackages, List deserializedPackages) + { Assert.Equal(originalPackages.Count, deserializedPackages.Count); for (var i = 0; i < originalPackages.Count; i++) { - Assert.True(PublicPropertiesEqual(originalPackages[i], deserializedPackages[i], "DependencySets", "FrameworkAssemblies", "PackageAssemblyReferences", "AssemblyReferences")); + AssertPublicPropertiesEqual( + originalPackages[i], + deserializedPackages[i], + "DependencySets", + "FrameworkAssemblies", + "PackageAssemblyReferences", + "AssemblyReferences"); } } - private static bool PublicPropertiesEqual(T a, T b, params string[] ignoreProperties) where T : class + private static void AssertPublicPropertiesEqual(T a, T b, params string[] ignoreProperties) where T : class { if (a != null && b != null) { @@ -55,15 +97,15 @@ private static bool PublicPropertiesEqual(T a, T b, params string[] ignorePro if (selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue)) && !(selfValue is IEnumerable)) { - return false; + Assert.False(true, $"The property '{pi.Name}' is not equal."); } } } - return true; + return; } - return a == b; + Assert.Equal(a, b); } private static List GenerateServerPackages(int count)