1738 lines
54 KiB
C#
1738 lines
54 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
#if NET45
|
|
using System.Threading.Tasks;
|
|
#endif
|
|
|
|
namespace FluentFTP {
|
|
|
|
/// <summary>
|
|
/// Parses a line from a file listing using the first successful parser, or the specified parser.
|
|
/// Returns an FtpListItem object representing the parsed line, or null if the line was unable to be parsed.
|
|
/// </summary>
|
|
public class FtpListParser {
|
|
|
|
#region Constants
|
|
|
|
// DATE/TIME FORMATS
|
|
private string[] unixDateFormats1 = { "MMM'-'d'-'yyyy", "MMM'-'dd'-'yyyy" };
|
|
private string[] unixDateFormats2 = { "MMM'-'d'-'yyyy'-'HH':'mm", "MMM'-'dd'-'yyyy'-'HH':'mm", "MMM'-'d'-'yyyy'-'H':'mm", "MMM'-'dd'-'yyyy'-'H':'mm", "MMM'-'dd'-'yyyy'-'H'.'mm" };
|
|
private string[] unixAltDateFormats1 = { "MMM'-'d'-'yyyy", "MMM'-'dd'-'yyyy" };
|
|
private string[] unixAltDateFormats2 = { "MMM'-'d'-'yyyy'-'HH':'mm:ss", "MMM'-'dd'-'yyyy'-'HH':'mm:ss", "MMM'-'d'-'yyyy'-'H':'mm:ss", "MMM'-'dd'-'yyyy'-'H':'mm:ss" };
|
|
private string[] windowsDateFormats = { "MM'-'dd'-'yy hh':'mmtt", "MM'-'dd'-'yy HH':'mm", "MM'-'dd'-'yyyy hh':'mmtt" };
|
|
private string[][] ibmDateFormats = { new string[] { "dd'/'MM'/'yy' 'HH':'mm':'ss", "dd'/'MM'/'yyyy' 'HH':'mm':'ss", "dd'.'MM'.'yy' 'HH':'mm':'ss" }, new string[] { "yy'/'MM'/'dd' 'HH':'mm':'ss", "yyyy'/'MM'/'dd' 'HH':'mm':'ss", "yy'.'MM'.'dd' 'HH':'mm':'ss" }, new string[] { "MM'/'dd'/'yy' 'HH':'mm':'ss", "MM'/'dd'/'yyyy' 'HH':'mm':'ss", "MM'.'dd'.'yy' 'HH':'mm':'ss" } };
|
|
private string[] nonstopDateFormats = { "d'-'MMM'-'yy HH':'mm':'ss" };
|
|
|
|
// FIELDS REQUIRED
|
|
private static int MIN_EXPECTED_FIELD_COUNT_UNIX = 7;
|
|
private static int MIN_EXPECTED_FIELD_COUNT_UNIXALT = 8;
|
|
private static int MIN_EXPECTED_FIELD_COUNT_VMS = 4;
|
|
private static int MIN_EXPECTED_FIELD_COUNT_OS400 = 5;
|
|
private static int MIN_EXPECTED_FIELD_COUNT_TANDEM = 7;
|
|
|
|
// UNIX
|
|
private static string SYMLINK_ARROW = "->";
|
|
private static char SYMLINK_CHAR = 'l';
|
|
private static char ORDINARY_FILE_CHAR = '-';
|
|
private static char DIRECTORY_CHAR = 'd';
|
|
|
|
// WINDOWS
|
|
private static string WIN_DIR = "<DIR>";
|
|
private static char[] WIN_SEP = { ' ' };
|
|
private static int MIN_EXPECTED_FIELD_COUNT_WIN = 4;
|
|
|
|
// VMS
|
|
private static string VMS_DIR = ".DIR";
|
|
private static string VMS_HDR = "Directory";
|
|
private static string VMS_TOTAL = "Total";
|
|
private static int DEFAULT_BLOCKSIZE = 512 * 1024;
|
|
|
|
// IBM
|
|
private static string IBM_DIR = "*DIR";
|
|
private static string IBM_DDIR = "*DDIR";
|
|
private static string IBM_MEM = "*MEM";
|
|
private static string IBM_FILE = "*FILE";
|
|
|
|
// NONSTOP
|
|
private static char[] NONSTOP_TRIM = { '"' };
|
|
|
|
#endregion
|
|
|
|
#region API
|
|
|
|
/// <summary>
|
|
/// the FTP connection that owns this parser
|
|
/// </summary>
|
|
public FtpClient client;
|
|
|
|
private static List<FtpParser> parsers = new List<FtpParser>{
|
|
FtpParser.Unix, FtpParser.Windows, FtpParser.IBM, FtpParser.VMS, FtpParser.NonStop
|
|
};
|
|
|
|
/// <summary>
|
|
/// which server type? (SYST)
|
|
/// </summary>
|
|
public string system;
|
|
|
|
/// <summary>
|
|
/// current parser, or parser set by user
|
|
/// </summary>
|
|
public FtpParser parser = FtpParser.Auto;
|
|
|
|
/// <summary>
|
|
/// parser calculated based on system type (SYST command)
|
|
/// </summary>
|
|
public FtpParser detectedParser = FtpParser.Auto;
|
|
|
|
/// <summary>
|
|
/// if we have detected that the current parser is valid
|
|
/// </summary>
|
|
public bool parserConfirmed = false;
|
|
|
|
/// <summary>
|
|
/// which culture to read filenames with?
|
|
/// </summary>
|
|
public CultureInfo parserCulture = CultureInfo.InvariantCulture;
|
|
|
|
/// <summary>
|
|
/// what is the time offset between server/client?
|
|
/// </summary>
|
|
public TimeSpan timeOffset = new TimeSpan();
|
|
|
|
/// <summary>
|
|
/// any time offset between server/client?
|
|
/// </summary>
|
|
public bool hasTimeOffset = false;
|
|
|
|
/// <summary>
|
|
/// VMS ONLY : the blocksize used to calculate the file
|
|
/// </summary>
|
|
public int vmsBlocksize = DEFAULT_BLOCKSIZE;
|
|
|
|
/// <summary>
|
|
/// Is the version number returned as part of the filename?
|
|
///
|
|
/// Some VMS FTP servers do not permit a file to be deleted unless
|
|
/// the filename includes the version number. Note that directories are
|
|
/// never returned with the version number.
|
|
/// </summary>
|
|
public bool vmsNameHasVersion = false;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="FtpListParser"/> class.
|
|
/// </summary>
|
|
/// <param name="client">An existing <see cref="FtpClient"/> object</param>
|
|
public FtpListParser(FtpClient client = null) {
|
|
this.client = client;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to auto-detect which parser is suitable given a system string.
|
|
/// </summary>
|
|
/// <param name="system">result of SYST command</param>
|
|
public void Init(string system) {
|
|
|
|
parserConfirmed = false;
|
|
this.system = system != null ? system.Trim() : null;
|
|
|
|
if (system != null) {
|
|
if (system.ToUpper().StartsWith("WINDOWS")) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Info, "Auto-detected Windows listing parser");
|
|
parser = FtpParser.Windows;
|
|
} else if (system.ToUpper().IndexOf("UNIX") >= 0 || system.ToUpper().IndexOf("AIX") >= 0) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Info, "Auto-detected UNIX listing parser");
|
|
parser = FtpParser.Unix;
|
|
} else if (system.ToUpper().IndexOf("VMS") >= 0) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Info, "Auto-detected VMS listing parser");
|
|
parser = FtpParser.VMS;
|
|
} else if (system.ToUpper().IndexOf("OS/400") >= 0) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Info, "Auto-detected OS/400 listing parser");
|
|
parser = FtpParser.IBM;
|
|
} else {
|
|
parser = FtpParser.Unix;
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Warn, "Cannot auto-detect listing parser for system '" + system + "', using Unix parser");
|
|
}
|
|
}
|
|
|
|
detectedParser = parser;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse raw file list from server into file objects, using the currently active parser.
|
|
/// </summary>
|
|
public FtpListItem[] ParseMultiLine(string[] fileStrings, bool isMachineList) {
|
|
|
|
FtpListItem[] files = new FtpListItem[fileStrings.Length];
|
|
|
|
if (fileStrings.Length == 0) {
|
|
return files;
|
|
}
|
|
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Parse() called using culture: " + parserCulture.EnglishName);
|
|
|
|
ValidateParser(fileStrings);
|
|
|
|
int count = 0;
|
|
for (int i = 0; i < fileStrings.Length; i++) {
|
|
if (fileStrings[i] == null || fileStrings[i].Trim().Length == 0)
|
|
continue;
|
|
|
|
try {
|
|
|
|
// MULTI LINE LISTINGS
|
|
FtpListItem file = null;
|
|
if (IsMultiLine(parser)) {
|
|
StringBuilder filename = new StringBuilder(fileStrings[i]);
|
|
while (i + 1 < fileStrings.Length && fileStrings[i + 1].IndexOf(';') < 0) {
|
|
filename.Append(" ").Append(fileStrings[i + 1]);
|
|
i++;
|
|
}
|
|
file = ParseSingleLine(null, filename.ToString(), FtpCapability.NONE, isMachineList);
|
|
} else {
|
|
|
|
// SINGLE LINE LISTINGS
|
|
file = ParseSingleLine(null, fileStrings[i], FtpCapability.NONE, isMachineList);
|
|
}
|
|
|
|
// skip blank lines
|
|
if (file != null) {
|
|
files[count++] = file;
|
|
}
|
|
|
|
} catch (CriticalListParseException) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Restarting parsing from first entry in list");
|
|
i = -1;
|
|
count = 0;
|
|
continue;
|
|
}
|
|
}
|
|
FtpListItem[] result = new FtpListItem[count];
|
|
Array.Copy(files, 0, result, 0, count);
|
|
return result;
|
|
}
|
|
|
|
private bool IsMultiLine(FtpParser p) {
|
|
return p == FtpParser.VMS;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse raw file from server into a file object, using the currently active parser.
|
|
/// </summary>
|
|
public FtpListItem ParseSingleLine(string path, string file, FtpCapability caps, bool isMachineList) {
|
|
|
|
FtpListItem result = null;
|
|
|
|
// force machine listing if it is
|
|
if (isMachineList) {
|
|
result = ParseMachineList(file, caps);
|
|
} else {
|
|
|
|
// use custom parser if given
|
|
if (m_customParser != null) {
|
|
result = m_customParser(file, caps);
|
|
} else {
|
|
|
|
if (IsWrongParser()) {
|
|
ValidateParser(new string[] { file });
|
|
}
|
|
|
|
// use one of the in-built parsers
|
|
switch (parser) {
|
|
case FtpParser.Legacy:
|
|
result = ParseLegacy(path, file, caps);
|
|
break;
|
|
case FtpParser.Machine:
|
|
result = ParseMachineList(file, caps);
|
|
break;
|
|
case FtpParser.Windows:
|
|
result = ParseWindows(file);
|
|
break;
|
|
case FtpParser.Unix:
|
|
result = ParseUnix(file);
|
|
break;
|
|
case FtpParser.UnixAlt:
|
|
result = ParseUnixAlt(file);
|
|
break;
|
|
case FtpParser.VMS:
|
|
result = ParseVMS(file);
|
|
break;
|
|
case FtpParser.IBM:
|
|
result = ParseIBM(file);
|
|
break;
|
|
case FtpParser.NonStop:
|
|
result = ParseNonstop(file);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if parsed file successfully
|
|
if (result != null) {
|
|
|
|
// apply time difference between server/client
|
|
if (hasTimeOffset) {
|
|
result.Modified = result.Modified - timeOffset;
|
|
}
|
|
|
|
// calc absolute file paths
|
|
CalcFullPaths(result, path, false);
|
|
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate if the current parser is correct, or if another parser seems more appropriate.
|
|
/// </summary>
|
|
private void ValidateParser(string[] files) {
|
|
|
|
if (IsWrongParser()) {
|
|
|
|
// by default use the UNIX parser, if none detected
|
|
if (detectedParser == FtpParser.Auto) {
|
|
detectedParser = FtpParser.Unix;
|
|
}
|
|
if (parser == FtpParser.Auto) {
|
|
parser = detectedParser;
|
|
}
|
|
|
|
// if machine listings not supported, switch to UNIX parser
|
|
if (IsWrongMachineListing()) {
|
|
parser = detectedParser;
|
|
}
|
|
|
|
// use the initially set parser (from SYST)
|
|
if (IsParserValid(parser, files)) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Confirmed format " + parser.ToString());
|
|
parserConfirmed = true;
|
|
return;
|
|
}
|
|
foreach (FtpParser p in parsers) {
|
|
if (IsParserValid(p, files)) {
|
|
parser = p;
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Detected format " + parser.ToString());
|
|
parserConfirmed = true;
|
|
return;
|
|
}
|
|
}
|
|
parser = FtpParser.Unix;
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Could not detect format. Using default " + parser.ToString());
|
|
|
|
}
|
|
}
|
|
|
|
private bool IsWrongParser() {
|
|
return parser == FtpParser.Auto || !parserConfirmed || IsWrongMachineListing();
|
|
}
|
|
private bool IsWrongMachineListing() {
|
|
return parser == FtpParser.Machine && client != null && !client.HasFeature(FtpCapability.MLSD);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate if the current parser is correct
|
|
/// </summary>
|
|
private bool IsParserValid(FtpParser p, string[] files) {
|
|
switch (p) {
|
|
case FtpParser.Windows:
|
|
return IsWindowsValid(files);
|
|
case FtpParser.Unix:
|
|
return IsUnixValid(files);
|
|
case FtpParser.VMS:
|
|
return IsVMSValid(files);
|
|
case FtpParser.IBM:
|
|
return IsIBMValid(files);
|
|
case FtpParser.NonStop:
|
|
return IsNonstopValid(files);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Legacy Parsers
|
|
|
|
/// <summary>
|
|
/// Parses a line from a file listing using the first successful match in the Parsers collection.
|
|
/// </summary>
|
|
/// <param name="path">The source path of the file listing</param>
|
|
/// <param name="buf">A line from the file listing</param>
|
|
/// <param name="capabilities">Server capabilities</param>
|
|
/// <returns>A FtpListItem object representing the parsed line, null if the line was
|
|
/// unable to be parsed. If you have encountered an unsupported list type add a parser
|
|
/// to the public static Parsers collection of FtpListItem.</returns>
|
|
private static FtpListItem ParseLegacy(string path, string buf, FtpCapability capabilities) {
|
|
if (!string.IsNullOrEmpty(buf)) {
|
|
FtpListItem item;
|
|
|
|
foreach (Parser parser in Parsers) {
|
|
if ((item = parser(buf, capabilities)) != null) {
|
|
item.Input = buf;
|
|
return item;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Used for synchronizing access to the Parsers collection
|
|
/// </summary>
|
|
private static Object m_parserLock = new Object();
|
|
|
|
/// <summary>
|
|
/// Initializes the default list of parsers
|
|
/// </summary>
|
|
private static void InitParsers() {
|
|
lock (m_parserLock) {
|
|
if (m_parsers == null) {
|
|
m_parsers = new List<Parser>();
|
|
m_parsers.Add(new Parser(ParseMachineList));
|
|
m_parsers.Add(new Parser(ParseUnixList));
|
|
m_parsers.Add(new Parser(ParseDosList));
|
|
m_parsers.Add(new Parser(ParseVMSList));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static List<Parser> m_parsers = null;
|
|
/// <summary>
|
|
/// Collection of parsers. Each parser object contains
|
|
/// a regex string that uses named groups, i.e., (?<group_name>foobar).
|
|
/// The support group names are modify for last write time, size for the
|
|
/// size and name for the name of the file system object. Each group name is
|
|
/// optional, if they are present then those values are retrieved from a
|
|
/// successful match. In addition, each parser contains a Type property
|
|
/// which gets set in the FtpListItem object to distinguish between different
|
|
/// types of objects.
|
|
/// </summary>
|
|
private static Parser[] Parsers {
|
|
get {
|
|
Parser[] parsers;
|
|
|
|
lock (m_parserLock) {
|
|
if (m_parsers == null)
|
|
InitParsers();
|
|
|
|
parsers = m_parsers.ToArray();
|
|
}
|
|
|
|
return parsers;
|
|
}
|
|
}
|
|
|
|
private static Parser m_customParser;
|
|
|
|
/// <summary>
|
|
/// Adds a custom parser
|
|
/// </summary>
|
|
/// <param name="parser">The parser delegate to add</param>
|
|
/// <example><code source="..\Examples\CustomParser.cs" lang="cs" /></example>
|
|
public static void AddParser(Parser parser) {
|
|
lock (m_parserLock) {
|
|
if (m_parsers == null)
|
|
InitParsers();
|
|
|
|
m_parsers.Add(parser);
|
|
m_customParser = parser;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all parser delegates
|
|
/// </summary>
|
|
public static void ClearParsers() {
|
|
lock (m_parserLock) {
|
|
if (m_parsers == null)
|
|
InitParsers();
|
|
|
|
m_parsers.Clear();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the specified parser
|
|
/// </summary>
|
|
/// <param name="parser">The parser delegate to remove</param>
|
|
public static void RemoveParser(Parser parser) {
|
|
lock (m_parserLock) {
|
|
if (m_parsers == null)
|
|
InitParsers();
|
|
|
|
m_parsers.Remove(parser);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses LIST format listings
|
|
/// </summary>
|
|
/// <param name="buf">A line from the listing</param>
|
|
/// <param name="capabilities">Server capabilities</param>
|
|
/// <returns>FtpListItem if the item is able to be parsed</returns>
|
|
private static FtpListItem ParseUnixList(string buf, FtpCapability capabilities) {
|
|
string regex =
|
|
@"(?<permissions>.+)\s+" +
|
|
@"(?<objectcount>\d+)\s+" +
|
|
@"(?<user>.+)\s+" +
|
|
@"(?<group>.+)\s+" +
|
|
@"(?<size>\d+)\s+" +
|
|
@"(?<modify>\w+\s+\d+\s+\d+:\d+|\w+\s+\d+\s+\d+)\s" +
|
|
@"(?<name>.*)$";
|
|
FtpListItem item = new FtpListItem();
|
|
Match m;
|
|
|
|
if (!(m = Regex.Match(buf, regex, RegexOptions.IgnoreCase)).Success)
|
|
return null;
|
|
|
|
// if this field is missing we can't determine
|
|
// what the object is.
|
|
if (m.Groups["permissions"].Value.Length == 0)
|
|
return null;
|
|
|
|
switch (m.Groups["permissions"].Value[0]) {
|
|
case 'd':
|
|
item.Type = FtpFileSystemObjectType.Directory;
|
|
break;
|
|
case '-':
|
|
case 's':
|
|
item.Type = FtpFileSystemObjectType.File;
|
|
break;
|
|
case 'l':
|
|
item.Type = FtpFileSystemObjectType.Link;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
// if we can't determine a file name then
|
|
// we are not considering this a successful parsing operation.
|
|
if (m.Groups["name"].Value.Length < 1)
|
|
return null;
|
|
item.Name = m.Groups["name"].Value;
|
|
|
|
switch (item.Type) {
|
|
case FtpFileSystemObjectType.Directory:
|
|
// ignore these...
|
|
if (item.Name == "." || item.Name == "..")
|
|
return null;
|
|
break;
|
|
case FtpFileSystemObjectType.Link:
|
|
if (!item.Name.Contains(" -> "))
|
|
return null;
|
|
item.LinkTarget = item.Name.Remove(0, item.Name.IndexOf("-> ") + 3).Trim();
|
|
item.Name = item.Name.Remove(item.Name.IndexOf(" -> "));
|
|
break;
|
|
}
|
|
|
|
// for date parser testing only
|
|
//capabilities = ~(capabilities & FtpCapability.MDTM);
|
|
|
|
////
|
|
// Ignore the Modify times sent in LIST format for files
|
|
// when the server has support for the MDTM command
|
|
// because they will never be as accurate as what can be had
|
|
// by using the MDTM command. MDTM does not work on directories
|
|
// so if a modify time was parsed from the listing we will try
|
|
// to convert it to a DateTime object and use it for directories.
|
|
////
|
|
if (((capabilities & FtpCapability.MDTM) != FtpCapability.MDTM || item.Type == FtpFileSystemObjectType.Directory) && m.Groups["modify"].Value.Length > 0) {
|
|
item.Modified = m.Groups["modify"].Value.GetFtpDate(DateTimeStyles.AssumeLocal);
|
|
if (item.Modified == DateTime.MinValue) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Warn, "GetFtpDate() failed on " + m.Groups["modify"].Value);
|
|
}
|
|
} else {
|
|
if (m.Groups["modify"].Value.Length == 0)
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Warn, "RegEx failed to parse modified date from " + buf);
|
|
else if (item.Type == FtpFileSystemObjectType.Directory)
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Warn, "Modified times of directories are ignored in UNIX long listings.");
|
|
else if ((capabilities & FtpCapability.MDTM) == FtpCapability.MDTM)
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Warn, "Ignoring modified date because MDTM feature is present. If you aren't already, pass FtpListOption.Modify or FtpListOption.SizeModify to GetListing() to retrieve the modification time.");
|
|
}
|
|
|
|
if (m.Groups["size"].Value.Length > 0) {
|
|
long size;
|
|
|
|
if (long.TryParse(m.Groups["size"].Value, out size))
|
|
item.Size = size;
|
|
}
|
|
|
|
if (m.Groups["permissions"].Value.Length > 0) {
|
|
CalcUnixPermissions(item, m.Groups["permissions"].Value);
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses IIS DOS format listings
|
|
/// </summary>
|
|
/// <param name="buf">A line from the listing</param>
|
|
/// <param name="capabilities">Server capabilities</param>
|
|
/// <returns>FtpListItem if the item is able to be parsed</returns>
|
|
private static FtpListItem ParseDosList(string buf, FtpCapability capabilities) {
|
|
FtpListItem item = new FtpListItem();
|
|
string[] datefmt = new string[] {
|
|
"MM-dd-yy hh:mmtt",
|
|
"MM-dd-yyyy hh:mmtt"
|
|
};
|
|
Match m;
|
|
|
|
// directory
|
|
if ((m = Regex.Match(buf, @"(?<modify>\d+-\d+-\d+\s+\d+:\d+\w+)\s+<DIR>\s+(?<name>.*)$", RegexOptions.IgnoreCase)).Success) {
|
|
DateTime modify;
|
|
|
|
item.Type = FtpFileSystemObjectType.Directory;
|
|
item.Name = m.Groups["name"].Value;
|
|
|
|
//if (DateTime.TryParse(m.Groups["modify"].Value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out modify))
|
|
if (DateTime.TryParseExact(m.Groups["modify"].Value, datefmt, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out modify))
|
|
item.Modified = modify;
|
|
}
|
|
// file
|
|
else if ((m = Regex.Match(buf, @"(?<modify>\d+-\d+-\d+\s+\d+:\d+\w+)\s+(?<size>\d+)\s+(?<name>.*)$", RegexOptions.IgnoreCase)).Success) {
|
|
DateTime modify;
|
|
long size;
|
|
|
|
item.Type = FtpFileSystemObjectType.File;
|
|
item.Name = m.Groups["name"].Value;
|
|
|
|
if (long.TryParse(m.Groups["size"].Value, out size))
|
|
item.Size = size;
|
|
|
|
//if (DateTime.TryParse(m.Groups["modify"].Value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out modify))
|
|
if (DateTime.TryParseExact(m.Groups["modify"].Value, datefmt, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out modify))
|
|
item.Modified = modify;
|
|
} else
|
|
return null;
|
|
|
|
return item;
|
|
}
|
|
|
|
private static FtpListItem ParseVMSList(string buf, FtpCapability capabilities) {
|
|
string regex =
|
|
@"(?<name>.+)\.(?<extension>.+);(?<version>\d+)\s+" +
|
|
@"(?<size>\d+)\s+" +
|
|
@"(?<modify>\d+-\w+-\d+\s+\d+:\d+)";
|
|
Match m;
|
|
|
|
if ((m = Regex.Match(buf, regex)).Success) {
|
|
FtpListItem item = new FtpListItem();
|
|
|
|
item.Name = (m.Groups["name"].Value + "." +
|
|
m.Groups["extension"].Value + ";" +
|
|
m.Groups["version"].Value);
|
|
|
|
if (m.Groups["extension"].Value.ToUpper() == "DIR")
|
|
item.Type = FtpFileSystemObjectType.Directory;
|
|
else
|
|
item.Type = FtpFileSystemObjectType.File;
|
|
|
|
long itemSize = 0;
|
|
if (!long.TryParse(m.Groups["size"].Value, out itemSize))
|
|
itemSize = -1;
|
|
|
|
item.Size = itemSize;
|
|
|
|
DateTime itemModified = DateTime.MinValue;
|
|
|
|
if (!DateTime.TryParse(m.Groups["modify"].Value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out itemModified))
|
|
itemModified = DateTime.MinValue;
|
|
|
|
item.Modified = itemModified;
|
|
|
|
return item;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ftp listing line parser
|
|
/// </summary>
|
|
/// <param name="line">The line from the listing</param>
|
|
/// <param name="capabilities">The server capabilities</param>
|
|
/// <returns>FtpListItem if the line can be parsed, null otherwise</returns>
|
|
public delegate FtpListItem Parser(string line, FtpCapability capabilities);
|
|
|
|
#endregion
|
|
|
|
#region Machine Listing Parser
|
|
|
|
/// <summary>
|
|
/// Parses MLSD/MLST format listings
|
|
/// </summary>
|
|
/// <param name="buf">A line from the listing</param>
|
|
/// <param name="capabilities">Server capabilities</param>
|
|
/// <returns>FtpListItem if the item is able to be parsed</returns>
|
|
private static FtpListItem ParseMachineList(string buf, FtpCapability capabilities) {
|
|
FtpListItem item = new FtpListItem();
|
|
Match m;
|
|
|
|
if (!(m = Regex.Match(buf, "type=(?<type>.+?);", RegexOptions.IgnoreCase)).Success)
|
|
return null;
|
|
|
|
switch (m.Groups["type"].Value.ToLower()) {
|
|
case "dir":
|
|
case "pdir":
|
|
case "cdir":
|
|
item.Type = FtpFileSystemObjectType.Directory;
|
|
break;
|
|
case "file":
|
|
item.Type = FtpFileSystemObjectType.File;
|
|
break;
|
|
// These are not supported for now.
|
|
case "link":
|
|
case "device":
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
if ((m = Regex.Match(buf, "; (?<name>.*)$", RegexOptions.IgnoreCase)).Success)
|
|
item.Name = m.Groups["name"].Value;
|
|
else // if we can't parse the file name there is a problem.
|
|
return null;
|
|
|
|
if ((m = Regex.Match(buf, "modify=(?<modify>.+?);", RegexOptions.IgnoreCase)).Success)
|
|
item.Modified = m.Groups["modify"].Value.GetFtpDate(DateTimeStyles.AssumeUniversal);
|
|
|
|
if ((m = Regex.Match(buf, "created?=(?<create>.+?);", RegexOptions.IgnoreCase)).Success)
|
|
item.Created = m.Groups["create"].Value.GetFtpDate(DateTimeStyles.AssumeUniversal);
|
|
|
|
if ((m = Regex.Match(buf, @"size=(?<size>\d+);", RegexOptions.IgnoreCase)).Success) {
|
|
long size;
|
|
|
|
if (long.TryParse(m.Groups["size"].Value, out size))
|
|
item.Size = size;
|
|
}
|
|
|
|
if ((m = Regex.Match(buf, @"unix.mode=(?<mode>\d+);", RegexOptions.IgnoreCase)).Success) {
|
|
if (m.Groups["mode"].Value.Length == 4) {
|
|
item.SpecialPermissions = (FtpSpecialPermissions)int.Parse(m.Groups["mode"].Value[0].ToString());
|
|
item.OwnerPermissions = (FtpPermission)int.Parse(m.Groups["mode"].Value[1].ToString());
|
|
item.GroupPermissions = (FtpPermission)int.Parse(m.Groups["mode"].Value[2].ToString());
|
|
item.OthersPermissions = (FtpPermission)int.Parse(m.Groups["mode"].Value[3].ToString());
|
|
CalcChmod(item);
|
|
} else if (m.Groups["mode"].Value.Length == 3) {
|
|
item.OwnerPermissions = (FtpPermission)int.Parse(m.Groups["mode"].Value[0].ToString());
|
|
item.GroupPermissions = (FtpPermission)int.Parse(m.Groups["mode"].Value[1].ToString());
|
|
item.OthersPermissions = (FtpPermission)int.Parse(m.Groups["mode"].Value[2].ToString());
|
|
CalcChmod(item);
|
|
}
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Unix Parser
|
|
|
|
private bool IsUnixValid(string[] listing) {
|
|
int count = Math.Min(listing.Length, 10);
|
|
|
|
bool perms1 = false;
|
|
bool perms2 = false;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
if (listing[i].Trim().Length == 0)
|
|
continue;
|
|
string[] fields = SplitString(listing[i]);
|
|
if (fields.Length < MIN_EXPECTED_FIELD_COUNT_UNIX)
|
|
continue;
|
|
// check perms
|
|
char ch00 = Char.ToLower(fields[0][0]);
|
|
if (ch00 == '-' || ch00 == 'l' || ch00 == 'd')
|
|
perms1 = true;
|
|
|
|
if (fields[0].Length > 1) {
|
|
char ch01 = Char.ToLower(fields[0][1]);
|
|
if (ch01 == 'r' || ch01 == '-')
|
|
perms2 = true;
|
|
}
|
|
|
|
// last chance - Connect:Enterprise has -ART------TCP
|
|
if (!perms2 && fields[0].Length > 2 && fields[0].IndexOf('-', 2) > 0)
|
|
perms2 = true;
|
|
}
|
|
if (perms1 && perms2)
|
|
return true;
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Not in UNIX format");
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses Unix format listings
|
|
/// </summary>
|
|
/// <param name="raw">A line from the listing</param>
|
|
/// <returns>FtpListItem if the item is able to be parsed</returns>
|
|
private FtpListItem ParseUnix(string raw) {
|
|
|
|
//-----------------------------------------------------
|
|
// EXAMPLES
|
|
|
|
// lrwxrwxrwx 1 wuftpd wuftpd 14 Jul 22 2002 MIRRORS -> README-MIRRORS
|
|
// -rw-r--r-- 1 b173771 users 431 Mar 31 20:04 .htaccess
|
|
//-----------------------------------------------------
|
|
|
|
// test it is a valid line, e.g. "total 342522" is invalid
|
|
char ch = raw[0];
|
|
if (ch != ORDINARY_FILE_CHAR && ch != DIRECTORY_CHAR && ch != SYMLINK_CHAR)
|
|
return null;
|
|
|
|
string[] fields = SplitString(raw);
|
|
|
|
if (fields.Length < MIN_EXPECTED_FIELD_COUNT_UNIX) {
|
|
StringBuilder msg = new StringBuilder("Unexpected number of fields in listing '");
|
|
msg.Append(raw).Append("' - expected minimum ").Append(MIN_EXPECTED_FIELD_COUNT_UNIX).
|
|
Append(" fields but found ").Append(fields.Length).Append(" fields");
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, msg.ToString());
|
|
return null;
|
|
}
|
|
|
|
// field pos
|
|
int index = 0;
|
|
|
|
// first field is perms
|
|
string permissions = fields[index++];
|
|
ch = permissions[0];
|
|
bool isDir = false;
|
|
bool isLink = false;
|
|
if (ch == DIRECTORY_CHAR)
|
|
isDir = true;
|
|
else if (ch == SYMLINK_CHAR)
|
|
isLink = true;
|
|
|
|
// some servers don't supply the link count
|
|
int linkCount = 0;
|
|
if (Char.IsDigit(fields[index][0])) // assume it is if a digit
|
|
{
|
|
string linkCountStr = fields[index++];
|
|
try {
|
|
linkCount = System.Int32.Parse(linkCountStr);
|
|
} catch (FormatException) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to parse link count: " + linkCountStr);
|
|
}
|
|
} else if (fields[index][0] == '-') // IPXOS Treck FTP server
|
|
{
|
|
index++;
|
|
}
|
|
|
|
// owner and group
|
|
string owner = "";
|
|
string group = "";
|
|
// if 2 fields ahead is numeric and there's enough fields beyond (4) for
|
|
// the date, then the next two fields should be the owner & group
|
|
if (IsNumeric(fields[index + 2]) && fields.Length - (index + 2) > 4) {
|
|
owner = fields[index++];
|
|
group = fields[index++];
|
|
}
|
|
// no owner
|
|
else if (IsNumeric(fields[index + 1]) && fields.Length - (index + 1) > 4) {
|
|
group = fields[index++];
|
|
}
|
|
|
|
// size
|
|
long size = 0L;
|
|
string sizeStr = fields[index++].Replace(".", ""); // get rid of .'s in size
|
|
try {
|
|
size = Int64.Parse(sizeStr);
|
|
} catch (FormatException) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to parse size: " + sizeStr);
|
|
}
|
|
|
|
// next 3 fields are the date time
|
|
|
|
// we expect the month first on Unix.
|
|
// Connect:Enterprise UNIX has a weird extra numeric field here - we test if the
|
|
// next field is numeric and if so, we skip it (except we check for a BSD variant
|
|
// that means it is the day of the month)
|
|
int dayOfMonth = -1;
|
|
if (IsNumeric(fields[index])) {
|
|
// this just might be the day of month - BSD variant
|
|
// we check it is <= 31 AND that the next field starts
|
|
// with a letter AND the next has a ':' within it
|
|
try {
|
|
char[] chars = { '0' };
|
|
string str = fields[index].TrimStart(chars);
|
|
dayOfMonth = Int32.Parse(fields[index]);
|
|
if (dayOfMonth > 31) // can't be day of month
|
|
dayOfMonth = -1;
|
|
if (!(Char.IsLetter(fields[index + 1][0])))
|
|
dayOfMonth = -1;
|
|
if (fields[index + 2].IndexOf(':') <= 0)
|
|
dayOfMonth = -1;
|
|
} catch (FormatException) { }
|
|
index++;
|
|
}
|
|
|
|
int dateTimePos = index;
|
|
DateTime lastModified = DateTime.MinValue;
|
|
StringBuilder stamp = new StringBuilder(fields[index++]);
|
|
stamp.Append('-');
|
|
if (dayOfMonth > 0)
|
|
stamp.Append(dayOfMonth);
|
|
else
|
|
stamp.Append(fields[index++]);
|
|
stamp.Append('-');
|
|
|
|
string field = fields[index++];
|
|
if (field.IndexOf((System.Char)':') < 0 && field.IndexOf((System.Char)'.') < 0) {
|
|
stamp.Append(field); // year
|
|
try {
|
|
lastModified = DateTime.ParseExact(stamp.ToString(), unixDateFormats1,
|
|
parserCulture.DateTimeFormat, DateTimeStyles.None);
|
|
} catch (FormatException) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to parse date string '" + stamp.ToString() + "'");
|
|
}
|
|
} else {
|
|
// add the year ourselves as not present
|
|
int year = parserCulture.Calendar.GetYear(DateTime.Now);
|
|
stamp.Append(year).Append('-').Append(field);
|
|
try {
|
|
|
|
lastModified = DateTime.ParseExact(stamp.ToString(), unixDateFormats2,
|
|
parserCulture.DateTimeFormat, DateTimeStyles.None);
|
|
} catch (FormatException) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to parse date string '" + stamp.ToString() + "'");
|
|
}
|
|
|
|
// can't be in the future - must be the previous year
|
|
// add 2 days for time zones (thanks hgfischer)
|
|
if (lastModified > DateTime.Now.AddDays(2)) {
|
|
lastModified = lastModified.AddYears(-1);
|
|
}
|
|
}
|
|
|
|
// name of file or dir. Extract symlink if possible
|
|
string name = null;
|
|
string linkedname = null;
|
|
|
|
// we've got to find the starting point of the name. We
|
|
// do this by finding the pos of all the date/time fields, then
|
|
// the name - to ensure we don't get tricked up by a userid the
|
|
// same as the filename,for example
|
|
int pos = 0;
|
|
bool ok = true;
|
|
int dateFieldCount = dayOfMonth > 0 ? 2 : 3; // only 2 fields left if we had a leading day of month
|
|
for (int i = dateTimePos; i < dateTimePos + dateFieldCount; i++) {
|
|
pos = raw.IndexOf(fields[i], pos);
|
|
if (pos < 0) {
|
|
ok = false;
|
|
break;
|
|
} else {
|
|
pos += fields[i].Length;
|
|
}
|
|
}
|
|
if (ok) {
|
|
string remainder = raw.Substring(pos).Trim();
|
|
if (!isLink)
|
|
name = remainder;
|
|
else {
|
|
// symlink, try to extract it
|
|
pos = remainder.IndexOf(SYMLINK_ARROW);
|
|
if (pos <= 0) {
|
|
// couldn't find symlink, give up & just assign as name
|
|
name = remainder;
|
|
} else {
|
|
int len = SYMLINK_ARROW.Length;
|
|
name = remainder.Substring(0, (pos) - (0)).Trim();
|
|
if (pos + len < remainder.Length)
|
|
linkedname = remainder.Substring(pos + len);
|
|
}
|
|
}
|
|
} else {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to retrieve name: " + raw);
|
|
}
|
|
|
|
FtpListItem file = new FtpListItem(raw, name, size, isDir, ref lastModified);
|
|
if (isLink) {
|
|
file.Type = FtpFileSystemObjectType.Link;
|
|
file.LinkCount = linkCount;
|
|
file.LinkTarget = linkedname.Trim();
|
|
}
|
|
file.RawGroup = group;
|
|
file.RawOwner = owner;
|
|
file.RawPermissions = permissions;
|
|
CalcUnixPermissions(file, permissions);
|
|
return file;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses Unix format listings with alternate parser
|
|
/// </summary>
|
|
/// <param name="raw">A line from the listing</param>
|
|
/// <returns>FtpListItem if the item is able to be parsed</returns>
|
|
private FtpListItem ParseUnixAlt(string raw) {
|
|
|
|
//-----------------------------------------------------
|
|
// EXAMPLES
|
|
|
|
// -r-------- GMETECHNOLOGY 1 TSI 8 Nov 06 11:00:25 ,GMETECHNOLOGY,file02.csv,U,20071106A00001105190.txt
|
|
//-----------------------------------------------------
|
|
|
|
// test it is a valid line, e.g. "total 342522" is invalid
|
|
char ch = raw[0];
|
|
if (ch != ORDINARY_FILE_CHAR && ch != DIRECTORY_CHAR && ch != SYMLINK_CHAR)
|
|
return null;
|
|
|
|
string[] fields = SplitString(raw);
|
|
|
|
if (fields.Length < MIN_EXPECTED_FIELD_COUNT_UNIXALT) {
|
|
StringBuilder listing = new StringBuilder("Unexpected number of fields in listing '");
|
|
listing.Append(raw).Append("' - expected minimum ").Append(MIN_EXPECTED_FIELD_COUNT_UNIXALT).
|
|
Append(" fields but found ").Append(fields.Length).Append(" fields");
|
|
throw new FormatException(listing.ToString());
|
|
}
|
|
|
|
// field pos
|
|
int index = 0;
|
|
|
|
// first field is perms
|
|
string permissions = fields[index++];
|
|
ch = permissions[0];
|
|
bool isDir = false;
|
|
bool isLink = false;
|
|
if (ch == DIRECTORY_CHAR)
|
|
isDir = true;
|
|
else if (ch == SYMLINK_CHAR)
|
|
isLink = true;
|
|
|
|
string group = fields[index++];
|
|
|
|
// some servers don't supply the link count
|
|
int linkCount = 0;
|
|
if (Char.IsDigit(fields[index][0])) // assume it is if a digit
|
|
{
|
|
string linkCountStr = fields[index++];
|
|
try {
|
|
linkCount = System.Int32.Parse(linkCountStr);
|
|
} catch (FormatException) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to parse link count: " + linkCountStr);
|
|
}
|
|
}
|
|
|
|
string owner = fields[index++];
|
|
|
|
|
|
// size
|
|
long size = 0L;
|
|
string sizeStr = fields[index++];
|
|
try {
|
|
size = Int64.Parse(sizeStr);
|
|
} catch (FormatException) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to parse size: " + sizeStr);
|
|
}
|
|
|
|
// next 3 fields are the date time
|
|
|
|
// we expect the month first on Unix.
|
|
int dateTimePos = index;
|
|
DateTime lastModified = DateTime.MinValue;
|
|
StringBuilder stamp = new StringBuilder(fields[index++]);
|
|
stamp.Append('-').Append(fields[index++]).Append('-');
|
|
|
|
string field = fields[index++];
|
|
if (field.IndexOf((System.Char)':') < 0) {
|
|
stamp.Append(field); // year
|
|
try {
|
|
lastModified = DateTime.ParseExact(stamp.ToString(), unixAltDateFormats1,
|
|
parserCulture.DateTimeFormat, DateTimeStyles.None);
|
|
} catch (FormatException) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to parse date string '" + stamp.ToString() + "'");
|
|
}
|
|
} else {
|
|
// add the year ourselves as not present
|
|
int year = parserCulture.Calendar.GetYear(DateTime.Now);
|
|
stamp.Append(year).Append('-').Append(field);
|
|
try {
|
|
|
|
lastModified = DateTime.ParseExact(stamp.ToString(), unixAltDateFormats2,
|
|
parserCulture.DateTimeFormat, DateTimeStyles.None);
|
|
} catch (FormatException) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to parse date string '" + stamp.ToString() + "'");
|
|
}
|
|
|
|
// can't be in the future - must be the previous year
|
|
// add 2 days for time zones (thanks hgfischer)
|
|
if (lastModified > DateTime.Now.AddDays(2)) {
|
|
lastModified = lastModified.AddYears(-1);
|
|
}
|
|
}
|
|
|
|
// name of file or dir. Extract symlink if possible
|
|
string name = null;
|
|
|
|
// we've got to find the starting point of the name. We
|
|
// do this by finding the pos of all the date/time fields, then
|
|
// the name - to ensure we don't get tricked up by a userid the
|
|
// same as the filename,for example
|
|
int pos = 0;
|
|
bool ok = true;
|
|
for (int i = dateTimePos; i < dateTimePos + 3; i++) {
|
|
pos = raw.IndexOf(fields[i], pos);
|
|
if (pos < 0) {
|
|
ok = false;
|
|
break;
|
|
} else {
|
|
pos += fields[i].Length;
|
|
}
|
|
}
|
|
if (ok) {
|
|
name = raw.Substring(pos).Trim();
|
|
} else {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to retrieve name: " + raw);
|
|
}
|
|
|
|
FtpListItem file = new FtpListItem(raw, name, size, isDir, ref lastModified);
|
|
if (isLink) {
|
|
file.Type = FtpFileSystemObjectType.Link;
|
|
file.LinkCount = linkCount;
|
|
}
|
|
file.RawGroup = group;
|
|
file.RawOwner = owner;
|
|
file.RawPermissions = permissions;
|
|
CalcUnixPermissions(file, permissions);
|
|
return file;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Windows Parser
|
|
|
|
private bool IsWindowsValid(string[] listing) {
|
|
int count = Math.Min(listing.Length, 10);
|
|
|
|
bool dateStart = false;
|
|
bool timeColon = false;
|
|
bool dirOrFile = false;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
if (listing[i].Trim().Length == 0)
|
|
continue;
|
|
string[] fields = SplitString(listing[i]);
|
|
if (fields.Length < MIN_EXPECTED_FIELD_COUNT_WIN)
|
|
continue;
|
|
// first & last chars are digits of first field
|
|
if (Char.IsDigit(fields[0][0]) && Char.IsDigit(fields[0][fields[0].Length - 1]))
|
|
dateStart = true;
|
|
if (fields[1].IndexOf(':') > 0)
|
|
timeColon = true;
|
|
if (fields[2].ToUpper() == WIN_DIR || Char.IsDigit(fields[2][0]))
|
|
dirOrFile = true;
|
|
}
|
|
if (dateStart && timeColon && dirOrFile)
|
|
return true;
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Not in Windows format");
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses IIS/DOS format listings
|
|
/// </summary>
|
|
/// <param name="raw">A line from the listing</param>
|
|
/// <returns>FtpListItem if the item is able to be parsed</returns>
|
|
private FtpListItem ParseWindows(string raw) {
|
|
//-----------------------------------------------------
|
|
// EXAMPLES
|
|
|
|
// 05-17-03 02:47PM 70776 ftp.jar
|
|
// 08-28-03 10:08PM <DIR> EDT SSLTest
|
|
//-----------------------------------------------------
|
|
|
|
string[] fields = SplitString(raw);
|
|
|
|
if (fields.Length < MIN_EXPECTED_FIELD_COUNT_WIN)
|
|
return null;
|
|
|
|
// first two fields are date time
|
|
string lastModifiedStr = fields[0] + " " + fields[1];
|
|
DateTime lastModified = DateTime.MinValue;
|
|
try {
|
|
lastModified = DateTime.ParseExact(lastModifiedStr, windowsDateFormats,
|
|
parserCulture.DateTimeFormat, DateTimeStyles.None);
|
|
} catch (FormatException) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to parse date string '" + lastModifiedStr + "'");
|
|
}
|
|
|
|
// dir flag
|
|
bool isDir = false;
|
|
long size = 0L;
|
|
if (fields[2].ToUpper().Equals(WIN_DIR.ToUpper()))
|
|
isDir = true;
|
|
else {
|
|
try {
|
|
size = Int64.Parse(fields[2]);
|
|
} catch (FormatException) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to parse size: " + fields[2]);
|
|
}
|
|
}
|
|
|
|
// we've got to find the starting point of the name. We
|
|
// do this by finding the pos of all the date/time fields, then
|
|
// the name - to ensure we don't get tricked up by a date or dir the
|
|
// same as the filename, for example
|
|
int pos = 0;
|
|
bool ok = true;
|
|
for (int i = 0; i < 3; i++) {
|
|
pos = raw.IndexOf(fields[i], pos);
|
|
if (pos < 0) {
|
|
ok = false;
|
|
break;
|
|
} else {
|
|
pos += fields[i].Length;
|
|
}
|
|
}
|
|
string name = null;
|
|
if (ok) {
|
|
name = raw.Substring(pos).Trim();
|
|
} else {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to retrieve name: " + raw);
|
|
}
|
|
return new FtpListItem(raw, name, size, isDir, ref lastModified);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region VMS Parser
|
|
|
|
private bool IsVMSValid(String[] listing) {
|
|
int count = Math.Min(listing.Length, 10);
|
|
|
|
bool semiColonName = false;
|
|
bool squareBracketStart = false, squareBracketEnd = false;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
if (listing[i].Trim().Length == 0)
|
|
continue;
|
|
int pos = 0;
|
|
if ((pos = listing[i].IndexOf(';')) > 0 && (++pos < listing[i].Length) &&
|
|
Char.IsDigit(listing[i][pos]))
|
|
semiColonName = true;
|
|
if (listing[i].IndexOf('[') > 0)
|
|
squareBracketStart = true;
|
|
if (listing[i].IndexOf(']') > 0)
|
|
squareBracketEnd = true;
|
|
}
|
|
if (semiColonName && squareBracketStart && squareBracketEnd)
|
|
return true;
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Not in VMS format");
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses Vax/VMS format listings
|
|
/// </summary>
|
|
/// <param name="raw">A line from the listing</param>
|
|
/// <returns>FtpListItem if the item is able to be parsed</returns>
|
|
private FtpListItem ParseVMS(string raw) {
|
|
|
|
//-----------------------------------------------------
|
|
// EXAMPLES
|
|
//
|
|
// Directory dirname
|
|
// filename;version used/allocated dd-MMM-yyyy HH:mm:ss [group,owner] (PERMS)
|
|
// ...
|
|
//
|
|
// Total of n files, n/m blocks
|
|
//-----------------------------------------------------
|
|
|
|
string[] fields = SplitString(raw);
|
|
|
|
// skip blank lines
|
|
if (fields.Length <= 0)
|
|
return null;
|
|
// skip line which lists Directory
|
|
if (fields.Length >= 2 && fields[0].Equals(VMS_HDR))
|
|
return null;
|
|
// skip line which lists Total
|
|
if (fields.Length > 0 && fields[0].Equals(VMS_TOTAL))
|
|
return null;
|
|
if (fields.Length < MIN_EXPECTED_FIELD_COUNT_VMS)
|
|
return null;
|
|
|
|
// first field is name
|
|
string name = fields[0];
|
|
|
|
// make sure it is the name (ends with ';<INT>')
|
|
int semiPos = name.LastIndexOf(';');
|
|
// check for ;
|
|
if (semiPos <= 0) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "File version number not found in name '" + name + "'");
|
|
return null;
|
|
}
|
|
|
|
string nameNoVersion = name.Substring(0, semiPos);
|
|
|
|
// check for version after ;
|
|
string afterSemi = fields[0].Substring(semiPos + 1);
|
|
try {
|
|
Int64.Parse(afterSemi);
|
|
// didn't throw exception yet, must be number
|
|
// we don't use it currently but we might in future
|
|
} catch (FormatException) {
|
|
// don't worry about version number
|
|
}
|
|
|
|
// test is dir
|
|
bool isDir = false;
|
|
if (nameNoVersion.EndsWith(VMS_DIR)) {
|
|
isDir = true;
|
|
name = nameNoVersion.Substring(0, nameNoVersion.Length - VMS_DIR.Length);
|
|
}
|
|
|
|
if (!vmsNameHasVersion && !isDir) {
|
|
name = nameNoVersion;
|
|
}
|
|
|
|
// 2nd field is size USED/ALLOCATED format, or perhaps just USED
|
|
int slashPos = fields[1].IndexOf('/');
|
|
string sizeUsed = fields[1];
|
|
long size = 0;
|
|
if (slashPos == -1) {
|
|
|
|
// only filesize in bytes
|
|
size = Int64.Parse(fields[1]);
|
|
|
|
}else{
|
|
if (slashPos > 0)
|
|
sizeUsed = fields[1].Substring(0, slashPos);
|
|
size = Int64.Parse(sizeUsed) * vmsBlocksize;
|
|
}
|
|
|
|
// 3 & 4 fields are date time
|
|
string lastModifiedStr = FixDateVMS(fields);
|
|
DateTime lastModified = DateTime.MinValue;
|
|
try {
|
|
lastModified = DateTime.Parse(lastModifiedStr.ToString(), parserCulture.DateTimeFormat);
|
|
} catch (FormatException) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to parse date string '" + lastModifiedStr + "'");
|
|
}
|
|
|
|
// 5th field is [group,owner]
|
|
string group = null;
|
|
string owner = null;
|
|
if (fields.Length >= 5) {
|
|
if (fields[4][0] == '[' && fields[4][fields[4].Length - 1] == ']') {
|
|
int commaPos = fields[4].IndexOf(',');
|
|
if (commaPos < 0) {
|
|
owner = fields[4].Substring(1, fields[4].Length - 2);
|
|
group = "";
|
|
} else {
|
|
group = fields[4].Substring(1, commaPos - 1);
|
|
owner = fields[4].Substring(commaPos + 1, fields[4].Length - commaPos - 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 6th field is permissions e.g. (RWED,RWED,RE,)
|
|
string permissions = null;
|
|
if (fields.Length >= 6) {
|
|
if (fields[5][0] == '(' && fields[5][fields[5].Length - 1] == ')') {
|
|
permissions = fields[5].Substring(1, fields[5].Length - 2);
|
|
}
|
|
}
|
|
|
|
FtpListItem file = new FtpListItem(raw, name, size, isDir, ref lastModified);
|
|
file.RawGroup = group;
|
|
file.RawOwner = owner;
|
|
file.RawPermissions = permissions;
|
|
return file;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region NonStop Parser
|
|
|
|
private bool IsNonstopValid(string[] listing) {
|
|
return IsNonstopHeader(listing[0]);
|
|
}
|
|
|
|
private bool IsNonstopHeader(string line) {
|
|
if (line.IndexOf("Code") > 0 && line.IndexOf("EOF") > 0 &&
|
|
line.IndexOf("RWEP") > 0)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses NonStop format listings
|
|
/// </summary>
|
|
/// <param name="raw">A line from the listing</param>
|
|
/// <returns>FtpListItem if the item is able to be parsed</returns>
|
|
private FtpListItem ParseNonstop(string raw) {
|
|
//-----------------------------------------------------
|
|
// EXAMPLES
|
|
|
|
// File Code EOF Last Modification Owner RWEP
|
|
// IARPTS 101 16354 18-Mar-08 15:09:12 244, 10 "nnnn"
|
|
// JENNYCB2 101 16384 10-Jul-08 11:44:56 244, 10 "nnnn"
|
|
//-----------------------------------------------------
|
|
|
|
if (IsNonstopHeader(raw))
|
|
return null;
|
|
|
|
string[] fields = SplitString(raw);
|
|
|
|
if (fields.Length < MIN_EXPECTED_FIELD_COUNT_TANDEM)
|
|
return null;
|
|
|
|
string name = fields[0];
|
|
|
|
// first two fields are date time
|
|
string lastModifiedStr = fields[3] + " " + fields[4];
|
|
DateTime lastModified = DateTime.MinValue;
|
|
try {
|
|
lastModified = DateTime.ParseExact(lastModifiedStr, nonstopDateFormats,
|
|
parserCulture.DateTimeFormat, DateTimeStyles.None);
|
|
} catch (FormatException) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to parse date string '" + lastModifiedStr + "'");
|
|
}
|
|
|
|
// dir flag
|
|
bool isDir = false;
|
|
long size = 0L;
|
|
try {
|
|
size = Int64.Parse(fields[2]);
|
|
} catch (FormatException) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to parse size: " + fields[2]);
|
|
}
|
|
|
|
string owner = fields[5] + fields[6];
|
|
string permissions = fields[7].Trim(NONSTOP_TRIM);
|
|
|
|
FtpListItem file = new FtpListItem(raw, name, size, isDir, ref lastModified);
|
|
file.RawOwner = owner;
|
|
file.RawPermissions = permissions;
|
|
return file;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IBM Parser
|
|
|
|
private bool IsIBMValid(String[] listing) {
|
|
int count = Math.Min(listing.Length, 10);
|
|
|
|
bool dir = false;
|
|
bool ddir = false;
|
|
bool lib = false;
|
|
bool stmf = false;
|
|
bool flr = false;
|
|
bool file = false;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
if (listing[i].IndexOf("*DIR") > 0)
|
|
dir = true;
|
|
else if (listing[i].IndexOf("*FILE") > 0)
|
|
file = true;
|
|
else if (listing[i].IndexOf("*FLR") > 0)
|
|
flr = true;
|
|
else if (listing[i].IndexOf("*DDIR") > 0)
|
|
ddir = true;
|
|
else if (listing[i].IndexOf("*STMF") > 0)
|
|
stmf = true;
|
|
else if (listing[i].IndexOf("*LIB") > 0)
|
|
lib = true;
|
|
}
|
|
if (dir || file || ddir || lib || stmf || flr)
|
|
return true;
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Not in OS/400 format");
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses IBM OS/400 format listings
|
|
/// </summary>
|
|
/// <param name="raw">A line from the listing</param>
|
|
/// <returns>FtpListItem if the item is able to be parsed</returns>
|
|
private FtpListItem ParseIBM(string raw) {
|
|
|
|
//-----------------------------------------------------
|
|
// EXAMPLES
|
|
//
|
|
// In a library:
|
|
// CFT 45056 04/12/06 14:19:31 *FILE AFTFRE1.FILE
|
|
// CFT *MEM AFTFRE1.FILE/AFTFRE1.MBR
|
|
// CFT 36864 28/11/06 15:19:30 *FILE AFTFRE2.FILE
|
|
// CFT *MEM AFTFRE2.FILE/AFTFRE2.MBR
|
|
// CFT 45056 04/12/06 14:19:37 *FILE AFTFRE6.FILE
|
|
// CFT *MEM AFTFRE6.FILE/AFTFRE6.MBR
|
|
// QSYSOPR 28672 01/12/06 20:08:04 *FILE FPKI45POK5.FILE
|
|
// QSYSOPR *MEM FPKI45POK5.FILE/FPKI45POK5.MBR
|
|
//
|
|
// Inside a file:
|
|
// DEREK 76128 07/11/17 14:25:46 *FILE
|
|
// DEREK *MEM AAR.MBR
|
|
// DEREK *MEM AAS.MBR
|
|
//-----------------------------------------------------
|
|
|
|
string[] fields = SplitString(raw);
|
|
|
|
// skip blank lines
|
|
if (fields.Length <= 0)
|
|
return null;
|
|
// return what we can for MEM
|
|
if (fields.Length >= 2 && fields[1].Equals(IBM_MEM)) {
|
|
DateTime lastModifiedm = DateTime.MinValue;
|
|
string ownerm = fields[0];
|
|
string namem = fields[2];
|
|
FtpListItem filem = new FtpListItem(raw, namem, 0, false, ref lastModifiedm);
|
|
filem.RawOwner = ownerm;
|
|
return filem;
|
|
}
|
|
if (fields.Length < MIN_EXPECTED_FIELD_COUNT_OS400)
|
|
return null;
|
|
|
|
// first field is owner
|
|
string owner = fields[0];
|
|
|
|
// next is size
|
|
long size = Int64.Parse(fields[1]);
|
|
|
|
string lastModifiedStr = fields[2] + " " + fields[3];
|
|
DateTime lastModified = GetLastModifiedIBM(lastModifiedStr);
|
|
|
|
// test is dir
|
|
bool isDir = false;
|
|
if (fields[4] == IBM_DIR || fields[4] == IBM_DDIR || (fields.Length == 5 && fields[4] == IBM_FILE))
|
|
isDir = true;
|
|
|
|
// If there's no name, it's because we're inside a file. Fake out a "current directory" name instead.
|
|
string name = fields.Length >= 6
|
|
? fields[5]
|
|
: ".";
|
|
if (name.EndsWith("/")) {
|
|
isDir = true;
|
|
name = name.Substring(0, name.Length - 1);
|
|
}
|
|
|
|
FtpListItem file = new FtpListItem(raw, name, size, isDir, ref lastModified);
|
|
file.RawOwner = owner;
|
|
return file;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Utils
|
|
|
|
/// <summary>
|
|
/// Split into fields by splitting on strings
|
|
/// </summary>
|
|
private static string[] SplitString(string str) {
|
|
List<string> allTokens = new List<string>(str.Split(null));
|
|
for (int i = allTokens.Count - 1; i >= 0; i--)
|
|
if (((string)allTokens[i]).Trim().Length == 0)
|
|
allTokens.RemoveAt(i);
|
|
return (string[])allTokens.ToArray();
|
|
}
|
|
|
|
private int formatIndex = 0;
|
|
|
|
private static void CalcFullPaths(FtpListItem item, string path, bool isVMS) {
|
|
|
|
|
|
// EXIT IF NO DIR PATH PROVIDED
|
|
if (path == null) {
|
|
|
|
// check if the path is absolute
|
|
if (IsAbsolutePath(item.Name)) {
|
|
item.FullName = item.Name;
|
|
item.Name = item.Name.GetFtpFileName();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
// ONLY IF DIR PATH PROVIDED
|
|
|
|
// if this is a vax/openvms file listing
|
|
// there are no slashes in the path name
|
|
if (isVMS)
|
|
item.FullName = path + item.Name;
|
|
else {
|
|
//FtpTrace.WriteStatus(item.Name);
|
|
|
|
// remove globbing/wildcard from path
|
|
if (path.GetFtpFileName().Contains("*")) {
|
|
path = path.GetFtpDirectoryName();
|
|
}
|
|
|
|
if (item.Name != null) {
|
|
// absolute path? then ignore the path input to this method.
|
|
if (IsAbsolutePath(item.Name)) {
|
|
item.FullName = item.Name;
|
|
item.Name = item.Name.GetFtpFileName();
|
|
} else if (path != null) {
|
|
item.FullName = path.GetFtpPath(item.Name); //.GetFtpPathWithoutGlob();
|
|
} else {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Warn, "Couldn't determine the full path of this object: " +
|
|
Environment.NewLine + item.ToString());
|
|
}
|
|
}
|
|
|
|
|
|
// if a link target is set and it doesn't include an absolute path
|
|
// then try to resolve it.
|
|
if (item.LinkTarget != null && !item.LinkTarget.StartsWith("/")) {
|
|
if (item.LinkTarget.StartsWith("./"))
|
|
item.LinkTarget = path.GetFtpPath(item.LinkTarget.Remove(0, 2)).Trim();
|
|
else
|
|
item.LinkTarget = path.GetFtpPath(item.LinkTarget).Trim();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool IsAbsolutePath(string path) {
|
|
return path.StartsWith("/") || path.StartsWith("./") || path.StartsWith("../");
|
|
}
|
|
|
|
private static void CalcChmod(FtpListItem item) {
|
|
item.Chmod = FtpClient.CalcChmod(item.OwnerPermissions, item.GroupPermissions, item.OthersPermissions);
|
|
}
|
|
|
|
private static void CalcUnixPermissions(FtpListItem item, string permissions) {
|
|
Match perms = Regex.Match(permissions,
|
|
@"[\w-]{1}(?<owner>[\w-]{3})(?<group>[\w-]{3})(?<others>[\w-]{3})",
|
|
RegexOptions.IgnoreCase);
|
|
|
|
if (perms.Success) {
|
|
|
|
if (perms.Groups["owner"].Value.Length == 3) {
|
|
if (perms.Groups["owner"].Value[0] == 'r') {
|
|
item.OwnerPermissions |= FtpPermission.Read;
|
|
}
|
|
if (perms.Groups["owner"].Value[1] == 'w') {
|
|
item.OwnerPermissions |= FtpPermission.Write;
|
|
}
|
|
if (perms.Groups["owner"].Value[2] == 'x' || perms.Groups["owner"].Value[2] == 's') {
|
|
item.OwnerPermissions |= FtpPermission.Execute;
|
|
}
|
|
if (perms.Groups["owner"].Value[2] == 's' || perms.Groups["owner"].Value[2] == 'S') {
|
|
item.SpecialPermissions |= FtpSpecialPermissions.SetUserID;
|
|
}
|
|
}
|
|
|
|
if (perms.Groups["group"].Value.Length == 3) {
|
|
if (perms.Groups["group"].Value[0] == 'r') {
|
|
item.GroupPermissions |= FtpPermission.Read;
|
|
}
|
|
if (perms.Groups["group"].Value[1] == 'w') {
|
|
item.GroupPermissions |= FtpPermission.Write;
|
|
}
|
|
if (perms.Groups["group"].Value[2] == 'x' || perms.Groups["group"].Value[2] == 's') {
|
|
item.GroupPermissions |= FtpPermission.Execute;
|
|
}
|
|
if (perms.Groups["group"].Value[2] == 's' || perms.Groups["group"].Value[2] == 'S') {
|
|
item.SpecialPermissions |= FtpSpecialPermissions.SetGroupID;
|
|
}
|
|
}
|
|
|
|
if (perms.Groups["others"].Value.Length == 3) {
|
|
if (perms.Groups["others"].Value[0] == 'r') {
|
|
item.OthersPermissions |= FtpPermission.Read;
|
|
}
|
|
if (perms.Groups["others"].Value[1] == 'w') {
|
|
item.OthersPermissions |= FtpPermission.Write;
|
|
}
|
|
if (perms.Groups["others"].Value[2] == 'x' || perms.Groups["others"].Value[2] == 't') {
|
|
item.OthersPermissions |= FtpPermission.Execute;
|
|
}
|
|
if (perms.Groups["others"].Value[2] == 't' || perms.Groups["others"].Value[2] == 'T') {
|
|
item.SpecialPermissions |= FtpSpecialPermissions.Sticky;
|
|
}
|
|
}
|
|
|
|
CalcChmod(item);
|
|
}
|
|
}
|
|
|
|
|
|
// OS-SPECIFIC PARSERS
|
|
|
|
private static bool IsUnixListing(string raw) {
|
|
char ch = raw[0];
|
|
if (ch == ORDINARY_FILE_CHAR || ch == DIRECTORY_CHAR || ch == SYMLINK_CHAR)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
private static bool IsNumeric(string field) {
|
|
field = field.Replace(".", ""); // strip dots
|
|
for (int i = 0; i < field.Length; i++) {
|
|
if (!Char.IsDigit(field[i]))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private DateTime GetLastModifiedIBM(string lastModifiedStr) {
|
|
DateTime lastModified = DateTime.MinValue;
|
|
if (formatIndex >= ibmDateFormats.Length) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Warn, "Exhausted formats - failed to parse date");
|
|
return DateTime.MinValue;
|
|
}
|
|
int prevIndex = formatIndex;
|
|
for (int i = formatIndex; i < ibmDateFormats.Length; i++, formatIndex++) {
|
|
try {
|
|
lastModified = DateTime.ParseExact(lastModifiedStr, ibmDateFormats[formatIndex],
|
|
parserCulture.DateTimeFormat, DateTimeStyles.None);
|
|
if (lastModified > DateTime.Now.AddDays(2)) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Swapping to alternate format (found date in future)");
|
|
continue;
|
|
} else // all ok, exit loop
|
|
break;
|
|
} catch (FormatException) {
|
|
continue;
|
|
}
|
|
}
|
|
if (formatIndex >= ibmDateFormats.Length) {
|
|
FtpTrace.WriteStatus(FtpTraceLevel.Warn, "Exhausted formats - failed to parse date");
|
|
return DateTime.MinValue;
|
|
}
|
|
if (formatIndex > prevIndex) // we've changed formatters so redo
|
|
{
|
|
throw new CriticalListParseException();
|
|
}
|
|
return lastModified;
|
|
}
|
|
|
|
/// <summary> Fix the date string to make the month camel case</summary>
|
|
/// <param name="fields">array of fields</param>
|
|
private string FixDateVMS(string[] fields) {
|
|
// convert the last 2 chars of month to lower case
|
|
StringBuilder lastModifiedStr = new StringBuilder();
|
|
bool monthFound = false;
|
|
for (int i = 0; i < fields[2].Length; i++) {
|
|
if (!Char.IsLetter(fields[2][i])) {
|
|
lastModifiedStr.Append(fields[2][i]);
|
|
} else {
|
|
if (!monthFound) {
|
|
lastModifiedStr.Append(fields[2][i]);
|
|
monthFound = true;
|
|
} else {
|
|
lastModifiedStr.Append(Char.ToLower(fields[2][i]));
|
|
}
|
|
}
|
|
}
|
|
lastModifiedStr.Append(" ").Append(fields[3]);
|
|
return lastModifiedStr.ToString();
|
|
}
|
|
|
|
internal class CriticalListParseException : Exception {
|
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
}
|
|
} |