diff --git a/PolyShim.Tests/NetCore10/ConditionalWeakTableTests.cs b/PolyShim.Tests/NetCore10/ConditionalWeakTableTests.cs new file mode 100644 index 0000000..a84fde3 --- /dev/null +++ b/PolyShim.Tests/NetCore10/ConditionalWeakTableTests.cs @@ -0,0 +1,58 @@ +using System.Runtime.CompilerServices; +using FluentAssertions; +using Xunit; + +namespace PolyShim.Tests.NetCore10; + +public class ConditionalWeakTableTests +{ + [Fact] + public void GetValue_Test() + { + // Arrange + var table = new ConditionalWeakTable(); + var missingKey = new object(); + var existingKey = new object(); + table.Add(existingKey, "hello"); + + // Act + var createdValue = table.GetValue(missingKey, _ => "world"); + var existingValue = table.GetValue(existingKey, _ => "ignored"); + + // Assert + createdValue.Should().Be("world"); + existingValue.Should().Be("hello"); + } + + [Fact] + public void TryGetValue_Test() + { + // Arrange + var table = new ConditionalWeakTable(); + var existingKey = new object(); + var missingKey = new object(); + table.Add(existingKey, "hello"); + + // Act & Assert + table.TryGetValue(existingKey, out var existingValue).Should().BeTrue(); + existingValue.Should().Be("hello"); + + table.TryGetValue(missingKey, out _).Should().BeFalse(); + } + + [Fact] + public void Remove_Test() + { + // Arrange + var table = new ConditionalWeakTable(); + var existingKey = new object(); + var missingKey = new object(); + table.Add(existingKey, "hello"); + + // Act & Assert + table.Remove(existingKey).Should().BeTrue(); + table.TryGetValue(existingKey, out _).Should().BeFalse(); + + table.Remove(missingKey).Should().BeFalse(); + } +} diff --git a/PolyShim.Tests/NetCore10/WeakReferenceTests.cs b/PolyShim.Tests/NetCore10/WeakReferenceTests.cs new file mode 100644 index 0000000..80a434c --- /dev/null +++ b/PolyShim.Tests/NetCore10/WeakReferenceTests.cs @@ -0,0 +1,33 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace PolyShim.Tests.NetCore10; + +public class WeakReferenceTests +{ + [Fact] + public void TryGetTarget_Test() + { + // Arrange + var obj = new object(); + var reference = new WeakReference(obj); + + // Act & assert + reference.TryGetTarget(out var target).Should().BeTrue(); + target.Should().BeSameAs(obj); + } + + [Fact] + public void TryGetTarget_AfterSet_Test() + { + // Arrange + var reference = new WeakReference(new object()); + var obj = new object(); + reference.SetTarget(obj); + + // Act & assert + reference.TryGetTarget(out var target).Should().BeTrue(); + target.Should().BeSameAs(obj); + } +} diff --git a/PolyShim.Tests/NetCore30/ProcessStartInfoTests.cs b/PolyShim.Tests/NetCore30/ProcessStartInfoTests.cs new file mode 100644 index 0000000..46a724b --- /dev/null +++ b/PolyShim.Tests/NetCore30/ProcessStartInfoTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Diagnostics; +using FluentAssertions; +using Xunit; + +namespace PolyShim.Tests.NetCore30; + +public class ProcessStartInfoTests +{ + [Fact] + public void ArgumentList_Test() + { + // Arrange + var startInfo = new ProcessStartInfo { UseShellExecute = false, CreateNoWindow = true }; + + if (OperatingSystem.IsWindows()) + { + startInfo.FileName = "powershell"; + startInfo.ArgumentList.Add("-Command"); + startInfo.ArgumentList.Add("exit 42"); + } + else + { + startInfo.FileName = "sh"; + startInfo.ArgumentList.Add("-c"); + startInfo.ArgumentList.Add("exit 42"); + } + + using var process = new Process { StartInfo = startInfo }; + process.Start(); + + // Act + process.WaitForExit(); + + // Assert + process.ExitCode.Should().Be(42); + } +} diff --git a/PolyShim/NetCore10/ConditionalWeakTable.cs b/PolyShim/NetCore10/ConditionalWeakTable.cs new file mode 100644 index 0000000..c386841 --- /dev/null +++ b/PolyShim/NetCore10/ConditionalWeakTable.cs @@ -0,0 +1,104 @@ +#if (NETFRAMEWORK && !NET40_OR_GREATER) || (NETSTANDARD && !NETSTANDARD1_3_OR_GREATER) +#nullable enable +#pragma warning disable CS0436 + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; + +namespace System.Runtime.CompilerServices; + +// https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.conditionalweaktable-2 +#if !POLYSHIM_INCLUDE_COVERAGE +[ExcludeFromCodeCoverage] +#endif +internal sealed class ConditionalWeakTable + where TKey : class + where TValue : class +{ + public delegate TValue CreateValueCallback(TKey key); + + private sealed class Entry(WeakReference key, TValue value) + { + public readonly WeakReference Key = key; + public readonly TValue Value = value; + } + + private readonly List _entries = []; + private readonly Lock _lock = new(); + + private void Purge() + { + for (var i = _entries.Count - 1; i >= 0; i--) + { + if (!_entries[i].Key.IsAlive) + _entries.RemoveAt(i); + } + } + + public TValue GetValue(TKey key, CreateValueCallback createValueCallback) + { + using (_lock.EnterScope()) + { + Purge(); + + foreach (var entry in _entries) + { + if (ReferenceEquals(entry.Key.Target, key)) + return entry.Value; + } + + var value = createValueCallback(key); + _entries.Add(new Entry(new WeakReference(key), value)); + + return value; + } + } + + public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) + { + using (_lock.EnterScope()) + { + Purge(); + + foreach (var entry in _entries) + { + if (ReferenceEquals(entry.Key.Target, key)) + { + value = entry.Value; + return true; + } + } + + value = default; + return false; + } + } + + public void Add(TKey key, TValue value) + { + using (_lock.EnterScope()) + { + Purge(); + _entries.Add(new Entry(new WeakReference(key), value)); + } + } + + public bool Remove(TKey key) + { + using (_lock.EnterScope()) + { + for (var i = 0; i < _entries.Count; i++) + { + if (ReferenceEquals(_entries[i].Key.Target, key)) + { + _entries.RemoveAt(i); + return true; + } + } + + return false; + } + } +} +#endif diff --git a/PolyShim/NetCore10/WeakReference.cs b/PolyShim/NetCore10/WeakReference.cs new file mode 100644 index 0000000..3d056fb --- /dev/null +++ b/PolyShim/NetCore10/WeakReference.cs @@ -0,0 +1,29 @@ +#if NETFRAMEWORK && !NET45_OR_GREATER +#nullable enable +#pragma warning disable CS0436 + +using System.Diagnostics.CodeAnalysis; + +namespace System; + +// https://learn.microsoft.com/dotnet/api/system.weakreference-1 +#if !POLYSHIM_INCLUDE_COVERAGE +[ExcludeFromCodeCoverage] +#endif +internal sealed class WeakReference(T target, bool trackResurrection) + where T : class +{ + private readonly WeakReference _reference = new(target, trackResurrection); + + public WeakReference(T target) + : this(target, false) { } + + public void SetTarget(T target) => _reference.Target = target; + + public bool TryGetTarget([NotNullWhen(true)] out T? target) + { + target = _reference.Target as T; + return target is not null; + } +} +#endif diff --git a/PolyShim/NetCore30/ProcessStartInfo.cs b/PolyShim/NetCore30/ProcessStartInfo.cs new file mode 100644 index 0000000..29c91cc --- /dev/null +++ b/PolyShim/NetCore30/ProcessStartInfo.cs @@ -0,0 +1,156 @@ +#if (NETCOREAPP && !NETCOREAPP3_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD && !NETSTANDARD2_1_OR_GREATER) +#if FEATURE_PROCESS +#nullable enable +#pragma warning disable CS0436 + +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; +using System.Diagnostics.CodeAnalysis; + +#if !POLYSHIM_INCLUDE_COVERAGE +[ExcludeFromCodeCoverage] +#endif +file sealed class ProcessStartInfoArgumentList(ProcessStartInfo startInfo) : Collection +{ + public static readonly ConditionalWeakTable< + ProcessStartInfo, + ProcessStartInfoArgumentList + > BindingTable = new(); + + private readonly WeakReference _startInfo = new(startInfo); + + private void UpdateArguments() + { + static void AppendArgument(StringBuilder buffer, string arg) + { + // Short-circuit if no escaping is needed + if (arg.Length > 0) + { + var needsEscaping = false; + foreach (var c in arg) + { + if (char.IsWhiteSpace(c) || c == '"') + { + needsEscaping = true; + break; + } + } + + if (!needsEscaping) + { + buffer.Append(arg); + return; + } + } + + buffer.Append('"'); + + for (var i = 0; i < arg.Length; ) + { + var c = arg[i++]; + + if (c == '\\') + { + var backslashCount = 1; + while (i < arg.Length && arg[i] == '\\') + { + backslashCount++; + i++; + } + + if (i == arg.Length) + { + // Backslashes at end of string: double them + buffer.Append('\\', backslashCount * 2); + } + else if (arg[i] == '"') + { + // Backslashes before a quote: double them, then escape the quote + buffer.Append('\\', backslashCount * 2 + 1).Append('"'); + i++; + } + else + { + // Backslashes not before a quote: leave them as-is + buffer.Append('\\', backslashCount); + } + } + else if (c == '"') + { + buffer.Append('\\').Append('"'); + } + else + { + buffer.Append(c); + } + } + + buffer.Append('"'); + } + + if (!_startInfo.TryGetTarget(out var startInfo)) + return; + + if (Count == 0) + { + startInfo.Arguments = string.Empty; + return; + } + + var buffer = new StringBuilder(); + foreach (var arg in this) + { + if (buffer.Length > 0) + buffer.Append(' '); + + AppendArgument(buffer, arg); + } + + startInfo.Arguments = buffer.ToString(); + } + + protected override void InsertItem(int index, string item) + { + base.InsertItem(index, item); + UpdateArguments(); + } + + protected override void RemoveItem(int index) + { + base.RemoveItem(index); + UpdateArguments(); + } + + protected override void SetItem(int index, string item) + { + base.SetItem(index, item); + UpdateArguments(); + } + + protected override void ClearItems() + { + base.ClearItems(); + UpdateArguments(); + } +} + +#if !POLYSHIM_INCLUDE_COVERAGE +[ExcludeFromCodeCoverage] +#endif +internal static class MemberPolyfills_NetCore30_ProcessStartInfo +{ + extension(ProcessStartInfo startInfo) + { + // https://learn.microsoft.com/dotnet/api/system.diagnostics.processstartinfo.argumentlist + public Collection ArgumentList => + ProcessStartInfoArgumentList.BindingTable.GetValue( + startInfo, + key => new ProcessStartInfoArgumentList(key) + ); + } +} +#endif +#endif diff --git a/Signatures.md b/Signatures.md index 28fcc80..6230bd2 100644 --- a/Signatures.md +++ b/Signatures.md @@ -1,8 +1,8 @@ # Signatures -- **Total:** 492 -- **Types:** 111 -- **Members:** 381 +- **Total:** 495 +- **Types:** 113 +- **Members:** 382 ___ @@ -60,6 +60,8 @@ ___ - [`Task CancelAsync()`](https://learn.microsoft.com/dotnet/api/system.threading.cancellationtokensource.cancelasync) .NET 8.0 - `CompilerFeatureRequiredAttribute` - [**[class]**](https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.compilerfeaturerequiredattribute) .NET 7.0 +- `ConditionalWeakTable` + - [**[class]**](https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.conditionalweaktable-2) .NET Core 1.0 - `ConfiguredCancelableAsyncEnumerable` - [**[struct]**](https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.configuredcancelableasyncenumerable-1) .NET Core 3.0 - `ConfiguredValueTaskAwaitable` @@ -375,6 +377,8 @@ ___ - [`bool WaitForExit(TimeSpan)`](https://learn.microsoft.com/dotnet/api/system.diagnostics.process.waitforexit#system-diagnostics-process-waitforexit(system-timespan)) .NET 7.0 - [`Task WaitForExitAsync(CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.diagnostics.process.waitforexitasync) .NET 5.0 - [`void Kill(bool)`](https://learn.microsoft.com/dotnet/api/system.diagnostics.process.kill#system-diagnostics-process-kill(system-boolean)) .NET Core 3.0 +- `ProcessStartInfo` + - [`Collection ArgumentList`](https://learn.microsoft.com/dotnet/api/system.diagnostics.processstartinfo.argumentlist) .NET Core 3.0 - `Progress` - [**[class]**](https://learn.microsoft.com/dotnet/api/system.progress-1) .NET Core 1.0 - `Queue` @@ -676,3 +680,5 @@ ___ - `Version` - [`static bool TryParse(string?, out Version?)`](https://learn.microsoft.com/dotnet/api/system.version.tryparse) .NET Core 1.0 - [`static Version Parse(string)`](https://learn.microsoft.com/dotnet/api/system.version.parse) .NET Core 1.0 +- `WeakReference` + - [**[class]**](https://learn.microsoft.com/dotnet/api/system.weakreference-1) .NET Core 1.0