diff --git a/eng/testing/linker/trimmingTests.targets b/eng/testing/linker/trimmingTests.targets index 8446128ce5b303..8fdc0f9b82816a 100644 --- a/eng/testing/linker/trimmingTests.targets +++ b/eng/testing/linker/trimmingTests.targets @@ -57,6 +57,10 @@ <_projectSourceFile>%(TestConsoleApps.ProjectCompileItems) + + <_additionalProjectSourceFiles Include="%(TestConsoleApps.AdditionalSourceFiles)" /> + + + diff --git a/src/libraries/System.ComponentModel.TypeConverter/src/System/ComponentModel/TypeDescriptor.cs b/src/libraries/System.ComponentModel.TypeConverter/src/System/ComponentModel/TypeDescriptor.cs index ae6c63b09c38a3..a7c831032a62e2 100644 --- a/src/libraries/System.ComponentModel.TypeConverter/src/System/ComponentModel/TypeDescriptor.cs +++ b/src/libraries/System.ComponentModel.TypeConverter/src/System/ComponentModel/TypeDescriptor.cs @@ -85,8 +85,7 @@ private TypeDescriptor() [EditorBrowsable(EditorBrowsableState.Advanced)] public static Type InterfaceType { - // TODO: replace this with DynamicallyAccessedMembersAttribute (https://github.com/dotnet/runtime/issues/37837) - [DynamicDependency("#ctor", typeof(TypeDescriptorInterface))] + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] get => typeof(TypeDescriptorInterface); } diff --git a/src/libraries/System.ComponentModel.TypeConverter/tests/TrimmingTests/InterfaceTypeTest.cs b/src/libraries/System.ComponentModel.TypeConverter/tests/TrimmingTests/InterfaceTypeTest.cs new file mode 100644 index 00000000000000..85f5d7f419a505 --- /dev/null +++ b/src/libraries/System.ComponentModel.TypeConverter/tests/TrimmingTests/InterfaceTypeTest.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel; + +/// +/// Tests that the System.ComponentModel.TypeDescriptor.InterfaceType +/// property works as expected when used in a trimmed application. +/// +class Program +{ + static int Main(string[] args) + { + Type type = TypeDescriptor.InterfaceType; + + // Tests that the ctor for System.ComponentModel.TypeDescriptor+TypeDescriptorInterface is not trimmed out. + object obj = Activator.CreateInstance(type); + string expectedObjTypeNamePrefix = "System.ComponentModel.TypeDescriptor+TypeDescriptorInterface, System.ComponentModel.TypeConverter, Version="; + + return obj != null && obj.GetType().AssemblyQualifiedName.StartsWith(expectedObjTypeNamePrefix) + ? 100 + : -1; + } +} diff --git a/src/libraries/System.Data.Common/src/System/Data/SQLTypes/SqlXml.cs b/src/libraries/System.Data.Common/src/System/Data/SQLTypes/SqlXml.cs index a1f4c409c3b1bb..a6f168d101aeaf 100644 --- a/src/libraries/System.Data.Common/src/System/Data/SQLTypes/SqlXml.cs +++ b/src/libraries/System.Data.Common/src/System/Data/SQLTypes/SqlXml.cs @@ -128,7 +128,6 @@ private static Func Crea private static MethodInfo CreateSqlReaderMethodInfo { - [DynamicDependency("CreateSqlReader", typeof(System.Xml.XmlReader))] get { if (s_createSqlReaderMethodInfo == null) diff --git a/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/CallInstruction.Generated.cs b/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/CallInstruction.Generated.cs index ed2ee838c56bc2..b58ef2ff8656c4 100644 --- a/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/CallInstruction.Generated.cs +++ b/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/CallInstruction.Generated.cs @@ -4,10 +4,10 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Dynamic; using System.Dynamic.Utils; using System.Reflection; -using System.Diagnostics.CodeAnalysis; using System.Threading; namespace System.Linq.Expressions.Interpreter @@ -160,17 +160,7 @@ private static CallInstruction FastCreate(MethodInfo target, ParameterIn #endif #if FEATURE_DLG_INVOKE - // TODO: replace these with DynamicallyAccessedMembersAttribute (https://github.com/dotnet/runtime/issues/37837) - [DynamicDependency("#ctor", typeof(ActionCallInstruction))] - [DynamicDependency("#ctor", typeof(ActionCallInstruction<>))] - [DynamicDependency("#ctor", typeof(ActionCallInstruction<,>))] - [DynamicDependency("#ctor", typeof(ActionCallInstruction<,,>))] - [DynamicDependency("#ctor", typeof(ActionCallInstruction<,,,>))] - [DynamicDependency("#ctor", typeof(FuncCallInstruction<>))] - [DynamicDependency("#ctor", typeof(FuncCallInstruction<,>))] - [DynamicDependency("#ctor", typeof(FuncCallInstruction<,,>))] - [DynamicDependency("#ctor", typeof(FuncCallInstruction<,,,>))] - [DynamicDependency("#ctor", typeof(FuncCallInstruction<,,,,>))] + [return: DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes.PublicConstructors)] private static Type GetHelperType(MethodInfo info, Type[] arrTypes) { Type t; diff --git a/src/libraries/System.Linq.Expressions/tests/TrimmingTests/GetHelperTypeTests.cs b/src/libraries/System.Linq.Expressions/tests/TrimmingTests/GetHelperTypeTests.cs new file mode 100644 index 00000000000000..57ab775e6561f5 --- /dev/null +++ b/src/libraries/System.Linq.Expressions/tests/TrimmingTests/GetHelperTypeTests.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +/// +/// Tests that the System.Linq.Expressions.Interpreter.CallInstruction.GetHelperType +/// method works as expected when used in a trimmed application. +/// +internal class Program +{ + static int Main(string[] args) + { + for (int rank = 1; rank < 6; rank++) + { + Array arrayObj = Array.CreateInstance(typeof(string), Enumerable.Repeat(1, rank).ToArray()); + arrayObj.SetValue("solitary value", Enumerable.Repeat(0, rank).ToArray()); + ConstantExpression array = Expression.Constant(arrayObj); + IEnumerable indices = Enumerable.Repeat(Expression.Default(typeof(int)), rank); + // This code path for the Compile call excercises the method being tested. + Func func = Expression.Lambda>( + Expression.ArrayAccess(array, indices)).Compile(preferInterpretation: true); + + if (func() != "solitary value") + { + return -1; + } + } + + return 100; + } +} diff --git a/src/libraries/System.Linq.Expressions/tests/TrimmingTests/System.Linq.Expressions.TrimmingTests.proj b/src/libraries/System.Linq.Expressions/tests/TrimmingTests/System.Linq.Expressions.TrimmingTests.proj new file mode 100644 index 00000000000000..da4a46f2ae141a --- /dev/null +++ b/src/libraries/System.Linq.Expressions/tests/TrimmingTests/System.Linq.Expressions.TrimmingTests.proj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/libraries/System.Net.HttpListener/src/System/Net/Windows/CookieExtensions.cs b/src/libraries/System.Net.HttpListener/src/System/Net/Windows/CookieExtensions.cs index 3543dffbfbc126..69c48a053fcac6 100644 --- a/src/libraries/System.Net.HttpListener/src/System/Net/Windows/CookieExtensions.cs +++ b/src/libraries/System.Net.HttpListener/src/System/Net/Windows/CookieExtensions.cs @@ -13,7 +13,6 @@ internal static class CookieExtensions { private static Func s_toServerStringFunc; - [DynamicDependency("ToServerString", typeof(Cookie))] public static string ToServerString(this Cookie cookie) { s_toServerStringFunc ??= (Func)typeof(Cookie).GetMethod("ToServerString", BindingFlags.Instance | BindingFlags.NonPublic).CreateDelegate(typeof(Func)); @@ -23,7 +22,6 @@ public static string ToServerString(this Cookie cookie) private static Func s_cloneFunc; - [DynamicDependency("Clone", typeof(Cookie))] public static Cookie Clone(this Cookie cookie) { s_cloneFunc ??= (Func)typeof(Cookie).GetMethod("Clone", BindingFlags.Instance | BindingFlags.NonPublic).CreateDelegate(typeof(Func)); @@ -42,7 +40,6 @@ private enum CookieVariant private static Func s_getVariantFunc; - [DynamicDependency("get_Variant", typeof(Cookie))] public static bool IsRfc2965Variant(this Cookie cookie) { s_getVariantFunc ??= (Func)typeof(Cookie).GetProperty("Variant", BindingFlags.Instance | BindingFlags.NonPublic).GetGetMethod(true).CreateDelegate(typeof(Func)); @@ -55,7 +52,6 @@ internal static class CookieCollectionExtensions { private static Func s_internalAddFunc; - [DynamicDependency("InternalAdd", typeof(CookieCollection))] public static int InternalAdd(this CookieCollection cookieCollection, Cookie cookie, bool isStrict) { s_internalAddFunc ??= (Func)typeof(CookieCollection).GetMethod("InternalAdd", BindingFlags.Instance | BindingFlags.NonPublic).CreateDelegate(typeof(Func)); diff --git a/src/libraries/System.Net.HttpListener/tests/TrimmingTests/CookieExtensionsTest.Clone.cs b/src/libraries/System.Net.HttpListener/tests/TrimmingTests/CookieExtensionsTest.Clone.cs new file mode 100644 index 00000000000000..a4f375e5baf8ca --- /dev/null +++ b/src/libraries/System.Net.HttpListener/tests/TrimmingTests/CookieExtensionsTest.Clone.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Net; +using System.Threading.Tasks; + +namespace CookieExtensionsTest +{ + /// + /// Tests that the System.Net.CookieExtensions.Clone() + /// method works as expected when used in a trimmed application. + /// + internal class Program + { + static async Task Main(string[] args) + { + var helper = new TestHelper(); + HttpListenerResponse response = await helper.GetResponse(); + var cookie = new Cookie("name", "value"); + response.SetCookie(cookie); + + // Cookies are cloned. + cookie.Value = "value3"; + if (response.Cookies[0].Value != "value") + { + return -1; + } + + return 100; + } + } +} diff --git a/src/libraries/System.Net.HttpListener/tests/TrimmingTests/CookieExtensionsTest.Helper.cs b/src/libraries/System.Net.HttpListener/tests/TrimmingTests/CookieExtensionsTest.Helper.cs new file mode 100644 index 00000000000000..a537eedf416c2f --- /dev/null +++ b/src/libraries/System.Net.HttpListener/tests/TrimmingTests/CookieExtensionsTest.Helper.cs @@ -0,0 +1,239 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace CookieExtensionsTest +{ + internal class TestHelper + { + private readonly HttpListenerFactory _factory; + private readonly Socket _client; + + public TestHelper() + { + _factory = new HttpListenerFactory(); + _client = _factory.GetConnectedSocket(); + } + + public async Task GetRequest(string requestType, string[] headers) + { + _client.Send(_factory.GetContent("1.1", requestType, query: null, text: "Text\r\n", headers, true)); + + HttpListener listener = _factory.GetListener(); + return (await listener.GetContextAsync()).Request; + } + + public async Task GetResponse() + { + _client.Send(_factory.GetContent(httpVersion: "1.1", "POST", query: null, "Give me a context, please", headers: null, headerOnly: false)); + HttpListenerContext context = await _factory.GetListener().GetContextAsync(); + return context.Response; + } + + public string GetClientResponse(int expectedLength) + { + byte[] buffer = new byte[expectedLength]; + + int totalReceived = 0; + while (totalReceived < expectedLength) + { + int bytesReceived = _client.Receive(buffer, totalReceived, buffer.Length - totalReceived, SocketFlags.None); + if (bytesReceived == 0) + { + throw new Exception($"Unexpected early end of response: received {totalReceived} bytes, expected {expectedLength}"); + } + totalReceived += bytesReceived; + } + + return Encoding.UTF8.GetString(buffer, 0, totalReceived); + } + } + + // Utilities for generating URL prefixes for HttpListener + internal class HttpListenerFactory : IDisposable + { + const int StartPort = 1025; + const int MaxStartAttempts = IPEndPoint.MaxPort - StartPort + 1; + private static readonly object s_nextPortLock = new object(); + private static int s_nextPort = StartPort; + + private readonly HttpListener _processPrefixListener; + private readonly Exception _processPrefixException; + private readonly string _processPrefix; + private readonly string _hostname; + private readonly int _port; + + public HttpListenerFactory() + { + // Find a URL prefix that is not in use on this machine *and* uses a port that's not in use. + // Once we find this prefix, keep a listener on it for the duration of the process, so other processes + // can't steal it. + _hostname = "localhost"; + string path = Guid.NewGuid().ToString("N"); + string pathComponent = $"{path}/"; + + for (int attempt = 0; attempt < MaxStartAttempts; attempt++) + { + int port = GetNextPort(); + string prefix = $"http://{_hostname}:{port}/{pathComponent}"; + + var listener = new HttpListener(); + try + { + listener.Prefixes.Add(prefix); + listener.Start(); + + _processPrefixListener = listener; + _processPrefix = prefix; + _port = port; + + _processPrefixException = null; + Socket socket = GetConnectedSocket(); + socket.Close(); + + break; + } + catch (Exception e) + { + // can't use this prefix + listener.Close(); + + // Remember the exception for later + _processPrefixException = e; + + if (e is HttpListenerException listenerException) + { + // If we can't access the host (e.g. if it is '+' or '*' and the current user is the administrator) + // then throw. + const int ERROR_ACCESS_DENIED = 5; + if (listenerException.ErrorCode == ERROR_ACCESS_DENIED && (_hostname == "*" || _hostname == "+")) + { + throw new InvalidOperationException($"Access denied for host {_hostname}"); + } + } + else if (!(e is SocketException)) + { + // If this is not an HttpListenerException or SocketException, something very wrong has happened, and there's no point + // in trying again. + break; + } + } + } + + // At this point, either we've reserved a prefix, or we've tried everything and failed. If we failed, + // we've saved the exception for later. We'll defer actually *throwing* the exception until a test + // asks for the prefix, because dealing with a type initialization exception is not nice in xunit. + } + + public int Port + { + get + { + if (_port == 0) + { + throw new Exception("Could not reserve a port for HttpListener", _processPrefixException); + } + + return _port; + } + } + + public string ListeningUrl + { + get + { + if (_processPrefix == null) + { + throw new Exception("Could not reserve a port for HttpListener", _processPrefixException); + } + + return _processPrefix; + } + } + + public HttpListener GetListener() => _processPrefixListener ?? throw new Exception("Could not reserve a port for HttpListener", _processPrefixException); + + public void Dispose() => _processPrefixListener?.Close(); + + public Socket GetConnectedSocket() + { + + if (_processPrefixException != null) + { + throw new Exception("Could not create HttpListener", _processPrefixException); + } + + string hostname = _hostname == "*" || _hostname == "+" ? "localhost" : _hostname; + + // Some platforms or distributions require IPv6 sockets if the OS supports IPv6. Others (e.g. Ubuntu) don't. + try + { + AddressFamily addressFamily = Socket.OSSupportsIPv6 ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork; + Socket socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp); + socket.Connect(hostname, Port); + return socket; + } + catch + { + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.Connect(hostname, Port); + return socket; + } + } + + public byte[] GetContent(string httpVersion, string requestType, string query, string text, IEnumerable headers, bool headerOnly) + { + headers ??= Enumerable.Empty(); + + Uri listeningUri = new Uri(ListeningUrl); + string rawUrl = listeningUri.PathAndQuery; + if (query != null) + { + rawUrl += query; + } + + string content = $"{requestType} {rawUrl} HTTP/{httpVersion}\r\n"; + if (!headers.Any(header => header.ToLower().StartsWith("host:"))) + { + content += $"Host: { listeningUri.Host}\r\n"; + } + if (text != null && !headers.Any(header => header.ToLower().StartsWith("content-length:"))) + { + content += $"Content-Length: {text.Length}\r\n"; + } + foreach (string header in headers) + { + content += header + "\r\n"; + } + content += "\r\n"; + + if (!headerOnly && text != null) + { + content += text; + } + + return Encoding.UTF8.GetBytes(content); + } + + private static int GetNextPort() + { + lock (s_nextPortLock) + { + int port = s_nextPort++; + if (s_nextPort > IPEndPoint.MaxPort) + { + s_nextPort = StartPort; + } + return port; + } + } + } +} diff --git a/src/libraries/System.Net.HttpListener/tests/TrimmingTests/CookieExtensionsTest.InternalAdd.cs b/src/libraries/System.Net.HttpListener/tests/TrimmingTests/CookieExtensionsTest.InternalAdd.cs new file mode 100644 index 00000000000000..67232ae6b9b24f --- /dev/null +++ b/src/libraries/System.Net.HttpListener/tests/TrimmingTests/CookieExtensionsTest.InternalAdd.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Net; +using System.Threading.Tasks; + +namespace CookieExtensionsTest +{ + /// + /// Tests that the System.Net.CookieExtensions.InternalAdd() + /// method works as expected when used in a trimmed application. + /// + internal class Program + { + static async Task Main(string[] args) + { + var helper = new TestHelper(); + + string cookieString = "cookie: name=value"; + Cookie expected = new Cookie("name", "value"); + + HttpListenerRequest request = await helper.GetRequest("POST", new[] { cookieString }); + Cookie actual = request.Cookies[0]; + + if (request.Cookies.Count != 1 || + actual.Name != expected.Name || + actual.Value != expected.Value || + actual.Port != expected.Port || + actual.Path != expected.Path || + actual.Domain != expected.Domain) + { + return -1; + } + + return 100; + } + } +} diff --git a/src/libraries/System.Net.HttpListener/tests/TrimmingTests/CookieExtensionsTest.ToServerString.cs b/src/libraries/System.Net.HttpListener/tests/TrimmingTests/CookieExtensionsTest.ToServerString.cs new file mode 100644 index 00000000000000..3a7dab2d9ee072 --- /dev/null +++ b/src/libraries/System.Net.HttpListener/tests/TrimmingTests/CookieExtensionsTest.ToServerString.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Net; +using System.Threading.Tasks; + +namespace CookieExtensionsTest +{ + /// + /// Tests that the System.Net.CookieExtensions.ToServerString() + /// method works as expected when used in a trimmed application. + /// + internal class Program + { + static async Task Main(string[] args) + { + var helper = new TestHelper(); + + CookieCollection cookies = new CookieCollection + { + new Cookie("name1", "value1"), + new Cookie("name2", "value2") { Port = "\"300\"" } + }; + + int expectedBytes = 196; + string expectedSetCookie = "Set-Cookie: name1=value1"; + string expectedSetCookie2 = "Set-Cookie2: name2=value2; Port=\"300\"; Version=1"; + + HttpListenerResponse response = await helper.GetResponse(); + response.Cookies = cookies; + + response.Close(); + + if (expectedSetCookie.Replace("Set-Cookie: ", "") != response.Headers["Set-Cookie"] || + expectedSetCookie2.Replace("Set-Cookie2: ", "") != response.Headers["Set-Cookie2"]) + { + return -1; + } + + string clientResponse = helper.GetClientResponse(expectedBytes); + + if (!clientResponse.Contains($"\r\n{expectedSetCookie}\r\n") || + !clientResponse.Contains($"\r\n{expectedSetCookie2}\r\n")) + { + return -1; + } + + return 100; + } + } +} diff --git a/src/libraries/System.Net.HttpListener/tests/TrimmingTests/System.Net.HttpListener.TrimmingTests.proj b/src/libraries/System.Net.HttpListener/tests/TrimmingTests/System.Net.HttpListener.TrimmingTests.proj new file mode 100644 index 00000000000000..2ab8b1999b438e --- /dev/null +++ b/src/libraries/System.Net.HttpListener/tests/TrimmingTests/System.Net.HttpListener.TrimmingTests.proj @@ -0,0 +1,17 @@ + + + + + + CookieExtensionsTest.Helper.cs + + + CookieExtensionsTest.Helper.cs + + + CookieExtensionsTest.Helper.cs + + + + +