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 {
///
/// 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.
///
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 = "
";
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
///
/// the FTP connection that owns this parser
///
public FtpClient client;
private static List parsers = new List{
FtpParser.Unix, FtpParser.Windows, FtpParser.IBM, FtpParser.VMS, FtpParser.NonStop
};
///
/// which server type? (SYST)
///
public string system;
///
/// current parser, or parser set by user
///
public FtpParser parser = FtpParser.Auto;
///
/// parser calculated based on system type (SYST command)
///
public FtpParser detectedParser = FtpParser.Auto;
///
/// if we have detected that the current parser is valid
///
public bool parserConfirmed = false;
///
/// which culture to read filenames with?
///
public CultureInfo parserCulture = CultureInfo.InvariantCulture;
///
/// what is the time offset between server/client?
///
public TimeSpan timeOffset = new TimeSpan();
///
/// any time offset between server/client?
///
public bool hasTimeOffset = false;
///
/// VMS ONLY : the blocksize used to calculate the file
///
public int vmsBlocksize = DEFAULT_BLOCKSIZE;
///
/// 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.
///
public bool vmsNameHasVersion = false;
///
/// Initializes a new instance of the class.
///
/// An existing object
public FtpListParser(FtpClient client = null) {
this.client = client;
}
///
/// Try to auto-detect which parser is suitable given a system string.
///
/// result of SYST command
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;
}
///
/// Parse raw file list from server into file objects, using the currently active parser.
///
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;
}
///
/// Parse raw file from server into a file object, using the currently active parser.
///
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;
}
///
/// Validate if the current parser is correct, or if another parser seems more appropriate.
///
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);
}
///
/// Validate if the current parser is correct
///
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
///
/// Parses a line from a file listing using the first successful match in the Parsers collection.
///
/// The source path of the file listing
/// A line from the file listing
/// Server capabilities
/// 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.
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;
}
///
/// Used for synchronizing access to the Parsers collection
///
private static Object m_parserLock = new Object();
///
/// Initializes the default list of parsers
///
private static void InitParsers() {
lock (m_parserLock) {
if (m_parsers == null) {
m_parsers = new List();
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 m_parsers = null;
///
/// 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.
///
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;
///
/// Adds a custom parser
///
/// The parser delegate to add
///
public static void AddParser(Parser parser) {
lock (m_parserLock) {
if (m_parsers == null)
InitParsers();
m_parsers.Add(parser);
m_customParser = parser;
}
}
///
/// Removes all parser delegates
///
public static void ClearParsers() {
lock (m_parserLock) {
if (m_parsers == null)
InitParsers();
m_parsers.Clear();
}
}
///
/// Removes the specified parser
///
/// The parser delegate to remove
public static void RemoveParser(Parser parser) {
lock (m_parserLock) {
if (m_parsers == null)
InitParsers();
m_parsers.Remove(parser);
}
}
///
/// Parses LIST format listings
///
/// A line from the listing
/// Server capabilities
/// FtpListItem if the item is able to be parsed
private static FtpListItem ParseUnixList(string buf, FtpCapability capabilities) {
string regex =
@"(?.+)\s+" +
@"(?\d+)\s+" +
@"(?.+)\s+" +
@"(?.+)\s+" +
@"(?\d+)\s+" +
@"(?\w+\s+\d+\s+\d+:\d+|\w+\s+\d+\s+\d+)\s" +
@"(?.*)$";
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;
}
///
/// Parses IIS DOS format listings
///
/// A line from the listing
/// Server capabilities
/// FtpListItem if the item is able to be parsed
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, @"(?\d+-\d+-\d+\s+\d+:\d+\w+)\s+\s+(?.*)$", 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, @"(?\d+-\d+-\d+\s+\d+:\d+\w+)\s+(?\d+)\s+(?.*)$", 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 =
@"(?.+)\.(?.+);(?\d+)\s+" +
@"(?\d+)\s+" +
@"(?\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;
}
///
/// Ftp listing line parser
///
/// The line from the listing
/// The server capabilities
/// FtpListItem if the line can be parsed, null otherwise
public delegate FtpListItem Parser(string line, FtpCapability capabilities);
#endregion
#region Machine Listing Parser
///
/// Parses MLSD/MLST format listings
///
/// A line from the listing
/// Server capabilities
/// FtpListItem if the item is able to be parsed
private static FtpListItem ParseMachineList(string buf, FtpCapability capabilities) {
FtpListItem item = new FtpListItem();
Match m;
if (!(m = Regex.Match(buf, "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, "; (?.*)$", 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=(?.+?);", RegexOptions.IgnoreCase)).Success)
item.Modified = m.Groups["modify"].Value.GetFtpDate(DateTimeStyles.AssumeUniversal);
if ((m = Regex.Match(buf, "created?=(?.+?);", RegexOptions.IgnoreCase)).Success)
item.Created = m.Groups["create"].Value.GetFtpDate(DateTimeStyles.AssumeUniversal);
if ((m = Regex.Match(buf, @"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=(?\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;
}
///
/// Parses Unix format listings
///
/// A line from the listing
/// FtpListItem if the item is able to be parsed
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;
}
///
/// Parses Unix format listings with alternate parser
///
/// A line from the listing
/// FtpListItem if the item is able to be parsed
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;
}
///
/// Parses IIS/DOS format listings
///
/// A line from the listing
/// FtpListItem if the item is able to be parsed
private FtpListItem ParseWindows(string raw) {
//-----------------------------------------------------
// EXAMPLES
// 05-17-03 02:47PM 70776 ftp.jar
// 08-28-03 10:08PM 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;
}
///
/// Parses Vax/VMS format listings
///
/// A line from the listing
/// FtpListItem if the item is able to be parsed
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 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;
}
///
/// Parses NonStop format listings
///
/// A line from the listing
/// FtpListItem if the item is able to be parsed
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;
}
///
/// Parses IBM OS/400 format listings
///
/// A line from the listing
/// FtpListItem if the item is able to be parsed
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
///
/// Split into fields by splitting on strings
///
private static string[] SplitString(string str) {
List allTokens = new List(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}(?[\w-]{3})(?[\w-]{3})(?[\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;
}
/// Fix the date string to make the month camel case
/// array of fields
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
}
}