Helps to enumerate a directory with virtual . and .. entries
 /// <summary>
 /// Initializes a new instance of the <see cref="FactsListFormatter"/> class.
 /// </summary>
 /// <param name="user">The user to create this formatter for</param>
 /// <param name="enumerator">The enumerator for the directory listing to format</param>
 /// <param name="activeFacts">The active facts to return for the entries</param>
 /// <param name="absoluteName">Returns an absolute entry name</param>
 public FactsListFormatter(FtpUser user, DirectoryListingEnumerator enumerator, ISet<string> activeFacts, bool absoluteName)
 {
     _user = user;
     _enumerator = enumerator;
     _activeFacts = activeFacts;
     _absoluteName = absoluteName;
 }
        private async Task<FtpResponse> ProcessMlstAsync(FtpCommand command, CancellationToken cancellationToken)
        {
            var argument = command.Argument;
            var path = Data.Path.Clone();
            IUnixFileSystemEntry targetEntry;

            if (string.IsNullOrEmpty(argument))
            {
                targetEntry = path.Count == 0 ? Data.FileSystem.Root : path.Peek();
            }
            else
            {
                var foundEntry = await Data.FileSystem.SearchEntryAsync(path, argument, cancellationToken);
                if (foundEntry?.Entry == null)
                    return new FtpResponse(550, "File system entry not found.");
                targetEntry = foundEntry.Entry;
            }

            await Connection.WriteAsync($"250- {targetEntry.Name}", cancellationToken);
            var entries = new List<IUnixFileSystemEntry>()
            {
                targetEntry,
            };
            var enumerator = new DirectoryListingEnumerator(entries, Data.FileSystem, path, false);
            var formatter = new FactsListFormatter(Data.User, enumerator, Data.ActiveMlstFacts, true);
            while (enumerator.MoveNext())
            {
                var name = enumerator.Name;
                var entry = enumerator.Entry;
                var line = formatter.Format(entry, name);
                await Connection.WriteAsync($" {line}", cancellationToken);
            }

            return new FtpResponse(250, "End");
        }
        /// <inheritdoc/>
        public override async Task<FtpResponse> Process(FtpCommand command, CancellationToken cancellationToken)
        {
            await Connection.WriteAsync(new FtpResponse(150, "Opening data connection."), cancellationToken);
            ITcpSocketClient responseSocket;
            try
            {
                responseSocket = await Connection.CreateResponseSocket();
            }
            catch (Exception)
            {
                return new FtpResponse(425, "Can't open data connection.");
            }
            try
            {
                // Parse arguments in a way that's compatible with broken FTP clients
                var argument = new ListArguments(command.Argument);
                var showHidden = argument.All;

                // Instantiate the formatter
                IListFormatter formatter;
                if (string.Equals(command.Name, "NLST", StringComparison.OrdinalIgnoreCase))
                {
                    formatter = new ShortListFormatter();
                }
                else if (string.Equals(command.Name, "LS", StringComparison.OrdinalIgnoreCase))
                {
                    formatter = new LongListFormatter();
                }
                else
                {
                    formatter = new LongListFormatter();
                }

                // Parse the given path to determine the mask (e.g. when information about a file was requested)
                var directoriesToProcess = new Queue<DirectoryQueueItem>();

                // Use braces to avoid the definition of mask and path in the following parts
                // of this function.
                {
                    var mask = "*";
                    var path = Data.Path.Clone();

                    if (!string.IsNullOrEmpty(argument.Path))
                    {
                        var foundEntry = await Data.FileSystem.SearchEntryAsync(path, argument.Path, cancellationToken);
                        if (foundEntry?.Directory == null)
                            return new FtpResponse(550, "File system entry not found.");
                        var dirEntry = foundEntry.Entry as IUnixDirectoryEntry;
                        if (dirEntry == null)
                        {
                            mask = foundEntry.FileName;
                        }
                        else if (!dirEntry.IsRoot)
                        {
                            path.Push(dirEntry);
                        }
                    }
                    directoriesToProcess.Enqueue(new DirectoryQueueItem(path, mask));
                }

                var encoding = Data.NlstEncoding ?? Connection.Encoding;

                using (var stream = await Connection.CreateEncryptedStream(responseSocket.WriteStream))
                {
                    using (var writer = new StreamWriter(stream, encoding, 4096, true)
                    {
                        NewLine = "\r\n",
                    })
                    {
                        while (directoriesToProcess.Count != 0)
                        {
                            var queueItem = directoriesToProcess.Dequeue();

                            var currentPath = queueItem.Path;
                            var mask = queueItem.Mask;
                            var currentDirEntry = currentPath.Count != 0 ? currentPath.Peek() : Data.FileSystem.Root;

                            if (argument.Recursive)
                            {
                                var line = currentPath.ToDisplayString() + ":";
                                Connection.Log?.Debug(line);
                                await writer.WriteLineAsync(line);
                            }

                            var mmOptions = new Options()
                            {
                                IgnoreCase = Data.FileSystem.FileSystemEntryComparer.Equals("a", "A"),
                                NoGlobStar = true,
                                Dot = true,
                            };

                            var mm = new Minimatcher(mask, mmOptions);

                            var entries = await Data.FileSystem.GetEntriesAsync(currentDirEntry, cancellationToken);
                            var enumerator = new DirectoryListingEnumerator(entries, Data.FileSystem, currentPath, true);
                            while (enumerator.MoveNext())
                            {
                                var name = enumerator.Name;
                                if (!enumerator.IsDotEntry)
                                {
                                    if (!mm.IsMatch(name))
                                        continue;
                                    if (name.StartsWith(".") && !showHidden)
                                        continue;
                                }

                                var entry = enumerator.Entry;

                                if (argument.Recursive && !enumerator.IsDotEntry)
                                {
                                    var dirEntry = entry as IUnixDirectoryEntry;
                                    if (dirEntry != null)
                                    {
                                        var subDirPath = currentPath.Clone();
                                        subDirPath.Push(dirEntry);
                                        directoriesToProcess.Enqueue(new DirectoryQueueItem(subDirPath, "*"));
                                    }
                                }

                                var line = formatter.Format(entry, name);
                                Connection.Log?.Debug(line);
                                await writer.WriteLineAsync(line);
                            }
                        }
                    }
                }
            }
            finally
            {
                responseSocket.Dispose();
            }

            // Use 250 when the connection stays open.
            return new FtpResponse(250, "Closing data connection.");
        }
        private async Task<FtpResponse> ProcessMlsdAsync(FtpCommand command, CancellationToken cancellationToken)
        {
            var argument = command.Argument;
            var path = Data.Path.Clone();
            IUnixDirectoryEntry dirEntry;

            if (string.IsNullOrEmpty(argument))
            {
                dirEntry = path.Count == 0 ? Data.FileSystem.Root : path.Peek();
            }
            else
            {
                var foundEntry = await Data.FileSystem.SearchEntryAsync(path, argument, cancellationToken);
                if (foundEntry?.Entry == null)
                    return new FtpResponse(550, "File system entry not found.");
                dirEntry = foundEntry.Entry as IUnixDirectoryEntry;
                if (dirEntry == null)
                    return new FtpResponse(501, "Not a directory.");
                if (!dirEntry.IsRoot)
                    path.Push(dirEntry);
            }

            await Connection.WriteAsync(new FtpResponse(150, "Opening data connection."), cancellationToken);
            ITcpSocketClient responseSocket;
            try
            {
                responseSocket = await Connection.CreateResponseSocket();
            }
            catch (Exception)
            {
                return new FtpResponse(425, "Can't open data connection.");
            }

            try
            {
                var encoding = Data.NlstEncoding ?? Connection.Encoding;
                using (var stream = await Connection.CreateEncryptedStream(responseSocket.WriteStream))
                {
                    using (var writer = new StreamWriter(stream, encoding, 4096, true)
                    {
                        NewLine = "\r\n",
                    })
                    {
                        var entries = await Data.FileSystem.GetEntriesAsync(dirEntry, cancellationToken);
                        var enumerator = new DirectoryListingEnumerator(entries, Data.FileSystem, path, true);
                        var formatter = new FactsListFormatter(Data.User, enumerator, Data.ActiveMlstFacts, false);
                        while (enumerator.MoveNext())
                        {
                            var name = enumerator.Name;
                            var entry = enumerator.Entry;
                            var line = formatter.Format(entry, name);
                            Connection.Log?.Debug(line);
                            await writer.WriteLineAsync(line);
                        }
                        await writer.FlushAsync();
                    }
                    await stream.FlushAsync(cancellationToken);
                }
            }
            finally
            {
                responseSocket.Dispose();
            }

            // Use 250 when the connection stays open.
            return new FtpResponse(226, "Closing data connection.");
        }