Skip to content
Draft
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
63 changes: 63 additions & 0 deletions src/Api/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Text.RegularExpressions;

namespace Void.Proxy.Api.Extensions;

public static partial class StringExtensions
{
private static readonly char[] DefaultDelimiters = [',', ';', '\n'];

/// <summary>
/// Splits a string by multiple delimiters with optional escape character support.
/// </summary>
/// <param name="input">The string to split.</param>
/// <param name="delimiters">Custom delimiters to use. If null, uses default delimiters: comma, semicolon, and newline.</param>
/// <param name="escapeCharacter">Optional escape character (e.g., '\') to allow escaped delimiters in the input.</param>
/// <param name="removeEmptyEntries">Whether to remove empty entries from the result. Defaults to true.</param>
/// <returns>An array of strings split by the delimiters.</returns>
public static string[] SplitByDelimiters(this string input, char[]? delimiters = null, char? escapeCharacter = null, bool removeEmptyEntries = true)
{
if (string.IsNullOrWhiteSpace(input))
return [];

delimiters ??= DefaultDelimiters;

if (escapeCharacter is not null)
{
return SplitByDelimitersWithEscape(input, delimiters, escapeCharacter.Value, removeEmptyEntries);
}

var options = removeEmptyEntries ? StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries : StringSplitOptions.TrimEntries;
return input.Split(delimiters, options);
}

private static string[] SplitByDelimitersWithEscape(string input, char[] delimiters, char escapeCharacter, bool removeEmptyEntries)
{
var pattern = BuildEscapedDelimiterPattern(delimiters, escapeCharacter);
var regex = new Regex(pattern, RegexOptions.Compiled);
var parts = regex.Split(input);

var result = parts.Select(part => UnescapeDelimiters(part, delimiters, escapeCharacter).Trim()).ToArray();

if (removeEmptyEntries)
result = result.Where(part => !string.IsNullOrWhiteSpace(part)).ToArray();

return result;
}

private static string BuildEscapedDelimiterPattern(char[] delimiters, char escapeCharacter)
{
var escapedChar = Regex.Escape(escapeCharacter.ToString());
var delimiterPattern = string.Join("|", delimiters.Select(d => Regex.Escape(d.ToString())));
return $"(?<!{escapedChar})(?:{delimiterPattern})";
}

private static string UnescapeDelimiters(string input, char[] delimiters, char escapeCharacter)
{
foreach (var delimiter in delimiters)
{
input = input.Replace($"{escapeCharacter}{delimiter}", delimiter.ToString());
}

return input;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Reflection;
using System.Runtime.Loader;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using Nito.Disposables.Internals;
using NuGet.Common;
using NuGet.Configuration;
Expand All @@ -17,11 +16,12 @@
using Void.Proxy.Api.Console;
using Void.Proxy.Api.Events;
using Void.Proxy.Api.Events.Proxy;
using Void.Proxy.Api.Extensions;
using Void.Proxy.Api.Plugins.Dependencies;

namespace Void.Proxy.Plugins.Dependencies.Nuget;

public partial class NuGetDependencyResolver(ILogger<NuGetDependencyResolver> logger, IRunOptions runOptions, IConsoleService console, HttpClient httpClient) : INuGetDependencyResolver, IEventListener
public class NuGetDependencyResolver(ILogger<NuGetDependencyResolver> logger, IRunOptions runOptions, IConsoleService console, HttpClient httpClient) : INuGetDependencyResolver, IEventListener
{
private static readonly Option<string[]> RepositoryOption = new("--repository", "-r")
{
Expand All @@ -42,7 +42,7 @@ public partial class NuGetDependencyResolver(ILogger<NuGetDependencyResolver> lo
private readonly NuGet.Common.ILogger _nugetLogger = console.GetOptionValue(EnableNugetLoggingOption) ? new NuGetLogger(logger) : NullLogger.Instance;
private readonly HashSet<string> _repositories = [];

private IEnumerable<string> UriRepositories => UnescapedSemicolonRegex().Split(Environment.GetEnvironmentVariable("VOID_NUGET_REPOSITORIES") ?? "").Select(repo => repo.Replace(@"\;", ";")).Concat(_repositories.Concat(console.GetOptionValue(RepositoryOption) ?? [])).Where(uri => !string.IsNullOrWhiteSpace(uri));
private IEnumerable<string> UriRepositories => (Environment.GetEnvironmentVariable("VOID_NUGET_REPOSITORIES") ?? "").SplitByDelimiters([';'], escapeCharacter: '\\').Concat(_repositories.Concat(console.GetOptionValue(RepositoryOption) ?? [])).Where(uri => !string.IsNullOrWhiteSpace(uri));
private IEnumerable<SourceRepository> Repositories
{
get
Expand Down Expand Up @@ -543,7 +543,4 @@ private async Task ProbeRepositoriesAsync(CancellationToken cancellationToken =
foreach (var (url, status) in statuses)
logger.LogInformation(" - {Url} [{Status}]", url, status);
}

[GeneratedRegex(@"(?<!\\);")]
private static partial Regex UnescapedSemicolonRegex();
}
3 changes: 2 additions & 1 deletion src/Platform/Plugins/PluginService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Void.Proxy.Api.Events;
using Void.Proxy.Api.Events.Plugins;
using Void.Proxy.Api.Events.Services;
using Void.Proxy.Api.Extensions;
using Void.Proxy.Api.Plugins;
using Void.Proxy.Api.Plugins.Dependencies;
using Void.Proxy.Plugins.Containers;
Expand Down Expand Up @@ -125,7 +126,7 @@ static string[] GetVariablesPlugins()
if (string.IsNullOrWhiteSpace(args))
return [];

return args.Split(',', ';');
return args.SplitByDelimiters();
}
}

Expand Down
135 changes: 135 additions & 0 deletions src/Tests/ExtensionsTests/StringExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using Void.Proxy.Api.Extensions;
using Xunit;

namespace Void.Tests.ExtensionsTests;

public class StringExtensionsTests
{
[Fact]
public void SplitByDelimiters_WithDefaultDelimiters_SplitsByComma()
{
const string input = "a,b,c";
var result = input.SplitByDelimiters();
Assert.Equal(["a", "b", "c"], result);
}

[Fact]
public void SplitByDelimiters_WithDefaultDelimiters_SplitsBySemicolon()
{
const string input = "a;b;c";
var result = input.SplitByDelimiters();
Assert.Equal(["a", "b", "c"], result);
}

[Fact]
public void SplitByDelimiters_WithDefaultDelimiters_SplitsByNewline()
{
const string input = "a\nb\nc";
var result = input.SplitByDelimiters();
Assert.Equal(["a", "b", "c"], result);
}

[Fact]
public void SplitByDelimiters_WithDefaultDelimiters_SplitsByMixedDelimiters()
{
const string input = "a,b;c\nd";
var result = input.SplitByDelimiters();
Assert.Equal(["a", "b", "c", "d"], result);
}

[Fact]
public void SplitByDelimiters_WithDefaultDelimiters_RemovesEmptyEntries()
{
const string input = "a,,b;c";
var result = input.SplitByDelimiters();
Assert.Equal(["a", "b", "c"], result);
}

[Fact]
public void SplitByDelimiters_WithDefaultDelimiters_TrimsEntries()
{
const string input = " a , b ; c ";
var result = input.SplitByDelimiters();
Assert.Equal(["a", "b", "c"], result);
}

[Fact]
public void SplitByDelimiters_WithEmptyString_ReturnsEmptyArray()
{
const string input = "";
var result = input.SplitByDelimiters();
Assert.Empty(result);
}

[Fact]
public void SplitByDelimiters_WithWhitespaceString_ReturnsEmptyArray()
{
const string input = " ";
var result = input.SplitByDelimiters();
Assert.Empty(result);
}

[Fact]
public void SplitByDelimiters_WithCustomDelimiters_SplitsCorrectly()
{
const string input = "a|b|c";
var result = input.SplitByDelimiters(['|']);
Assert.Equal(["a", "b", "c"], result);
}

[Fact]
public void SplitByDelimiters_WithEscapeCharacter_HandlesEscapedDelimiters()
{
const string input = @"a\;b;c";
var result = input.SplitByDelimiters([';'], escapeCharacter: '\\');
Assert.Equal(["a;b", "c"], result);
}

[Fact]
public void SplitByDelimiters_WithEscapeCharacter_HandlesMultipleEscapedDelimiters()
{
const string input = @"a\;b\;c;d";
var result = input.SplitByDelimiters([';'], escapeCharacter: '\\');
Assert.Equal(["a;b;c", "d"], result);
}

[Fact]
public void SplitByDelimiters_WithEscapeCharacter_HandlesUnescapedDelimiters()
{
const string input = "a;b;c";
var result = input.SplitByDelimiters([';'], escapeCharacter: '\\');
Assert.Equal(["a", "b", "c"], result);
}

[Fact]
public void SplitByDelimiters_WithEscapeCharacter_HandlesComplexCase()
{
const string input = @"https://user:pass@repo1.com\;https://user:pass@repo2.com;https://repo3.com";
var result = input.SplitByDelimiters([';'], escapeCharacter: '\\');
Assert.Equal(["https://user:pass@repo1.com;https://user:pass@repo2.com", "https://repo3.com"], result);
}

[Fact]
public void SplitByDelimiters_RemoveEmptyEntriesFalse_KeepsEmptyEntries()
{
const string input = "a,,b;c";
var result = input.SplitByDelimiters(removeEmptyEntries: false);
Assert.Equal(["a", "", "b", "c"], result);
}

[Fact]
public void SplitByDelimiters_VoidPluginsScenario_WorksCorrectly()
{
const string input = "plugin1.dll,plugin2.dll;plugin3.dll";
var result = input.SplitByDelimiters();
Assert.Equal(["plugin1.dll", "plugin2.dll", "plugin3.dll"], result);
}

[Fact]
public void SplitByDelimiters_VoidNugetRepositoriesScenario_WorksCorrectly()
{
const string input = @"https://user1:pass1@repo1.com\;extra;https://repo2.com";
var result = input.SplitByDelimiters([';'], escapeCharacter: '\\');
Assert.Equal(["https://user1:pass1@repo1.com;extra", "https://repo2.com"], result);
}
}