SmtpServer icon indicating copy to clipboard operation
SmtpServer copied to clipboard

Pipereader example require internal "unsafe" class

Open large opened this issue 3 years ago • 1 comments

Hi @cosullivan , I am currently upgrading to v9.0 and had to rebuild my ListenFactory with PIPE as design is now.

In your example: https://github.com/cosullivan/SmtpServer/blob/2b455e652bb451dd3f38a6b7b84fb1b06e4dbfb3/Examples/SampleApp/Examples/CustomEndpointListenerExample.cs#L133 The StringUtil are "unsafe" because of the fixed buffers and pointers in the internal class. (I'm an old c++ programmer, so that is why I write "unsafe". You know what you are doing, so it is not unsafe... :))

By using your internal class StringUtil in an example would bind you to implement the class or SmtpProject to get it compiled using the Nuget. Wouldn't be enough to use Streamreader/Streamwriter to handle PIPEs in your SmtpServer?

large avatar Jul 23 '21 10:07 large

I played with your example to get the full communication between server -> client. Here is my suggestion to achieve that:

    public sealed class CustomSecurableDuplexPipe : ISecurableDuplexPipe
    {
        readonly ISecurableDuplexPipe _securableDuplexPipe;

        public CustomSecurableDuplexPipe(ISecurableDuplexPipe securableDuplexPipe)
        {
            _securableDuplexPipe = securableDuplexPipe;
        }

        public Task UpgradeAsync(X509Certificate certificate, SslProtocols protocols, CancellationToken cancellationToken = default)
        {
            return _securableDuplexPipe.UpgradeAsync(certificate, protocols, cancellationToken);
        }

        public void Dispose()
        {
            Console.WriteLine("Raw transcript of last connection:");
            foreach (var logLine in LogArray)
                Console.WriteLine(logLine);

            _securableDuplexPipe.Dispose();
        }

	//Actual variable that holds the log
        private List<String> LogArray = new List<String>();

        public PipeReader Input => new LoggingPipeReader(_securableDuplexPipe.Input, LogArray);

        public PipeWriter Output => new LoggingPipeWriter(_securableDuplexPipe.Output, LogArray);

        public bool IsSecure => _securableDuplexPipe.IsSecure;
    }

    //Quite simular to LoggingPipeReader and as simple as possible
    public sealed class LoggingPipeWriter : PipeWriter
    {
        readonly PipeWriter _delegate;
        readonly List<String> _logArray; //Store data until EOF

        public LoggingPipeWriter(PipeWriter @delegate, List<String> @logArray)
        {
            _delegate = @delegate;
            _logArray = @logArray;
        }

        public override void Advance(int bytes)
        {
            var spandata = GetSpan().Slice(0, bytes);
            String str = Encoding.UTF8.GetString(spandata);
            _logArray.Add(str);
            _delegate.Advance(bytes);
        }

        //Unused, but required overrides
        public override void CancelPendingFlush()
            => _delegate.CancelPendingFlush();

        public override void Complete(Exception exception = null)
            => _delegate.Complete(exception);

        public override ValueTask<FlushResult> FlushAsync(CancellationToken cancellationToken = default)
            => _delegate.FlushAsync(cancellationToken);

        public override Memory<byte> GetMemory(int sizeHint = 0)
            => _delegate.GetMemory(sizeHint);

        public override Span<byte> GetSpan(int sizeHint = 0)
            => _delegate.GetSpan(sizeHint);
    }


    //LoggingPipeReader with helper for getting data after each CRLF
    public sealed class LoggingPipeReader : PipeReader
    {
        readonly PipeReader _delegate;
        readonly List<String> _logArray;

        public LoggingPipeReader(PipeReader @delegate, List<String> @logArray)
        {
            _delegate = @delegate;
            _logArray = @logArray;
        }

        public override async ValueTask<ReadResult> ReadAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            ReadResult readResult = await _delegate.ReadAsync(cancellationToken);
            ReadOnlySequence<byte> buffer = readResult.Buffer;
            string tmp = ProcessLine(ref buffer); ;
            if (tmp != null)
                _logArray.Add(tmp);
            return readResult;
        }

        //Unused, but required overrides
        public override void AdvanceTo(SequencePosition consumed) => _delegate.AdvanceTo(consumed);
        public override void AdvanceTo(SequencePosition consumed, SequencePosition examined)
            => _delegate.AdvanceTo(consumed, examined);
        public override void CancelPendingRead() => _delegate.CancelPendingRead();
        public override void Complete(Exception exception = null) => _delegate.Complete(exception);
        public override bool TryRead(out ReadResult result) => _delegate.TryRead(out result);

        //Helper class, raw copy from: https://dev.to/joni2nja/evaluating-readline-using-system-io-pipelines-performance-in-c-part-2-pmf
        private static ReadOnlySpan<byte> NewLine => new[] { (byte)'\r', (byte)'\n' };
        private static string ProcessLine(ref ReadOnlySequence<byte> buffer)
        {
            string str = null;

            if (buffer.IsSingleSegment)
            {
                var span = buffer.FirstSpan;
                int consumed;
                while (span.Length > 0)
                {
                    var newLine = span.IndexOf(NewLine);

                    if (newLine == -1) break;

                    var line = span.Slice(0, newLine);
                    str = Encoding.UTF8.GetString(line);

                    // simulate string processing
                    //str = str.AsSpan().Slice(0, 5).ToString();

                    consumed = line.Length + NewLine.Length;
                    span = span.Slice(consumed);
                    buffer = buffer.Slice(consumed);
                }
            }
            else
            {
                var sequenceReader = new SequenceReader<byte>(buffer);

                while (!sequenceReader.End)
                {
                    while (sequenceReader.TryReadTo(out ReadOnlySequence<byte> line, NewLine))
                    {
                        str = Encoding.UTF8.GetString(line);

                        // simulate string processing
                        //str = str.AsSpan().Slice(0, 5).ToString();
                    }

                    buffer = buffer.Slice(sequenceReader.Position);
                    sequenceReader.Advance(buffer.Length);
                }
            }

            return str;
        }
    }

The helper for the reader here seems to be quite good. Since data is short it never triggers SequenceReader though, so that is probably useless.

large avatar Jul 23 '21 21:07 large