Skip to content

Commit 4073495

Browse files
committed
NuGet.Server is case sensitive for Packages(Id='',Version='') endpoint
NuGet/NuGetGallery#2988
1 parent 1bb5796 commit 4073495

7 files changed

Lines changed: 179 additions & 2 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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;
5+
using System.Linq.Expressions;
6+
using System.Reflection;
7+
8+
namespace NuGet.Server.DataServices
9+
{
10+
public class IgnoreCaseForPackageIdInterceptor : ExpressionVisitor
11+
{
12+
private static readonly MemberInfo _idMember = typeof(ODataPackage).GetProperty("Id");
13+
private static readonly Expression<Func<string, string, int>> _ordinalIgnoreCaseComparer = (a, b) => StringComparer.OrdinalIgnoreCase.Compare(a, b);
14+
15+
protected override Expression VisitBinary(BinaryExpression node)
16+
{
17+
// Change equality comparisons on Version to normalized comparisons on NormalizedVersion
18+
if (node.NodeType == ExpressionType.Equal)
19+
{
20+
// Figure out which side is the target
21+
ConstantExpression constSide = (node.Left as ConstantExpression) ?? (node.Right as ConstantExpression);
22+
if (constSide != null && constSide.Type == typeof(string))
23+
{
24+
MemberExpression memberSide = (node.Right as MemberExpression) ?? (node.Left as MemberExpression);
25+
if (memberSide != null && memberSide.Member == _idMember)
26+
{
27+
// We have a "Package.Id == <constant>" expression!
28+
29+
// Rewrite it to StringComparer.OrdinalIgnoreCase.Compare(a, b) == 0
30+
return Expression.MakeBinary(
31+
ExpressionType.Equal,
32+
Expression.Invoke(_ordinalIgnoreCaseComparer,
33+
Expression.MakeMemberAccess(memberSide.Expression, _idMember),
34+
constSide
35+
),
36+
Expression.Constant(0));
37+
}
38+
}
39+
}
40+
return node;
41+
}
42+
}
43+
}

src/NuGet.Server/DataServices/PackageContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public IQueryable<ODataPackage> Packages
2222
.GetPackages()
2323
.Select(package => package.AsODataPackage())
2424
.AsQueryable()
25-
.InterceptWith(new NormalizeVersionInterceptor());
25+
.InterceptWith(new IgnoreCaseForPackageIdInterceptor(), new NormalizeVersionInterceptor());
2626
}
2727
}
2828
}

src/NuGet.Server/Infrastructure/ServerPackageRepository.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using System.Threading;
1111
using System.Threading.Tasks;
1212
using System.Web.Configuration;
13-
using NuGet.Resources;
1413
using NuGet.Server.Logging;
1514

1615
namespace NuGet.Server.Infrastructure

src/NuGet.Server/NuGet.Server.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
<Compile Include="Core\FrameworkNameExtensions.cs" />
7878
<Compile Include="Core\IServiceResolver.cs" />
7979
<Compile Include="Core\PackageExtensions.cs" />
80+
<Compile Include="DataServices\IgnoreCaseForPackageIdInterceptor.cs" />
8081
<Compile Include="DataServices\ODataPackage.cs" />
8182
<Compile Include="DataServices\PackageContext.cs" />
8283
<Compile Include="DataServices\PackageExtensions.cs" />
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Linq.Expressions;
8+
using System.Reflection;
9+
using NuGet.Server.DataServices;
10+
using Xunit;
11+
12+
namespace NuGet.Server.Tests
13+
{
14+
public class IgnoreCaseForPackageIdInterceptorTest
15+
{
16+
private static readonly MemberInfo _idMember = typeof(ODataPackage).GetProperty("Id");
17+
private static readonly Expression<Func<string, string, int>> _ordinalIgnoreCaseComparer = (a, b) => StringComparer.OrdinalIgnoreCase.Compare(a, b);
18+
19+
public static IEnumerable<object[]> TheoryData
20+
{
21+
get
22+
{
23+
return new[]
24+
{
25+
new object[]
26+
{
27+
Expression.MakeBinary(
28+
ExpressionType.Equal,
29+
Expression.Constant("NEWTONSOFT.JSON"),
30+
Expression.MakeMemberAccess(Expression.Parameter(typeof(ODataPackage)), _idMember)),
31+
32+
Expression.MakeBinary(
33+
ExpressionType.Equal,
34+
Expression.Invoke(_ordinalIgnoreCaseComparer,
35+
Expression.MakeMemberAccess(Expression.Parameter(typeof(ODataPackage)), _idMember),
36+
Expression.Constant("NEWTONSOFT.JSON")
37+
),
38+
Expression.Constant(0))
39+
},
40+
41+
new object[]
42+
{
43+
Expression.MakeBinary(
44+
ExpressionType.Equal,
45+
Expression.MakeMemberAccess(Expression.Parameter(typeof(ODataPackage)), _idMember),
46+
Expression.Constant("NEWTONSOFT.JSON")),
47+
48+
Expression.MakeBinary(
49+
ExpressionType.Equal,
50+
Expression.Invoke(_ordinalIgnoreCaseComparer,
51+
Expression.MakeMemberAccess(Expression.Parameter(typeof(ODataPackage)), _idMember),
52+
Expression.Constant("NEWTONSOFT.JSON")
53+
),
54+
Expression.Constant(0))
55+
}
56+
};
57+
}
58+
}
59+
60+
[Theory]
61+
[MemberData("TheoryData")]
62+
public void RewritesIdComparisonToIgnoreCaseComparison(Expression originalExpression, Expression expectedExpression)
63+
{
64+
// Arrange
65+
var interceptor = new IgnoreCaseForPackageIdInterceptor();
66+
67+
// Act
68+
var rewrittenExpression = interceptor.Visit(originalExpression);
69+
70+
// Assert
71+
Assert.Equal(rewrittenExpression.ToString(), expectedExpression.ToString());
72+
}
73+
74+
[Fact]
75+
public void FindsPackagesIgnoringCase()
76+
{
77+
// Arrange
78+
var data = new List<ODataPackage>();
79+
data.Add(new ODataPackage { Id = "foo" });
80+
data.Add(new ODataPackage { Id = "BAR" });
81+
data.Add(new ODataPackage { Id = "bAz" });
82+
83+
var queryable = data.AsQueryable().InterceptWith(new IgnoreCaseForPackageIdInterceptor());
84+
85+
// Act
86+
var result1 = queryable.FirstOrDefault(p => p.Id == "foo");
87+
var result2 = queryable.FirstOrDefault(p => p.Id == "FOO");
88+
var result3 = queryable.FirstOrDefault(p => p.Id == "Foo");
89+
90+
var result4 = queryable.FirstOrDefault(p => p.Id == "bar");
91+
var result5 = queryable.FirstOrDefault(p => p.Id == "BAR");
92+
var result6 = queryable.FirstOrDefault(p => p.Id == "baR");
93+
94+
var result7 = queryable.FirstOrDefault(p => p.Id == "baz");
95+
var result8 = queryable.FirstOrDefault(p => p.Id == "BAZ");
96+
var result9 = queryable.FirstOrDefault(p => p.Id == "bAz");
97+
98+
// Assert
99+
Assert.Equal(result1.Id, data[0].Id);
100+
Assert.Equal(result2.Id, data[0].Id);
101+
Assert.Equal(result3.Id, data[0].Id);
102+
103+
Assert.Equal(result4.Id, data[1].Id);
104+
Assert.Equal(result5.Id, data[1].Id);
105+
Assert.Equal(result6.Id, data[1].Id);
106+
107+
Assert.Equal(result7.Id, data[2].Id);
108+
Assert.Equal(result8.Id, data[2].Id);
109+
Assert.Equal(result9.Id, data[2].Id);
110+
}
111+
}
112+
}

test/NuGet.Server.Tests/NormalizeVersionInterceptorTest.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System.Collections.Generic;
5+
using System.Linq;
56
using System.Linq.Expressions;
67
using System.Reflection;
78
using NuGet.Server.DataServices;
@@ -62,5 +63,25 @@ public void RewritesVersionPropertyNameToNormalizedVersionPropertyName(Expressio
6263
// Assert
6364
Assert.Equal(rewrittenExpression.ToString(), expectedExpression.ToString());
6465
}
66+
67+
[Fact]
68+
public void FindsPackagesUsingNormalizedVersion()
69+
{
70+
// Arrange
71+
var data = new List<ODataPackage>();
72+
data.Add(new ODataPackage { Id = "foo", Version = "1.0.0.0.0.0", NormalizedVersion = "1.0.0"});
73+
74+
var queryable = data.AsQueryable().InterceptWith(new NormalizeVersionInterceptor());
75+
76+
// Act
77+
var result1 = queryable.FirstOrDefault(p => p.Version == "1.0");
78+
var result2 = queryable.FirstOrDefault(p => p.Version == "1.0.0");
79+
var result3 = queryable.FirstOrDefault(p => p.Version == "1.0.0.0");
80+
81+
// Assert
82+
Assert.Equal(result1, data[0]);
83+
Assert.Equal(result2, data[0]);
84+
Assert.Equal(result3, data[0]);
85+
}
6586
}
6687
}

test/NuGet.Server.Tests/NuGet.Server.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
</CodeAnalysisDependentAssemblyPaths>
6767
</ItemGroup>
6868
<ItemGroup>
69+
<Compile Include="IgnoreCaseForPackageIdInterceptorTest.cs" />
6970
<Compile Include="JsonNetPackagesSerializerTests.cs" />
7071
<Compile Include="FeedPackageTest.cs" />
7172
<Compile Include="Infrastructure\TemporaryDirectory.cs" />

0 commit comments

Comments
 (0)