Skip to content

Commit eec8cde

Browse files
committed
Migrate ServiceIndexResource
1 parent 106309a commit eec8cde

6 files changed

Lines changed: 283 additions & 38 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.Text.Json;
6+
using System.Text.Json.Serialization;
7+
8+
namespace NuGet.Protocol.Model
9+
{
10+
internal sealed class ServiceIndexModel
11+
{
12+
[JsonPropertyName("version")]
13+
public string? Version { get; set; }
14+
15+
[JsonPropertyName("resources")]
16+
public IReadOnlyList<ServiceIndexEntryModel>? Resources { get; set; }
17+
}
18+
19+
internal sealed class ServiceIndexEntryModel
20+
{
21+
[JsonPropertyName("@id")]
22+
public string? Id { get; set; }
23+
24+
/// <summary>JSON string or array of strings.</summary>
25+
[JsonPropertyName("@type")]
26+
public JsonElement Type { get; set; }
27+
28+
/// <summary>Optional JSON string or array of strings.</summary>
29+
[JsonPropertyName("clientVersion")]
30+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
31+
public JsonElement ClientVersion { get; set; }
32+
}
33+
}

src/NuGet.Core/NuGet.Protocol/Providers/ServiceIndexResourceV3Provider.cs

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
using System.Collections.Concurrent;
88
using System.Globalization;
99
using System.IO;
10+
using System.Text.Json;
1011
using System.Threading;
1112
using System.Threading.Tasks;
12-
using Newtonsoft.Json.Linq;
1313
using NuGet.Common;
1414
using NuGet.Configuration;
1515
using NuGet.Protocol.Core.Types;
16+
using NuGet.Protocol.Model;
17+
using NuGet.Protocol.Utility;
1618
using NuGet.Versioning;
1719

1820
namespace NuGet.Protocol
@@ -190,34 +192,34 @@ ex.InnerException is IOException &&
190192

191193
private static async Task<ServiceIndexResourceV3> ConsumeServiceIndexStreamAsync(Stream stream, DateTime utcNow, PackageSource source, CancellationToken token)
192194
{
193-
// Parse the JSON
194-
JObject json = await stream.AsJObjectAsync(token);
195-
196-
// Use SemVer instead of NuGetVersion, the service index should always be
197-
// in strict SemVer format
198-
JToken versionToken;
199-
if (json.TryGetValue("version", out versionToken) &&
200-
versionToken.Type == JTokenType.String)
195+
ServiceIndexModel index;
196+
try
201197
{
202-
SemanticVersion version;
203-
if (SemanticVersion.TryParse((string)versionToken, out version) &&
204-
version.Major == 3)
205-
{
206-
return new ServiceIndexResourceV3(json, utcNow, source);
207-
}
208-
else
209-
{
210-
string errorMessage = string.Format(
211-
CultureInfo.CurrentCulture,
212-
Strings.Protocol_UnsupportedVersion,
213-
(string)versionToken);
214-
throw new InvalidDataException(errorMessage);
215-
}
198+
index = await JsonSerializer.DeserializeAsync(stream, JsonContext.Default.ServiceIndexModel, token);
216199
}
217-
else
200+
catch (JsonException ex)
201+
{
202+
throw new InvalidDataException(string.Format(
203+
CultureInfo.CurrentCulture,
204+
Strings.Protocol_InvalidJsonObject,
205+
source.Source), ex);
206+
}
207+
208+
if (index?.Version is not string versionString)
218209
{
219210
throw new InvalidDataException(Strings.Protocol_MissingVersion);
220211
}
212+
213+
// Use SemVer instead of NuGetVersion; the service index should always be in strict SemVer format.
214+
if (!SemanticVersion.TryParse(versionString, out SemanticVersion version) || version.Major != 3)
215+
{
216+
throw new InvalidDataException(string.Format(
217+
CultureInfo.CurrentCulture,
218+
Strings.Protocol_UnsupportedVersion,
219+
versionString));
220+
}
221+
222+
return new ServiceIndexResourceV3(index, utcNow, source);
221223
}
222224

223225
protected class ServiceIndexCacheInfo

src/NuGet.Core/NuGet.Protocol/Resources/ServiceIndexResourceV3.cs

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
using System;
77
using System.Collections.Generic;
88
using System.Linq;
9+
using System.Text.Json;
910
using Newtonsoft.Json.Linq;
1011
using NuGet.Configuration;
1112
using NuGet.Packaging;
1213
using NuGet.Protocol.Core.Types;
1314
using NuGet.Protocol.Events;
15+
using NuGet.Protocol.Model;
16+
using NuGet.Protocol.Utility;
1417
using NuGet.Versioning;
1518

1619
namespace NuGet.Protocol
@@ -21,6 +24,7 @@ namespace NuGet.Protocol
2124
public class ServiceIndexResourceV3 : INuGetResource
2225
{
2326
private readonly string _json;
27+
private readonly ServiceIndexModel _model;
2428
private readonly IDictionary<string, List<ServiceIndexEntry>> _index;
2529
private readonly DateTime _requestTime;
2630
private static readonly IReadOnlyList<ServiceIndexEntry> _emptyEntries = new List<ServiceIndexEntry>();
@@ -35,6 +39,13 @@ internal ServiceIndexResourceV3(JObject index, DateTime requestTime, PackageSour
3539

3640
public ServiceIndexResourceV3(JObject index, DateTime requestTime) : this(index, requestTime, null) { }
3741

42+
internal ServiceIndexResourceV3(ServiceIndexModel model, DateTime requestTime, PackageSource packageSource)
43+
{
44+
_model = model;
45+
_index = MakeLookup(model, packageSource);
46+
_requestTime = requestTime;
47+
}
48+
3849
/// <summary>
3950
/// Time the index was requested
4051
/// </summary>
@@ -56,10 +67,7 @@ public virtual IReadOnlyList<ServiceIndexEntry> Entries
5667

5768
public virtual string Json
5869
{
59-
get
60-
{
61-
return _json;
62-
}
70+
get { return _json ?? JsonSerializer.Serialize(_model, JsonContext.Default.ServiceIndexModel); }
6371
}
6472

6573
/// <summary>
@@ -148,6 +156,87 @@ public virtual IReadOnlyList<Uri> GetServiceEntryUris(NuGetVersion clientVersion
148156
return GetServiceEntries(clientVersion, orderedTypes).Select(e => e.Uri).ToList();
149157
}
150158

159+
private static IDictionary<string, List<ServiceIndexEntry>> MakeLookup(ServiceIndexModel index, PackageSource packageSource)
160+
{
161+
var result = new Dictionary<string, List<ServiceIndexEntry>>(StringComparer.Ordinal);
162+
163+
if (index?.Resources is null)
164+
{
165+
return result;
166+
}
167+
168+
foreach (var resource in index.Resources)
169+
{
170+
var id = resource.Id;
171+
if (string.IsNullOrEmpty(id) || !Uri.TryCreate(id, UriKind.Absolute, out Uri uri))
172+
{
173+
continue;
174+
}
175+
176+
if (packageSource != null && uri.Scheme == Uri.UriSchemeHttp && packageSource.IsHttps)
177+
{
178+
ProtocolDiagnostics.RaiseEvent(new ProtocolDiagnosticServiceIndexEntryEvent(source: packageSource.Source, httpsSourceHasHttpResource: true));
179+
}
180+
181+
var types = GetStringValues(resource.Type).ToArray();
182+
183+
var clientVersions = new List<SemanticVersion>();
184+
if (resource.ClientVersion.ValueKind == JsonValueKind.Undefined)
185+
{
186+
clientVersions.Add(_defaultVersion);
187+
}
188+
else
189+
{
190+
foreach (var versionString in GetStringValues(resource.ClientVersion))
191+
{
192+
if (SemanticVersion.TryParse(versionString, out SemanticVersion semVer))
193+
{
194+
clientVersions.Add(semVer);
195+
}
196+
}
197+
}
198+
199+
foreach (var type in types)
200+
{
201+
foreach (var clientVersion in clientVersions)
202+
{
203+
if (!result.TryGetValue(type, out List<ServiceIndexEntry> entries))
204+
{
205+
entries = new List<ServiceIndexEntry>();
206+
result.Add(type, entries);
207+
}
208+
209+
entries.Add(new ServiceIndexEntry(uri, type, clientVersion));
210+
}
211+
}
212+
}
213+
214+
foreach (var type in result.Keys.ToArray())
215+
{
216+
result[type] = result[type].OrderByDescending(e => e.ClientVersion).ToList();
217+
}
218+
219+
return result;
220+
}
221+
222+
private static IEnumerable<string> GetStringValues(JsonElement element)
223+
{
224+
if (element.ValueKind == JsonValueKind.Array)
225+
{
226+
foreach (var item in element.EnumerateArray())
227+
{
228+
if (item.ValueKind == JsonValueKind.String)
229+
{
230+
yield return item.GetString();
231+
}
232+
}
233+
}
234+
else if (element.ValueKind == JsonValueKind.String)
235+
{
236+
yield return element.GetString();
237+
}
238+
}
239+
151240
private static IDictionary<string, List<ServiceIndexEntry>> MakeLookup(JObject index, PackageSource packageSource)
152241
{
153242
var result = new Dictionary<string, List<ServiceIndexEntry>>(StringComparer.Ordinal);

src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ namespace NuGet.Protocol.Utility
1111
#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant
1212
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true,
1313
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
14-
GenerationMode = JsonSourceGenerationMode.Metadata,
1514
Converters = [typeof(VersionRangeStjConverter)])]
1615
#pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant
1716
[JsonSerializable(typeof(HttpFileSystemBasedFindPackageByIdResource.FlatContainerVersionList))]
1817
[JsonSerializable(typeof(IReadOnlyList<V3VulnerabilityIndexEntry>), TypeInfoPropertyName = "VulnerabilityIndex")]
1918
[JsonSerializable(typeof(CaseInsensitiveDictionary<IReadOnlyList<PackageVulnerabilityInfo>>), TypeInfoPropertyName = "VulnerabilityPage")]
19+
[JsonSerializable(typeof(ServiceIndexModel))]
2020
internal partial class JsonContext : JsonSerializerContext
2121
{
2222
}

test/NuGet.Core.Tests/NuGet.Protocol.Tests/ServiceIndexResourceV3ProviderTests.cs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
#nullable disable
@@ -105,8 +105,8 @@ public async Task TryCreate_Throws_IfSourceLocationDoesNotReturnValidJson(string
105105
}
106106

107107
[Theory]
108-
[InlineData("{ version: \"not-semver\" } ")]
109-
[InlineData("{ version: \"3.0.0.0\" } ")] // not strict semver
108+
[InlineData("{ \"version\": \"not-semver\" }")]
109+
[InlineData("{ \"version\": \"3.0.0.0\" }")] // not strict semver
110110
public async Task TryCreate_Throws_IfInvalidVersionInJson(string content)
111111
{
112112
// Arrange
@@ -127,9 +127,7 @@ public async Task TryCreate_Throws_IfInvalidVersionInJson(string content)
127127
}
128128

129129
[Theory]
130-
[InlineData("{ json: \"that does not contain version.\" }")]
131-
[InlineData("{ version: 3 } ")] // version is not a string
132-
[InlineData("{ version: { value: 3 } } ")] // version is not a string
130+
[InlineData("{ \"json\": \"that does not contain version.\" }")]
133131
public async Task TryCreate_Throws_IfNoVersionInJson(string content)
134132
{
135133
// Arrange
@@ -149,12 +147,36 @@ public async Task TryCreate_Throws_IfNoVersionInJson(string content)
149147
Assert.Equal("The source does not have the 'version' property.", exception.InnerException.Message);
150148
}
151149

150+
[Theory]
151+
[InlineData("{ \"version\": 3 }")] // version is not a string
152+
[InlineData("{ \"version\": { \"value\": 3 } }")] // version is not a string
153+
public async Task TryCreate_Throws_IfVersionFieldIsWrongType(string content)
154+
{
155+
// Arrange
156+
// STJ throws JsonException for type mismatches during deserialization, which
157+
// maps to Protocol_InvalidJsonObject rather than Protocol_MissingVersion.
158+
var source = "https://contoso.test/index.json";
159+
var httpProvider = StaticHttpSource.CreateHttpSource(new Dictionary<string, string> { { source, content } });
160+
var provider = new ServiceIndexResourceV3Provider();
161+
var sourceRepository = new SourceRepository(new PackageSource(source),
162+
new INuGetResourceProvider[] { httpProvider, provider });
163+
164+
// Act
165+
var exception = await Assert.ThrowsAsync<FatalProtocolException>(async () =>
166+
{
167+
var result = await provider.TryCreate(sourceRepository, default(CancellationToken));
168+
});
169+
170+
// Assert
171+
Assert.IsType<InvalidDataException>(exception.InnerException);
172+
}
173+
152174
[Fact]
153175
public async Task TryCreate_ReturnsTrue_IfSourceLocationReturnsValidJson()
154176
{
155177
// Arrange
156178
var source = $"https://some-site-{new Guid().ToString()}.org/test.json";
157-
var content = @"{ version: '3.1.0-beta' }";
179+
var content = @"{ ""version"": ""3.1.0-beta"" }";
158180
var httpProvider = StaticHttpSource.CreateHttpSource(new Dictionary<string, string> { { source, content } });
159181
var provider = new ServiceIndexResourceV3Provider();
160182
var sourceRepository = new SourceRepository(new PackageSource(source),

0 commit comments

Comments
 (0)