Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions src/NuGet.Core/NuGet.Protocol/Plugins/PluginCacheEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NuGet.Common;
using NuGet.Shared;

namespace NuGet.Protocol.Plugins
{
Expand All @@ -18,17 +20,25 @@ namespace NuGet.Protocol.Plugins
/// </summary>
public sealed class PluginCacheEntry
{
private readonly IEnvironmentVariableReader _environmentVariableReader;

/// <summary>
/// Create a plugin cache entry.
/// </summary>
/// <param name="rootCacheFolder">The root cache folder, normally /localappdata/nuget/plugins-cache</param>
/// <param name="pluginFilePath">The full plugin file path, which will be used to create a key for the folder created in the root folder itself </param>
/// <param name="requestKey">A unique request key for the operation claims. Ideally the packageSourceRepository value of the PluginRequestKey. Example https://protected.package.feed/index.json, or Source-Agnostic</param>
public PluginCacheEntry(string rootCacheFolder, string pluginFilePath, string requestKey)
: this(rootCacheFolder, pluginFilePath, requestKey, environmentVariableReader: null)
{
}

Comment thread
Nigusu-Allehu marked this conversation as resolved.
internal PluginCacheEntry(string rootCacheFolder, string pluginFilePath, string requestKey, IEnvironmentVariableReader environmentVariableReader)
{
RootFolder = Path.Combine(rootCacheFolder, CachingUtility.RemoveInvalidFileNameChars(CachingUtility.ComputeHash(pluginFilePath, addIdentifiableCharacters: false)));
CacheFileName = Path.Combine(RootFolder, CachingUtility.RemoveInvalidFileNameChars(CachingUtility.ComputeHash(requestKey, addIdentifiableCharacters: false)) + ".dat");
NewCacheFileName = CacheFileName + "-new";
_environmentVariableReader = environmentVariableReader;
}

internal TimeSpan MaxAge { get; set; } = TimeSpan.FromDays(30);
Expand Down Expand Up @@ -61,11 +71,19 @@ public void LoadFromFile()

private void ProcessContent(Stream content)
{
var serializer = new JsonSerializer();
using (var sr = new StreamReader(content))
using (var jsonTextReader = new JsonTextReader(sr))
if (NuGetFeatureFlags.UseSystemTextJsonDeserializationFeatureSwitch
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this is just internal, NuGet generated, nuget read, I think we may be able to remove the non STJ path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't LoadFromFile() expose this method because it is a public API and uses this method as a utility method.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am assuming your point is rather about cache file. But we still have a public API

|| NuGetFeatureFlags.IsSystemTextJsonDeserializationEnabledByEnvironment(_environmentVariableReader))
{
OperationClaims = serializer.Deserialize<IReadOnlyList<OperationClaim>>(jsonTextReader);
OperationClaims = System.Text.Json.JsonSerializer.Deserialize(content, PluginCacheJsonContext.Default.IReadOnlyListOperationClaim);
}
else
{
var serializer = new Newtonsoft.Json.JsonSerializer();
using (var sr = new StreamReader(content))
using (var jsonTextReader = new JsonTextReader(sr))
{
OperationClaims = serializer.Deserialize<IReadOnlyList<OperationClaim>>(jsonTextReader);
}
}
}

Expand All @@ -90,7 +108,17 @@ public async Task UpdateCacheFileAsync()
FileShare.None,
CachingUtility.BufferSize))
{
var json = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(OperationClaims, Formatting.Indented));
byte[] json;
if (NuGetFeatureFlags.UseSystemTextJsonDeserializationFeatureSwitch
|| NuGetFeatureFlags.IsSystemTextJsonDeserializationEnabledByEnvironment(_environmentVariableReader))
{
json = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(OperationClaims, PluginCacheJsonContext.Default.IReadOnlyListOperationClaim);
}
else
{
json = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(OperationClaims, Formatting.Indented));
}

await fileStream.WriteAsync(json, 0, json.Length);
}

Expand Down
14 changes: 14 additions & 0 deletions src/NuGet.Core/NuGet.Protocol/Plugins/PluginCacheJsonContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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.Collections.Generic;
using System.Text.Json.Serialization;

namespace NuGet.Protocol.Plugins
{
[JsonSerializable(typeof(IReadOnlyList<OperationClaim>))]
[JsonSourceGenerationOptions(WriteIndented = true)]
internal partial class PluginCacheJsonContext : JsonSerializerContext
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Moq;
using NuGet.Common;
using NuGet.Protocol.Plugins;
using NuGet.Shared;
using NuGet.Test.Utility;
Expand All @@ -15,13 +17,20 @@ namespace NuGet.Protocol.Tests.Plugins
[Collection(nameof(NotThreadSafeResourceCollection))]
public class PluginCacheEntryTests
{
[Fact]
public void PluginCacheEntry_DoesNotThrowWithNoFile()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void PluginCacheEntry_DoesNotThrowWithNoFile(bool useStj)
{
using (var testDirectory = TestDirectory.Create())
{
var entry = new PluginCacheEntry(testDirectory.Path, "a", "b");
// Arrange
var entry = CreateEntry(testDirectory.Path, "a", "b", useStj);

// Act
entry.LoadFromFile();

// Assert
Assert.Null(entry.OperationClaims);
}
}
Expand All @@ -47,8 +56,9 @@ public void PluginCacheEntry_UsesShorterPaths()

[Theory]
[MemberData(nameof(GetsRoundTripsValuesData))]
public async Task PluginCacheEntry_RoundTripsValuesAsync(string[] values)
public async Task PluginCacheEntry_RoundTripsValuesAsync(string[] values, bool useStj)
{
// Arrange
var list = new List<OperationClaim>();
foreach (var val in values)
{
Expand All @@ -58,27 +68,57 @@ public async Task PluginCacheEntry_RoundTripsValuesAsync(string[] values)

using (var testDirectory = TestDirectory.Create())
{
var entry = new PluginCacheEntry(testDirectory.Path, "a", "b");
entry.LoadFromFile();
var entry = CreateEntry(testDirectory.Path, "a", "b", useStj);
entry.OperationClaims = list;
await entry.UpdateCacheFileAsync();

var newEntry = new PluginCacheEntry(testDirectory.Path, "a", "b");
// Act
await entry.UpdateCacheFileAsync();
var newEntry = CreateEntry(testDirectory.Path, "a", "b", useStj);
newEntry.LoadFromFile();

// Assert
Assert.True(EqualityUtility.SequenceEqualWithNullCheck(entry.OperationClaims, newEntry.OperationClaims));
}
}

[Fact]
public async Task PluginCacheEntry_DoesNotDeleteAnOpenedFile()
[Theory]
[MemberData(nameof(GetsRoundTripsValuesData))]
public async Task PluginCacheEntry_CrossDeserializationCompatibility(string[] values, bool writeWithStj)
{
// Arrange
var list = new List<OperationClaim>();
foreach (var val in values)
{
Enum.TryParse(val, out OperationClaim result);
list.Add(result);
}

using (var testDirectory = TestDirectory.Create())
{
var writeEntry = CreateEntry(testDirectory.Path, "a", "b", writeWithStj);
writeEntry.OperationClaims = list;
await writeEntry.UpdateCacheFileAsync();

// Act
var readEntry = CreateEntry(testDirectory.Path, "a", "b", !writeWithStj);
readEntry.LoadFromFile();

// Assert
Assert.True(EqualityUtility.SequenceEqualWithNullCheck(writeEntry.OperationClaims, readEntry.OperationClaims));
}
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task PluginCacheEntry_DoesNotDeleteAnOpenedFile(bool useStj)
{
// Arrange
var list = new List<OperationClaim>() { OperationClaim.Authentication };

using (var testDirectory = TestDirectory.Create())
{
var entry = new PluginCacheEntry(testDirectory.Path, "a", "b");
entry.LoadFromFile();
var entry = CreateEntry(testDirectory.Path, "a", "b", useStj);
entry.OperationClaims = list;
await entry.UpdateCacheFileAsync();

Expand All @@ -88,6 +128,7 @@ public async Task PluginCacheEntry_DoesNotDeleteAnOpenedFile()

Assert.True(File.Exists(CacheFileName));

// Act
using (var fileStream = new FileStream(
CacheFileName,
FileMode.Open,
Expand All @@ -101,17 +142,29 @@ public async Task PluginCacheEntry_DoesNotDeleteAnOpenedFile()
await entry.UpdateCacheFileAsync(); // this should not update
}

// Assert
entry.LoadFromFile();
Assert.True(EqualityUtility.SequenceEqualWithNullCheck(entry.OperationClaims, new List<OperationClaim>() { OperationClaim.Authentication }));
}
}

public static IEnumerable<object[]> GetsRoundTripsValuesData()
{
yield return new object[] { new string[] { "Authentication", "DownloadPackage" } };
yield return new object[] { new string[] { "Authentication" } };
yield return new object[] { new string[] { "DownloadPackage" } };
yield return new object[] { new string[] { } };
foreach (bool useStj in new[] { true, false })
{
yield return new object[] { new string[] { "Authentication", "DownloadPackage" }, useStj };
yield return new object[] { new string[] { "Authentication" }, useStj };
yield return new object[] { new string[] { "DownloadPackage" }, useStj };
yield return new object[] { new string[] { }, useStj };
}
}

private static PluginCacheEntry CreateEntry(string rootCacheFolder, string pluginFilePath, string requestKey, bool useStj)
{
var envReader = new Mock<IEnvironmentVariableReader>();
envReader.Setup(e => e.GetEnvironmentVariable(NuGetFeatureFlags.UseSystemTextJsonDeserializationEnvVar))
.Returns(useStj ? "true" : "false");
Comment thread
Nigusu-Allehu marked this conversation as resolved.
return new PluginCacheEntry(rootCacheFolder, pluginFilePath, requestKey, envReader.Object);
}
}
}