// NAnt - A .NET build tool // Copyright (C) 2001-2002 Gerry Shaw // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // Gerry Shaw (gerry_shaw@yahoo.com) // Kevin Dente (kevindente@yahoo.com) // This is useful for debugging where filesets are scanned from - scanning is one // of the most intensive activities for NAnt //#define DEBUG_REGEXES /* Examples: "**\*.class" matches all .class files/dirs in a directory tree. "test\a??.java" matches all files/dirs which start with an 'a', then two more characters and then ".java", in a directory called test. "**" matches everything in a directory tree. "**\test\**\XYZ*" matches all files/dirs that start with "XYZ" and where there is a parent directory called test (e.g. "abc\test\def\ghi\XYZ123"). Example of usage: DirectoryScanner scanner = DirectoryScanner(); scanner.Includes.Add("**\\*.class"); scanner.Exlucdes.Add("modules\\*\\**"); scanner.BaseDirectory = "test"; scanner.Scan(); foreach (string filename in GetIncludedFiles()) { Console.WriteLine(filename); } */ using System; using System.Collections; using System.Collections.Specialized; using System.Globalization; using System.IO; using System.Text; using System.Text.RegularExpressions; using NAnt.Core.Util; namespace NAnt.Core { /// /// Used for searching filesystem based on given include/exclude rules. /// /// /// Simple client code for testing the class. /// /// while (true) { /// DirectoryScanner scanner = new DirectoryScanner(); /// /// Console.Write("Scan Basedirectory : "); /// string s = Console.ReadLine(); /// if (s.Length == 0) break; /// scanner.BaseDirectory = s; /// /// while(true) { /// Console.Write("Include pattern : "); /// s = Console.ReadLine(); /// if (s.Length == 0) break; /// scanner.Includes.Add(s); /// } /// /// while(true) { /// Console.Write("Exclude pattern : "); /// s = Console.ReadLine(); /// if (s.Length == 0) break; /// scanner.Excludes.Add(s); /// } /// /// foreach (string name in scanner.FileNames) /// Console.WriteLine("file:" + name); /// foreach (string name in scanner.DirectoryNames) /// Console.WriteLine("dir :" + name); /// /// Console.WriteLine(""); /// } /// /// [Serializable()] public class DirectoryScanner : ICloneable { #region Private Instance Fields // set to current directory in Scan if user doesn't specify something first. // keeping it null, lets the user detect if it's been set or not. private DirectoryInfo _baseDirectory; // holds the nant patterns (absolute or relative paths) private StringCollectionWithGoodToString _includes = new StringCollectionWithGoodToString(); private StringCollectionWithGoodToString _excludes = new StringCollectionWithGoodToString(); // holds the nant patterns converted to regular expression patterns (absolute canonized paths) private ArrayList _includePatterns; private ArrayList _excludePatterns; // holds the nant patterns converted to non-regex names (absolute canonized paths) private StringCollectionWithGoodToString _includeNames; private StringCollectionWithGoodToString _excludeNames; // holds the result from a scan private StringCollectionWithGoodToString _fileNames; private DirScannerStringCollection _directoryNames; // directories that should be scanned and directories scanned so far private DirScannerStringCollection _searchDirectories; private DirScannerStringCollection _scannedDirectories; private ArrayList _searchDirIsRecursive; #endregion Private Instance Fields #region Private Static Fields private static readonly log4net.ILog logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); private static Hashtable cachedCaseSensitiveRegexes = new Hashtable(); private static Hashtable cachedCaseInsensitiveRegexes = new Hashtable(); #endregion Private Static Fields #region Implementation of ICloneable /// /// Creates a shallow copy of the . /// /// /// A shallow copy of the . /// public object Clone() { DirectoryScanner clone = new DirectoryScanner(); if (_baseDirectory != null) { clone._baseDirectory = new DirectoryInfo(_baseDirectory.FullName); } if (_directoryNames != null) { clone._directoryNames = (DirScannerStringCollection) _directoryNames.Clone(); } if (_excludePatterns != null) { clone._excludePatterns = (ArrayList) _excludePatterns.Clone(); } if (_excludeNames != null) { clone._excludeNames = (StringCollectionWithGoodToString) _excludeNames.Clone(); } clone._excludes = (StringCollectionWithGoodToString) _excludes.Clone(); if (_fileNames != null) { clone._fileNames = (StringCollectionWithGoodToString) _fileNames.Clone(); } if (_includePatterns != null) { clone._includePatterns = (ArrayList) _includePatterns.Clone(); } if (_includeNames != null) { clone._includeNames = (StringCollectionWithGoodToString) _includeNames.Clone(); } clone._includes = (StringCollectionWithGoodToString) _includes.Clone(); if (_scannedDirectories != null) { clone._scannedDirectories = (DirScannerStringCollection) _scannedDirectories.Clone(); } if (_searchDirectories != null) { clone._searchDirectories = (DirScannerStringCollection) _searchDirectories.Clone(); } if (_searchDirIsRecursive != null) { clone._searchDirIsRecursive = (ArrayList) _searchDirIsRecursive.Clone(); } return clone; } #endregion Implementation of ICloneable #region Public Instance Properties /// /// Gets the collection of include patterns. /// public StringCollectionWithGoodToString Includes { get { return _includes; } } /// /// Gets the collection of exclude patterns. /// public StringCollectionWithGoodToString Excludes { get { return _excludes; } } /// /// The base directory to scan. The default is the /// current directory. /// public DirectoryInfo BaseDirectory { get { if (_baseDirectory == null) { _baseDirectory = new DirectoryInfo(CleanPath( Environment.CurrentDirectory).ToString()); } return _baseDirectory; } set { if (value != null) { // convert both slashes and backslashes to directory separator // char _baseDirectory = new DirectoryInfo(CleanPath( value.FullName).ToString()); } else { _baseDirectory = value; } } } /// /// Gets the list of files that match the given patterns. /// public StringCollectionWithGoodToString FileNames { get { if (_fileNames == null) { Scan(); } return _fileNames; } } /// /// Gets the list of directories that match the given patterns. /// public DirScannerStringCollection DirectoryNames { get { if (_directoryNames == null) { Scan(); } return _directoryNames; } } /// /// Gets the list of directories that were scanned for files. /// public DirScannerStringCollection ScannedDirectories { get { if (_scannedDirectories == null) { Scan(); } return _scannedDirectories; } } #endregion Public Instance Properties #region Public Instance Methods /// /// Uses and search criteria (relative to /// or absolute), to search for filesystem objects. /// public void Scan() { _includePatterns = new ArrayList(); _includeNames = new StringCollectionWithGoodToString (); _excludePatterns = new ArrayList(); _excludeNames = new StringCollectionWithGoodToString (); _fileNames = new StringCollectionWithGoodToString (); _directoryNames = new DirScannerStringCollection(); _searchDirectories = new DirScannerStringCollection(); _searchDirIsRecursive = new ArrayList(); _scannedDirectories = new DirScannerStringCollection(); #if DEBUG_REGEXES Console.WriteLine("*********************************************************************"); Console.WriteLine("DirectoryScanner.Scan()"); Console.WriteLine("*********************************************************************"); Console.WriteLine(new System.Diagnostics.StackTrace().ToString()); Console.WriteLine("Base Directory: " + BaseDirectory.FullName); Console.WriteLine("Includes:"); foreach (string strPattern in _includes) Console.WriteLine(strPattern); Console.WriteLine("Excludes:"); foreach (string strPattern in _excludes) Console.WriteLine(strPattern); Console.WriteLine("--- Starting Scan ---"); #endif // convert given NAnt patterns to regex patterns with absolute paths // side effect: searchDirectories will be populated ConvertPatterns(_includes, _includePatterns, _includeNames, true); ConvertPatterns(_excludes, _excludePatterns, _excludeNames, false); for (int index = 0; index < _searchDirectories.Count; index++) { ScanDirectory(_searchDirectories[index], (bool) _searchDirIsRecursive[index]); } #if DEBUG_REGEXES Console.WriteLine("*********************************************************************"); #endif } #endregion Public Instance Methods #region Private Instance Methods /// /// Parses specified NAnt search patterns for search directories and /// corresponding regex patterns. /// /// In. NAnt patterns. Absolute or relative paths. /// Out. Regex patterns. Absolute canonical paths. /// Out. Non-regex files. Absolute canonical paths. /// In. Whether to allow a pattern to add search directories. private void ConvertPatterns(StringCollection nantPatterns, ArrayList regexPatterns, StringCollection nonRegexFiles, bool addSearchDirectories) { string searchDirectory; string regexPattern; bool isRecursive; bool isRegex; foreach (string nantPattern in nantPatterns) { ParseSearchDirectoryAndPattern(addSearchDirectories, nantPattern, out searchDirectory, out isRecursive, out isRegex, out regexPattern); if (isRegex) { RegexEntry entry = new RegexEntry(); entry.IsRecursive = isRecursive; entry.BaseDirectory = searchDirectory; entry.Pattern = regexPattern; if (regexPattern.EndsWith(@"**/*") || regexPattern.EndsWith(@"**\*")) logger.Warn( "**/* pattern may not produce desired results" ); regexPatterns.Add(entry); } else { string exactName = Path.Combine(searchDirectory, regexPattern); if (!nonRegexFiles.Contains(exactName)) { nonRegexFiles.Add(exactName); } } if (!addSearchDirectories) { continue; } int index = _searchDirectories.IndexOf(searchDirectory); // if the directory was found before, but wasn't recursive // and is now, mark it as so if (index > -1) { if (!(bool)_searchDirIsRecursive[index] && isRecursive) { _searchDirIsRecursive[index] = isRecursive; } } // if the directory has not been added, add it if (index == -1) { _searchDirectories.Add(searchDirectory); _searchDirIsRecursive.Add(isRecursive); } } } /// /// Given a NAnt search pattern returns a search directory and an regex /// search pattern. /// /// Whether this pattern is an include or exclude pattern /// NAnt searh pattern (relative to the Basedirectory OR absolute, relative paths refering to parent directories ( ../ ) also supported) /// Out. Absolute canonical path to the directory to be searched /// Out. Whether the pattern is potentially recursive or not /// Out. Whether this is a regex pattern or not /// Out. Regex search pattern (absolute canonical path) private void ParseSearchDirectoryAndPattern(bool isInclude, string originalNAntPattern, out string searchDirectory, out bool recursive, out bool isRegex, out string regexPattern) { string s = originalNAntPattern; s = s.Replace('\\', Path.DirectorySeparatorChar); s = s.Replace('/', Path.DirectorySeparatorChar); // Get indices of pieces used for recursive check only int indexOfFirstDirectoryWildcard = s.IndexOf("**"); int indexOfLastOriginalDirectorySeparator = s.LastIndexOf(Path.DirectorySeparatorChar); // search for the first wildcard character (if any) and exclude the rest of the string beginnning from the character char[] wildcards = {'?', '*'}; int indexOfFirstWildcard = s.IndexOfAny(wildcards); if (indexOfFirstWildcard != -1) { // if found any wildcard characters s = s.Substring(0, indexOfFirstWildcard); } // find the last DirectorySeparatorChar (if any) and exclude the rest of the string int indexOfLastDirectorySeparator = s.LastIndexOf(Path.DirectorySeparatorChar); // The pattern is potentially recursive if and only if more than one base directory could be matched. // ie: // ** // **/*.txt // foo*/xxx // x/y/z?/www // This condition is true if and only if: // - The first wildcard is before the last directory separator, or // - The pattern contains a directory wildcard ("**") recursive = (indexOfFirstWildcard != -1 && (indexOfFirstWildcard < indexOfLastOriginalDirectorySeparator )) || indexOfFirstDirectoryWildcard != -1; // substring preceding the separator represents our search directory // and the part following it represents nant search pattern relative // to it if (indexOfLastDirectorySeparator != -1) { s = originalNAntPattern.Substring(0, indexOfLastDirectorySeparator); if (s.Length == 2 && s[1] == Path.VolumeSeparatorChar) { s += Path.DirectorySeparatorChar; } } else { s = ""; } //We only prepend BaseDirectory when s represents a relative path. if (Path.IsPathRooted(s)) { searchDirectory = new DirectoryInfo(s).FullName; } else { // we also (correctly) get to this branch of code when s.Length == 0 searchDirectory = new DirectoryInfo(Path.Combine( BaseDirectory.FullName, s)).FullName; } // remove trailing directory separator character, fixes bug #1195736 // // do not remove trailing directory separator if search directory is // root of drive (eg. d:\) if (StringUtils.EndsWith(searchDirectory, Path.DirectorySeparatorChar) && (searchDirectory.Length != 3 || searchDirectory[1] != Path.VolumeSeparatorChar)) { searchDirectory = searchDirectory.Substring(0, searchDirectory.Length - 1); } string modifiedNAntPattern = originalNAntPattern.Substring(indexOfLastDirectorySeparator + 1); // if it's not a wildcard, just return if (indexOfFirstWildcard == -1) { regexPattern = CleanPath(BaseDirectory.FullName, originalNAntPattern); isRegex = false; #if DEBUG_REGEXES Console.WriteLine( "Convert name: {0} -> {1}", originalNAntPattern, regexPattern ); #endif return; } //if the fs in case insensitive, make all the regex directories lowercase. regexPattern = ToRegexPattern(modifiedNAntPattern); #if DEBUG_REGEXES Console.WriteLine( "Convert pattern: {0} -> [{1}]{2}", originalNAntPattern, searchDirectory, regexPattern ); #endif isRegex = true; } private bool IsCaseSensitiveFileSystem(string path) { // Windows (not case-sensitive) is backslash, others (e.g. Unix) are not return (VolumeInfo.IsVolumeCaseSensitive(new Uri(Path.GetFullPath(path) + Path.DirectorySeparatorChar))); } /// /// Searches a directory recursively for files and directories matching /// the search criteria. /// /// Directory in which to search (absolute canonical path) /// Whether to scan recursively or not private void ScanDirectory(string path, bool recursive) { // scan each directory only once if (_scannedDirectories.Contains(path)) { return; } // add directory to list of scanned directories _scannedDirectories.Add(path); // if the path doesn't exist, return. if (!Directory.Exists(path)) { return; } // get info for the current directory DirectoryInfo currentDirectoryInfo = new DirectoryInfo(path); // check whether directory is on case-sensitive volume bool caseSensitive = IsCaseSensitiveFileSystem(path); CompareOptions compareOptions = CompareOptions.None; CompareInfo compare = CultureInfo.InvariantCulture.CompareInfo; if (!caseSensitive) compareOptions |= CompareOptions.IgnoreCase; ArrayList includedPatterns = new ArrayList(); ArrayList excludedPatterns = new ArrayList(); // Only include the valid patterns for this path foreach (RegexEntry entry in _includePatterns) { string baseDirectory = entry.BaseDirectory; // check if the directory being searched is equal to the // basedirectory of the RegexEntry if (compare.Compare(path, baseDirectory, compareOptions) == 0) { includedPatterns.Add(entry); } else { // check if the directory being searched is subdirectory of // basedirectory of RegexEntry if (!entry.IsRecursive) { continue; } // make sure basedirectory ends with directory separator if (!StringUtils.EndsWith(baseDirectory, Path.DirectorySeparatorChar)) { baseDirectory += Path.DirectorySeparatorChar; } // check if path is subdirectory of base directory if (compare.IsPrefix(path, baseDirectory)) { includedPatterns.Add(entry); } } } foreach (RegexEntry entry in _excludePatterns) { string baseDirectory = entry.BaseDirectory; if (entry.BaseDirectory.Length == 0 || compare.Compare(path, baseDirectory, compareOptions) == 0) { excludedPatterns.Add(entry); } else { // check if the directory being searched is subdirectory of // basedirectory of RegexEntry if (!entry.IsRecursive) { continue; } // make sure basedirectory ends with directory separator if (!StringUtils.EndsWith(baseDirectory, Path.DirectorySeparatorChar)) { baseDirectory += Path.DirectorySeparatorChar; } // check if path is subdirectory of base directory if (compare.IsPrefix(path, baseDirectory)) { excludedPatterns.Add(entry); } } } foreach (DirectoryInfo directoryInfo in currentDirectoryInfo.GetDirectories()) { if (recursive) { // scan subfolders if we are running recursively ScanDirectory(directoryInfo.FullName, true); } else { // otherwise just test to see if the subdirectories are included if (IsPathIncluded(directoryInfo.FullName, caseSensitive, includedPatterns, excludedPatterns)) { _directoryNames.Add(directoryInfo.FullName); } } } // scan files foreach (FileInfo fileInfo in currentDirectoryInfo.GetFiles()) { string filename = Path.Combine(path, fileInfo.Name); if (IsPathIncluded(filename, caseSensitive, includedPatterns, excludedPatterns)) { _fileNames.Add(Path.Combine(path, fileInfo.Name)); } } // check current path last so that delete task will correctly // delete empty directories. This may *seem* like a special case // but it is more like formalizing something in a way that makes // writing the delete task easier :) if (IsPathIncluded(path, caseSensitive, includedPatterns, excludedPatterns)) { _directoryNames.Add(path); } } private bool TestRegex(string path, RegexEntry entry, bool caseSensitive) { Hashtable regexCache = caseSensitive ? cachedCaseSensitiveRegexes : cachedCaseInsensitiveRegexes; Regex r = (Regex)regexCache[entry.Pattern]; if (r == null) { RegexOptions regexOptions = RegexOptions.Compiled; if (!caseSensitive) regexOptions |= RegexOptions.IgnoreCase; regexCache[entry.Pattern] = r = new Regex(entry.Pattern, regexOptions); } // Check to see if the empty string matches the pattern if (path.Length == entry.BaseDirectory.Length) { #if DEBUG_REGEXES Console.WriteLine("{0} (empty string) [basedir={1}]", entry.Pattern, entry.BaseDirectory); #endif return r.IsMatch(String.Empty); } bool endsWithSlash = StringUtils.EndsWith(entry.BaseDirectory, Path.DirectorySeparatorChar); #if DEBUG_REGEXES Console.WriteLine("{0} ({1}) [basedir={2}]", entry.Pattern, path.Substring(entry.BaseDirectory.Length + ((endsWithSlash) ? 0 : 1)), entry.BaseDirectory); #endif if (endsWithSlash) { return r.IsMatch(path.Substring(entry.BaseDirectory.Length)); } else { return r.IsMatch(path.Substring(entry.BaseDirectory.Length + 1)); } } private bool IsPathIncluded(string path, bool caseSensitive, ArrayList includedPatterns, ArrayList excludedPatterns) { bool included = false; CompareOptions compareOptions = CompareOptions.None; CompareInfo compare = CultureInfo.InvariantCulture.CompareInfo; if (!caseSensitive) compareOptions |= CompareOptions.IgnoreCase; #if DEBUG_REGEXES Console.WriteLine("Test: {0}", path); #endif // check path against include names foreach (string name in _includeNames) { #if DEBUG_REGEXES Console.WriteLine("Test include name: '{0}'", name); #endif if (compare.Compare(name, path, compareOptions) == 0) { included = true; #if DEBUG_REGEXES Console.WriteLine("Included by name: {0}", name); #endif break; } } // check path against include regexes if (!included) { foreach (RegexEntry entry in includedPatterns) { #if DEBUG_REGEXES Console.Write("Test include pattern: "); #endif if (TestRegex(path, entry, caseSensitive)) { included = true; #if DEBUG_REGEXES Console.WriteLine("Included by pattern: {0}", entry.Pattern); #endif break; } } } // check path against exclude names if (included) { foreach (string name in _excludeNames) { #if DEBUG_REGEXES Console.WriteLine("Test exclude name: '{0}'", name); #endif if (compare.Compare(name, path, compareOptions) == 0) { included = false; #if DEBUG_REGEXES Console.WriteLine("Excluded by name: {0}", name); #endif break; } } } // check path against exclude regexes if (included) { foreach (RegexEntry entry in excludedPatterns) { #if DEBUG_REGEXES Console.Write("Test exclude pattern: "); #endif if (TestRegex(path, entry, caseSensitive)) { included = false; #if DEBUG_REGEXES Console.WriteLine("Excluded by pattern: {0}", entry.Pattern); #endif break; } } } #if DEBUG_REGEXES Console.WriteLine("Result: {0}", included); #endif return included; } #endregion Private Instance Methods #region Private Static Methods private static StringBuilder CleanPath(string nantPath) { StringBuilder pathBuilder = new StringBuilder(nantPath); // NAnt patterns can use either / \ as a directory seperator. // We must replace both of these characters with Path.DirectorySeperatorChar pathBuilder.Replace('/', Path.DirectorySeparatorChar); pathBuilder.Replace('\\', Path.DirectorySeparatorChar); return pathBuilder; } private static string CleanPath(string baseDirectory, string nantPath) { return new DirectoryInfo(Path.Combine(baseDirectory, CleanPath(nantPath).ToString())).FullName; } /// /// Converts search pattern to a regular expression pattern. /// /// Search pattern relative to the search directory. /// Regular expresssion private static string ToRegexPattern(string nantPattern) { StringBuilder pattern = CleanPath(nantPattern); // The '\' character is a special character in regular expressions // and must be escaped before doing anything else. pattern.Replace(@"\", @"\\"); // Escape the rest of the regular expression special characters. // NOTE: Characters other than . $ ^ { [ ( | ) * + ? \ match themselves. // TODO: Decide if ] and } are missing from this list, the above // list of characters was taking from the .NET SDK docs. pattern.Replace(".", @"\."); pattern.Replace("$", @"\$"); pattern.Replace("^", @"\^"); pattern.Replace("{", @"\{"); pattern.Replace("[", @"\["); pattern.Replace("(", @"\("); pattern.Replace(")", @"\)"); pattern.Replace("+", @"\+"); // Special case directory seperator string under Windows. string seperator = Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture); if (seperator == @"\") { seperator = @"\\"; } // workaround for mono bug #72166 string replacementSeparator = null; if (PlatformHelper.IsMono && seperator == @"\\") { replacementSeparator = "MONOBUG72166"; } else { replacementSeparator = seperator; } // Convert NAnt pattern characters to regular expression patterns. // Start with ? - it's used below pattern.Replace("?", "[^" + seperator + "]?"); // SPECIAL CASE: any *'s directory between slashes or at the end of the // path are replaced with a 1..n pattern instead of 0..n: (?<=\\)\*(?=($|\\)) // This ensures that C:\*foo* matches C:\foo and C:\* won't match C:. pattern = new StringBuilder(Regex.Replace(pattern.ToString(), "(?<=" + seperator + ")\\*(?=($|" + seperator + "))", "[^" + replacementSeparator + "]+")); // SPECIAL CASE: to match subdirectory OR current directory, If // we do this then we can write something like 'src/**/*.cs' // to match all the files ending in .cs in the src directory OR // subdirectories of src. pattern.Replace(seperator + "**" + seperator, replacementSeparator + "(.|?" + replacementSeparator + ")?" ); pattern.Replace("**" + seperator, ".|(?<=^|" + replacementSeparator + ")" ); pattern.Replace(seperator + "**", "(?=$|" + replacementSeparator + ").|" ); // .| is a place holder for .* to prevent it from being replaced in next line pattern.Replace("**", ".|"); pattern.Replace("*", "[^" + replacementSeparator + "]*"); pattern.Replace(".|", ".*"); // replace place holder string // Help speed up the search if (pattern.Length > 0) { pattern.Insert(0, '^'); // start of line pattern.Append('$'); // end of line } if (PlatformHelper.IsMono && seperator == @"\\") { pattern.Replace("MONOBUG72166", seperator); } string patternText = pattern.ToString(); if (patternText.StartsWith("^.*")) patternText = patternText.Substring(3); if (patternText.EndsWith(".*$")) patternText = patternText.Substring(0, pattern.Length-3); return patternText.ToString(); } #endregion Private Static Methods [Serializable()] private class RegexEntry { public bool IsRecursive; public string BaseDirectory; public string Pattern; } } [Serializable()] public class StringCollectionWithGoodToString : StringCollection, ICloneable { #region Implementation of ICloneable /// /// Creates a shallow copy of the . /// /// /// A shallow copy of the . /// public virtual object Clone() { string[] strings = new string[Count]; CopyTo(strings, 0); StringCollectionWithGoodToString clone = new StringCollectionWithGoodToString(); clone.AddRange(strings); return clone; } #endregion Implementation of ICloneable #region Override implemenation of Object /// /// Creates a string representing a list of the strings in the collection. /// /// /// A string that represents the contents. /// public override string ToString() { StringBuilder sb = new StringBuilder(base.ToString()); sb.Append(":" + Environment.NewLine); foreach (string s in this) { sb.Append(s); sb.Append(Environment.NewLine); } return sb.ToString(); } #endregion Override implemenation of Object } [Serializable()] public class DirScannerStringCollection : StringCollectionWithGoodToString { #region Override implementation of ICloneable /// /// Creates a shallow copy of the . /// /// /// A shallow copy of the . /// public override object Clone() { string[] strings = new string[Count]; CopyTo(strings, 0); DirScannerStringCollection clone = new DirScannerStringCollection(); clone.AddRange(strings); return clone; } #endregion Override implementation of ICloneable #region Override implementation of StringCollection /// /// Determines whether the specified string is in the /// . /// /// The string to locate in the . The value can be . /// /// if value is found in the ; otherwise, . /// /// /// String comparisons within the /// are only case-sensitive if the filesystem on which /// is located, is case-sensitive. /// public new virtual bool Contains(string value) { return (IndexOf(value) > -1); } /// /// Searches for the specified string and returns the zero-based index /// of the first occurrence within the . /// /// The string to locate. The value can be . /// /// The zero-based index of the first occurrence of /// in the , if found; otherwise, -1. /// /// /// String comparisons within the /// are only case-sensitive if the filesystem on which /// is located, is case-sensitive. /// public new virtual int IndexOf(string value) { if (value == null || IsCaseSensitiveFileSystem(value)) { return base.IndexOf(value); } else { foreach (string s in this) { if (string.Compare(s, value, true, CultureInfo.InvariantCulture) == 0) { return base.IndexOf(s); } } return -1; } } #endregion Override implementation of StringCollection #region Private Instance Methods /// /// Determines whether the filesystem on which the specified path is /// located is case-sensitive. /// /// The path of which should be determined whether its on a case-sensitive filesystem. /// /// if is located on a /// case-sensitive filesystem; otherwise, . /// private bool IsCaseSensitiveFileSystem(string path) { return PlatformHelper.IsVolumeCaseSensitive(Path.GetFullPath(path) + Path.DirectorySeparatorChar); } #endregion Private Instance Methods } }