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 } }