Skip to content

use Regex Source Generator for .NET 7+ #1401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 20, 2024
Merged
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
7 changes: 0 additions & 7 deletions src/Renci.SshNet/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,6 @@ dotnet_diagnostic.S2589.severity = none

dotnet_diagnostic.S2372.severity = none

#### SYSLIB diagnostics ####

# SYSLIB1045: Use 'GeneratedRegexAttribute' to generate the regular expression implementation at compile-time
#
# TODO: Remove this when https://github.com/sshnet/SSH.NET/issues/1131 is implemented.
dotnet_diagnostic.SYSLIB1045.severity = none

#### StyleCop Analyzers rules ####

# SA1123: Do not place regions within elements
Expand Down
26 changes: 20 additions & 6 deletions src/Renci.SshNet/Connection/HttpConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,32 @@ namespace Renci.SshNet.Connection
/// </item>
/// </list>
/// </remarks>
internal sealed class HttpConnector : ProxyConnector
internal sealed partial class HttpConnector : ProxyConnector
{
private const string HttpResponsePattern = @"HTTP/(?<version>\d[.]\d) (?<statusCode>\d{3}) (?<reasonPhrase>.+)$";
private const string HttpHeaderPattern = @"(?<fieldName>[^\[\]()<>@,;:\""/?={} \t]+):(?<fieldValue>.+)?";

#if NET7_0_OR_GREATER
private static readonly Regex HttpResponseRegex = GetHttpResponseRegex();
private static readonly Regex HttpHeaderRegex = GetHttpHeaderRegex();

[GeneratedRegex(HttpResponsePattern)]
private static partial Regex GetHttpResponseRegex();

[GeneratedRegex(HttpHeaderPattern)]
private static partial Regex GetHttpHeaderRegex();
#else
private static readonly Regex HttpResponseRegex = new Regex(HttpResponsePattern, RegexOptions.Compiled);
private static readonly Regex HttpHeaderRegex = new Regex(HttpHeaderPattern, RegexOptions.Compiled);
#endif

public HttpConnector(ISocketFactory socketFactory)
: base(socketFactory)
{
}

protected override void HandleProxyConnect(IConnectionInfo connectionInfo, Socket socket)
{
var httpResponseRe = new Regex(@"HTTP/(?<version>\d[.]\d) (?<statusCode>\d{3}) (?<reasonPhrase>.+)$");
var httpHeaderRe = new Regex(@"(?<fieldName>[^\[\]()<>@,;:\""/?={} \t]+):(?<fieldValue>.+)?");

SocketAbstraction.Send(socket, SshData.Ascii.GetBytes(string.Format(CultureInfo.InvariantCulture,
"CONNECT {0}:{1} HTTP/1.0\r\n",
connectionInfo.Host,
Expand Down Expand Up @@ -71,7 +85,7 @@ protected override void HandleProxyConnect(IConnectionInfo connectionInfo, Socke

if (statusCode is null)
{
var statusMatch = httpResponseRe.Match(response);
var statusMatch = HttpResponseRegex.Match(response);
if (statusMatch.Success)
{
var httpStatusCode = statusMatch.Result("${statusCode}");
Expand All @@ -86,7 +100,7 @@ protected override void HandleProxyConnect(IConnectionInfo connectionInfo, Socke
}

// continue on parsing message headers coming from the server
var headerMatch = httpHeaderRe.Match(response);
var headerMatch = HttpHeaderRegex.Match(response);
if (headerMatch.Success)
{
var fieldName = headerMatch.Result("${fieldName}");
Expand Down
16 changes: 12 additions & 4 deletions src/Renci.SshNet/Connection/ProtocolVersionExchange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,19 @@ namespace Renci.SshNet.Connection
/// <remarks>
/// https://tools.ietf.org/html/rfc4253#section-4.2.
/// </remarks>
internal sealed class ProtocolVersionExchange : IProtocolVersionExchange
internal sealed partial class ProtocolVersionExchange : IProtocolVersionExchange
{
private const byte Null = 0x00;
private const string ServerVersionPattern = "^SSH-(?<protoversion>[^-]+)-(?<softwareversion>.+?)([ ](?<comments>.+))?$";

private static readonly Regex ServerVersionRe = new Regex("^SSH-(?<protoversion>[^-]+)-(?<softwareversion>.+?)([ ](?<comments>.+))?$", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
#if NET7_0_OR_GREATER
private static readonly Regex ServerVersionRegex = GetServerVersionRegex();

[GeneratedRegex(ServerVersionPattern, RegexOptions.ExplicitCapture)]
private static partial Regex GetServerVersionRegex();
#else
private static readonly Regex ServerVersionRegex = new Regex(ServerVersionPattern, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
#endif

/// <summary>
/// Performs the SSH protocol version exchange.
Expand Down Expand Up @@ -57,7 +65,7 @@ public SshIdentification Start(string clientVersion, Socket socket, TimeSpan tim
throw CreateServerResponseDoesNotContainIdentification(bytesReceived);
}

var identificationMatch = ServerVersionRe.Match(line);
var identificationMatch = ServerVersionRegex.Match(line);
if (identificationMatch.Success)
{
return new SshIdentification(GetGroupValue(identificationMatch, "protoversion"),
Expand Down Expand Up @@ -104,7 +112,7 @@ public async Task<SshIdentification> StartAsync(string clientVersion, Socket soc
throw CreateServerResponseDoesNotContainIdentification(bytesReceived);
}

var identificationMatch = ServerVersionRe.Match(line);
var identificationMatch = ServerVersionRegex.Match(line);
if (identificationMatch.Success)
{
return new SshIdentification(GetGroupValue(identificationMatch, "protoversion"),
Expand Down
25 changes: 20 additions & 5 deletions src/Renci.SshNet/Netconf/NetConfSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,32 @@

namespace Renci.SshNet.NetConf
{
internal sealed class NetConfSession : SubsystemSession, INetConfSession
internal sealed partial class NetConfSession : SubsystemSession, INetConfSession
{
private const string Prompt = "]]>]]>";

private const string LengthPattern = @"\n#(?<length>\d+)\n";
private const string ReplyPattern = @"\n##\n";
private readonly StringBuilder _data = new StringBuilder();
private bool _usingFramingProtocol;
private EventWaitHandle _serverCapabilitiesConfirmed = new AutoResetEvent(initialState: false);
private EventWaitHandle _rpcReplyReceived = new AutoResetEvent(initialState: false);
private StringBuilder _rpcReply = new StringBuilder();
private int _messageId;

#if NET7_0_OR_GREATER
private static readonly Regex LengthRegex = GetLengthRegex();
private static readonly Regex ReplyRegex = GetReplyRegex();

[GeneratedRegex(LengthPattern)]
private static partial Regex GetLengthRegex();

[GeneratedRegex(ReplyPattern)]
private static partial Regex GetReplyRegex();
#else
private static readonly Regex LengthRegex = new Regex(LengthPattern, RegexOptions.Compiled);
private static readonly Regex ReplyRegex = new Regex(ReplyPattern, RegexOptions.Compiled);
#endif

/// <summary>
/// Gets NetConf server capabilities.
/// </summary>
Expand Down Expand Up @@ -145,7 +160,7 @@ protected override void OnDataReceived(byte[] data)

for (; ; )
{
var match = Regex.Match(chunk.Substring(position), @"\n#(?<length>\d+)\n");
var match = LengthRegex.Match(chunk.Substring(position));
if (!match.Success)
{
break;
Expand All @@ -157,9 +172,9 @@ protected override void OnDataReceived(byte[] data)
}

#if NET7_0_OR_GREATER
if (Regex.IsMatch(chunk.AsSpan(position), @"\n##\n"))
if (ReplyRegex.IsMatch(chunk.AsSpan(position)))
#else
if (Regex.IsMatch(chunk.Substring(position), @"\n##\n"))
if (ReplyRegex.IsMatch(chunk.Substring(position)))
#endif // NET7_0_OR_GREATER
{
_ = _rpcReplyReceived.Set();
Expand Down
13 changes: 11 additions & 2 deletions src/Renci.SshNet/PrivateKeyFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,19 @@ namespace Renci.SshNet
/// </list>
/// </para>
/// </remarks>
public class PrivateKeyFile : IPrivateKeySource, IDisposable
public partial class PrivateKeyFile : IPrivateKeySource, IDisposable
{
private static readonly Regex PrivateKeyRegex = new Regex(@"^-+ *BEGIN (?<keyName>\w+( \w+)*) PRIVATE KEY *-+\r?\n((Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?<cipherName>[A-Z0-9-]+),(?<salt>[A-F0-9]+)\r?\n\r?\n)|(Comment: ""?[^\r\n]*""?\r?\n))?(?<data>([a-zA-Z0-9/+=]{1,80}\r?\n)+)(\r?\n)?-+ *END \k<keyName> PRIVATE KEY *-+",
private const string PrivateKeyPattern = @"^-+ *BEGIN (?<keyName>\w+( \w+)*) PRIVATE KEY *-+\r?\n((Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?<cipherName>[A-Z0-9-]+),(?<salt>[A-F0-9]+)\r?\n\r?\n)|(Comment: ""?[^\r\n]*""?\r?\n))?(?<data>([a-zA-Z0-9/+=]{1,80}\r?\n)+)(\r?\n)?-+ *END \k<keyName> PRIVATE KEY *-+";

#if NET7_0_OR_GREATER
private static readonly Regex PrivateKeyRegex = GetPrivateKeyRegex();

[GeneratedRegex(PrivateKeyPattern, RegexOptions.Multiline | RegexOptions.ExplicitCapture)]
private static partial Regex GetPrivateKeyRegex();
#else
private static readonly Regex PrivateKeyRegex = new Regex(PrivateKeyPattern,
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture);
#endif

private readonly List<HostAlgorithm> _hostAlgorithms = new List<HostAlgorithm>();
private Key _key;
Expand Down
34 changes: 27 additions & 7 deletions src/Renci.SshNet/ScpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,31 @@ namespace Renci.SshNet
public partial class ScpClient : BaseClient
{
private const string Message = "filename";
private static readonly Regex FileInfoRe = new Regex(@"C(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)", RegexOptions.Compiled);
private const string FileInfoPattern = @"C(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)";
private const string DirectoryInfoPattern = @"D(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)";
private const string TimestampPattern = @"T(?<mtime>\d+) 0 (?<atime>\d+) 0";

#if NET7_0_OR_GREATER
private static readonly Regex FileInfoRegex = GetFileInfoRegex();
private static readonly Regex DirectoryInfoRegex = GetDirectoryInfoRegex();
private static readonly Regex TimestampRegex = GetTimestampRegex();

[GeneratedRegex(FileInfoPattern)]
private static partial Regex GetFileInfoRegex();

[GeneratedRegex(DirectoryInfoPattern)]
private static partial Regex GetDirectoryInfoRegex();

[GeneratedRegex(TimestampPattern)]
private static partial Regex GetTimestampRegex();
#else
private static readonly Regex FileInfoRegex = new Regex(FileInfoPattern, RegexOptions.Compiled);
private static readonly Regex DirectoryInfoRegex = new Regex(DirectoryInfoPattern, RegexOptions.Compiled);
private static readonly Regex TimestampRegex = new Regex(TimestampPattern, RegexOptions.Compiled);
#endif

private static readonly byte[] SuccessConfirmationCode = { 0 };
private static readonly byte[] ErrorConfirmationCode = { 1 };
private static readonly Regex DirectoryInfoRe = new Regex(@"D(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)", RegexOptions.Compiled);
private static readonly Regex TimestampRe = new Regex(@"T(?<mtime>\d+) 0 (?<atime>\d+) 0", RegexOptions.Compiled);

private IRemotePathTransformation _remotePathTransformation;
private TimeSpan _operationTimeout;
Expand Down Expand Up @@ -458,7 +478,7 @@ public void Download(string filename, Stream destination)
SendSuccessConfirmation(channel); // Send reply

var message = ReadString(input);
var match = FileInfoRe.Match(message);
var match = FileInfoRegex.Match(message);

if (match.Success)
{
Expand Down Expand Up @@ -757,7 +777,7 @@ private void InternalDownload(IChannelSession channel, Stream input, FileSystemI
continue;
}

var match = DirectoryInfoRe.Match(message);
var match = DirectoryInfoRegex.Match(message);
if (match.Success)
{
SendSuccessConfirmation(channel); // Send reply
Expand All @@ -784,7 +804,7 @@ private void InternalDownload(IChannelSession channel, Stream input, FileSystemI
continue;
}

match = FileInfoRe.Match(message);
match = FileInfoRegex.Match(message);
if (match.Success)
{
// Read file
Expand Down Expand Up @@ -814,7 +834,7 @@ private void InternalDownload(IChannelSession channel, Stream input, FileSystemI
continue;
}

match = TimestampRe.Match(message);
match = TimestampRegex.Match(message);
if (match.Success)
{
// Read timestamp
Expand Down