diff --git a/src/System.IO.FileSystem/src/Microsoft/Win32/SafeHandles/SafeFindHandle.Windows.cs b/src/System.IO.FileSystem/src/Microsoft/Win32/SafeHandles/SafeFindHandle.Windows.cs index c9c118c1b51d..dc02183f4306 100644 --- a/src/System.IO.FileSystem/src/Microsoft/Win32/SafeHandles/SafeFindHandle.Windows.cs +++ b/src/System.IO.FileSystem/src/Microsoft/Win32/SafeHandles/SafeFindHandle.Windows.cs @@ -3,10 +3,7 @@ // See the LICENSE file in the project root for more information. using System; -using System.Security; using System.Runtime.InteropServices; -using System.Runtime.CompilerServices; -using Microsoft.Win32; namespace Microsoft.Win32.SafeHandles { diff --git a/src/System.IO.FileSystem/src/System/IO/Directory.cs b/src/System.IO.FileSystem/src/System/IO/Directory.cs index d5e09c82819c..93e2d57c00ad 100644 --- a/src/System.IO.FileSystem/src/System/IO/Directory.cs +++ b/src/System.IO.FileSystem/src/System/IO/Directory.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.IO.Enumeration; using System.Linq; -using System.Security; namespace System.IO { @@ -42,9 +41,6 @@ public static DirectoryInfo CreateDirectory(string path) } // Tests if the given path refers to an existing DirectoryInfo on disk. - // - // Your application must have Read permission to the directory's - // contents. public static bool Exists(string path) { try @@ -59,8 +55,6 @@ public static bool Exists(string path) return FileSystem.DirectoryExists(fullPath); } catch (ArgumentException) { } - catch (NotSupportedException) { } // Security can throw this on ":" - catch (SecurityException) { } catch (IOException) { } catch (UnauthorizedAccessException) { } diff --git a/src/System.IO.FileSystem/src/System/IO/File.cs b/src/System.IO.FileSystem/src/System/IO/File.cs index 9af91fb0a669..2253c2b68c25 100644 --- a/src/System.IO.FileSystem/src/System/IO/File.cs +++ b/src/System.IO.FileSystem/src/System/IO/File.cs @@ -5,7 +5,6 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; -using System.Security; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -44,39 +43,18 @@ public static StreamWriter AppendText(string path) return new StreamWriter(path, append: true); } - - // Copies an existing file to a new file. An exception is raised if the - // destination file already exists. Use the - // Copy(string, string, boolean) method to allow - // overwriting an existing file. - // - // The caller must have certain FileIOPermissions. The caller must have - // Read permission to sourceFileName and Create - // and Write permissions to destFileName. - // + /// + /// Copies an existing file to a new file. + /// An exception is raised if the destination file already exists. + /// public static void Copy(string sourceFileName, string destFileName) - { - if (sourceFileName == null) - throw new ArgumentNullException(nameof(sourceFileName), SR.ArgumentNull_FileName); - if (destFileName == null) - throw new ArgumentNullException(nameof(destFileName), SR.ArgumentNull_FileName); - if (sourceFileName.Length == 0) - throw new ArgumentException(SR.Argument_EmptyFileName, nameof(sourceFileName)); - if (destFileName.Length == 0) - throw new ArgumentException(SR.Argument_EmptyFileName, nameof(destFileName)); - - InternalCopy(sourceFileName, destFileName, false); - } + => Copy(sourceFileName, destFileName, overwrite: false); - // Copies an existing file to a new file. If overwrite is - // false, then an IOException is thrown if the destination file - // already exists. If overwrite is true, the file is - // overwritten. - // - // The caller must have certain FileIOPermissions. The caller must have - // Read permission to sourceFileName - // and Write permissions to destFileName. - // + /// + /// Copies an existing file to a new file. + /// If is false, an exception will be + /// raised if the destination exists. Otherwise it will be overwritten. + /// public static void Copy(string sourceFileName, string destFileName, bool overwrite) { if (sourceFileName == null) @@ -88,36 +66,13 @@ public static void Copy(string sourceFileName, string destFileName, bool overwri if (destFileName.Length == 0) throw new ArgumentException(SR.Argument_EmptyFileName, nameof(destFileName)); - InternalCopy(sourceFileName, destFileName, overwrite); + FileSystem.CopyFile(Path.GetFullPath(sourceFileName), Path.GetFullPath(destFileName), overwrite); } - /// - /// Note: This returns the fully qualified name of the destination file. - /// - internal static string InternalCopy(string sourceFileName, string destFileName, bool overwrite) - { - Debug.Assert(sourceFileName != null); - Debug.Assert(destFileName != null); - Debug.Assert(sourceFileName.Length > 0); - Debug.Assert(destFileName.Length > 0); - - string fullSourceFileName = Path.GetFullPath(sourceFileName); - string fullDestFileName = Path.GetFullPath(destFileName); - - FileSystem.CopyFile(fullSourceFileName, fullDestFileName, overwrite); - - return fullDestFileName; - } - - // Creates a file in a particular path. If the file exists, it is replaced. // The file is opened with ReadWrite access and cannot be opened by another // application until it has been closed. An IOException is thrown if the // directory specified doesn't exist. - // - // Your application must have Create, Read, and Write permissions to - // the file. - // public static FileStream Create(string path) { return Create(path, DefaultBufferSize); @@ -127,48 +82,30 @@ public static FileStream Create(string path) // The file is opened with ReadWrite access and cannot be opened by another // application until it has been closed. An IOException is thrown if the // directory specified doesn't exist. - // - // Your application must have Create, Read, and Write permissions to - // the file. - // public static FileStream Create(string path, int bufferSize) - { - return new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize); - } + => new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize); public static FileStream Create(string path, int bufferSize, FileOptions options) - { - return new FileStream(path, FileMode.Create, FileAccess.ReadWrite, - FileShare.None, bufferSize, options); - } + => new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize, options); // Deletes a file. The file specified by the designated path is deleted. // If the file does not exist, Delete succeeds without throwing // an exception. // - // On NT, Delete will fail for a file that is open for normal I/O - // or a file that is memory mapped. - // - // Your application must have Delete permission to the target file. - // + // On Windows, Delete will fail for a file that is open for normal I/O + // or a file that is memory mapped. public static void Delete(string path) { if (path == null) throw new ArgumentNullException(nameof(path)); - string fullPath = Path.GetFullPath(path); - - FileSystem.DeleteFile(fullPath); + FileSystem.DeleteFile(Path.GetFullPath(path)); } - - // Tests if a file exists. The result is true if the file + // Tests whether a file exists. The result is true if the file // given by the specified path exists; otherwise, the result is // false. Note that if path describes a directory, // Exists will return true. - // - // Your application must have Read permission for the target directory. - // public static bool Exists(string path) { try @@ -179,6 +116,7 @@ public static bool Exists(string path) return false; path = Path.GetFullPath(path); + // After normalizing, check whether path ends in directory separator. // Otherwise, FillAttributeInfo removes it and we may return a false positive. // GetFullPath should never return null @@ -191,8 +129,6 @@ public static bool Exists(string path) return FileSystem.FileExists(path); } catch (ArgumentException) { } - catch (NotSupportedException) { } // Security can throw this on ":" - catch (SecurityException) { } catch (IOException) { } catch (UnauthorizedAccessException) { } diff --git a/src/System.IO.FileSystem/src/System/IO/FileInfo.cs b/src/System.IO.FileSystem/src/System/IO/FileInfo.cs index 3811ed8a2274..c6b3589decca 100644 --- a/src/System.IO.FileSystem/src/System/IO/FileInfo.cs +++ b/src/System.IO.FileSystem/src/System/IO/FileInfo.cs @@ -71,23 +71,15 @@ public bool IsReadOnly } public StreamReader OpenText() - => new StreamReader(FullPath, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); + => new StreamReader(NormalizedPath, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); public StreamWriter CreateText() - => new StreamWriter(FullPath, append: false); + => new StreamWriter(NormalizedPath, append: false); public StreamWriter AppendText() - => new StreamWriter(FullPath, append: true); + => new StreamWriter(NormalizedPath, append: true); - public FileInfo CopyTo(string destFileName) - { - if (destFileName == null) - throw new ArgumentNullException(nameof(destFileName), SR.ArgumentNull_FileName); - if (destFileName.Length == 0) - throw new ArgumentException(SR.Argument_EmptyFileName, nameof(destFileName)); - - return new FileInfo(File.InternalCopy(FullPath, destFileName, false), isNormalized: true); - } + public FileInfo CopyTo(string destFileName) => CopyTo(destFileName, overwrite: false); public FileInfo CopyTo(string destFileName, bool overwrite) { @@ -96,10 +88,12 @@ public FileInfo CopyTo(string destFileName, bool overwrite) if (destFileName.Length == 0) throw new ArgumentException(SR.Argument_EmptyFileName, nameof(destFileName)); - return new FileInfo(File.InternalCopy(FullPath, destFileName, overwrite), isNormalized: true); + string destinationPath = Path.GetFullPath(destFileName); + FileSystem.CopyFile(FullPath, destinationPath, overwrite); + return new FileInfo(destinationPath, isNormalized: true); } - public FileStream Create() => File.Create(FullPath); + public FileStream Create() => File.Create(NormalizedPath); public override void Delete() => FileSystem.DeleteFile(FullPath); @@ -110,13 +104,13 @@ public FileStream Open(FileMode mode, FileAccess access) => Open(mode, access, FileShare.None); public FileStream Open(FileMode mode, FileAccess access, FileShare share) - => new FileStream(FullPath, mode, access, share); + => new FileStream(NormalizedPath, mode, access, share); public FileStream OpenRead() - => new FileStream(FullPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, false); + => new FileStream(NormalizedPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, false); public FileStream OpenWrite() - => new FileStream(FullPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); + => new FileStream(NormalizedPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); // Moves a given file to a new location and potentially a new file name. // This method does work across volumes. @@ -153,7 +147,15 @@ public FileInfo Replace(string destinationFileName, string destinationBackupFile public FileInfo Replace(string destinationFileName, string destinationBackupFileName, bool ignoreMetadataErrors) { - File.Replace(FullPath, destinationFileName, destinationBackupFileName, ignoreMetadataErrors); + if (destinationFileName == null) + throw new ArgumentNullException(nameof(destinationFileName)); + + FileSystem.ReplaceFile( + FullPath, + Path.GetFullPath(destinationFileName), + destinationBackupFileName != null ? Path.GetFullPath(destinationBackupFileName) : null, + ignoreMetadataErrors); + return new FileInfo(destinationFileName); } diff --git a/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs b/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs index cbc0e9be0560..aac479e6e117 100644 --- a/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs +++ b/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs @@ -78,5 +78,8 @@ internal static void ThrowNotFound(string path) bool directoryError = !Directory.Exists(Path.GetDirectoryName(PathInternal.TrimEndingDirectorySeparator(path))); throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(Interop.Error.ENOENT), path, directoryError); } + + // There is no special handling for Unix- see Windows code for the reason we do this + internal string NormalizedPath => FullPath; } } diff --git a/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs b/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs index bfec961847ee..3ea48437e85d 100644 --- a/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs +++ b/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs @@ -150,5 +150,9 @@ public void Refresh() // when someone actually accesses a property _dataInitialized = FileSystem.FillAttributeInfo(FullPath, ref _data, returnErrorOnNotFound: false); } + + // If we're opened around a enumerated path that ends in a period or space we need to be able to + // act on the path normally (open streams/writers/etc.) + internal string NormalizedPath => PathInternal.EnsureExtendedPrefixIfNeeded(FullPath); } } diff --git a/src/System.IO.FileSystem/tests/Enumeration/TrimmedPaths.netcoreapp.cs b/src/System.IO.FileSystem/tests/Enumeration/TrimmedPaths.netcoreapp.cs index 8621e8eb662a..7ae7943b3b03 100644 --- a/src/System.IO.FileSystem/tests/Enumeration/TrimmedPaths.netcoreapp.cs +++ b/src/System.IO.FileSystem/tests/Enumeration/TrimmedPaths.netcoreapp.cs @@ -17,11 +17,11 @@ public void TrimmedPathsAreFound_Windows() // to access without using the \\?\ device syntax. We should, however, be able to find them // and retain the filename in the info classes and string results. - var directory = Directory.CreateDirectory(GetTestFilePath()); + DirectoryInfo directory = Directory.CreateDirectory(GetTestFilePath()); File.Create(@"\\?\" + Path.Combine(directory.FullName, "Trailing space ")).Dispose(); File.Create(@"\\?\" + Path.Combine(directory.FullName, "Trailing period.")).Dispose(); - var files = directory.GetFiles(); + FileInfo[] files = directory.GetFiles(); Assert.Equal(2, files.Count()); FSAssert.EqualWhenOrdered(new string[] { "Trailing space ", "Trailing period." }, files.Select(f => f.Name)); @@ -38,7 +38,7 @@ public void TrimmedPathsDeletion_Windows() // to access without using the \\?\ device syntax. We should, however, be able to delete them // from the info class. - var directory = Directory.CreateDirectory(GetTestFilePath()); + DirectoryInfo directory = Directory.CreateDirectory(GetTestFilePath()); File.Create(@"\\?\" + Path.Combine(directory.FullName, "Trailing space ")).Dispose(); File.Create(@"\\?\" + Path.Combine(directory.FullName, "Trailing period.")).Dispose(); @@ -47,18 +47,183 @@ public void TrimmedPathsDeletion_Windows() var paths = Directory.GetFiles(directory.FullName); Assert.All(paths, p => Assert.False(File.Exists(p))); - var files = directory.GetFiles(); + FileInfo[] files = directory.GetFiles(); Assert.Equal(2, files.Count()); Assert.All(files, f => Assert.True(f.Exists)); - foreach (var f in files) + foreach (FileInfo f in files) f.Refresh(); Assert.All(files, f => Assert.True(f.Exists)); - foreach (var f in files) + foreach (FileInfo f in files) { f.Delete(); f.Refresh(); } Assert.All(files, f => Assert.False(f.Exists)); + + foreach (FileInfo f in files) + { + f.Create().Dispose(); + f.Refresh(); + } + Assert.All(files, f => Assert.True(f.Exists)); + } + + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] + public void TrimmedPathsOpen_Windows() + { + // Trailing spaces and periods are eaten when normalizing in Windows, making them impossible + // to access without using the \\?\ device syntax. We should, however, be able to open them + // from the info class when enumerating. + + DirectoryInfo directory = Directory.CreateDirectory(GetTestFilePath()); + string fileOne = Path.Join(directory.FullName, "Trailing space "); + string fileTwo = Path.Join(directory.FullName, "Trailing period."); + File.Create(@"\\?\" + fileOne).Dispose(); + File.Create(@"\\?\" + fileTwo).Dispose(); + + FileInfo[] files = directory.GetFiles(); + Assert.Equal(2, files.Length); + foreach (FileInfo fi in directory.GetFiles()) + { + // Shouldn't throw hitting any of the Open overloads + using (FileStream stream = fi.Open(FileMode.Open)) + { } + using (FileStream stream = fi.Open(FileMode.Open, FileAccess.Read)) + { } + using (FileStream stream = fi.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { } + using (FileStream stream = fi.OpenRead()) + { } + using (FileStream stream = fi.OpenWrite()) + { } + } + } + + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] + public void TrimmedPathsText_Windows() + { + // Trailing spaces and periods are eaten when normalizing in Windows, making them impossible + // to access without using the \\?\ device syntax. We should, however, be able to open readers + // and writers from the the info class when enumerating. + + DirectoryInfo directory = Directory.CreateDirectory(GetTestFilePath()); + string fileOne = Path.Join(directory.FullName, "Trailing space "); + string fileTwo = Path.Join(directory.FullName, "Trailing period."); + File.WriteAllText(@"\\?\" + fileOne, "space"); + File.WriteAllText(@"\\?\" + fileTwo, "period"); + + FileInfo[] files = directory.GetFiles(); + Assert.Equal(2, files.Length); + foreach (FileInfo fi in directory.GetFiles()) + { + using (StreamReader reader = fi.OpenText()) + { + string content = reader.ReadToEnd(); + if (fi.FullName.EndsWith(fileOne)) + { + Assert.Equal("space", content); + } + else if (fi.FullName.EndsWith(fileTwo)) + { + Assert.Equal("period", content); + } + else + { + Assert.False(true, $"Unexpected name '{fi.FullName}'"); + } + } + + using (StreamWriter writer = fi.CreateText()) + { + writer.Write("foo"); + } + + using (StreamReader reader = fi.OpenText()) + { + Assert.Equal("foo", reader.ReadToEnd()); + } + + using (StreamWriter writer = fi.AppendText()) + { + writer.Write("bar"); + } + + using (StreamReader reader = fi.OpenText()) + { + Assert.Equal("foobar", reader.ReadToEnd()); + } + } + } + + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] + public void TrimmedPathsCopyTo_Windows() + { + // Trailing spaces and periods are eaten when normalizing in Windows, making them impossible + // to access without using the \\?\ device syntax. We should, however, be able to copy them + // without the special syntax from the info class when enumerating. + + DirectoryInfo directory = Directory.CreateDirectory(GetTestFilePath()); + string fileOne = Path.Join(directory.FullName, "Trailing space "); + string fileTwo = Path.Join(directory.FullName, "Trailing period."); + File.Create(@"\\?\" + fileOne).Dispose(); + File.Create(@"\\?\" + fileTwo).Dispose(); + + FileInfo[] files = directory.GetFiles(); + Assert.Equal(2, files.Length); + foreach (FileInfo fi in directory.GetFiles()) + { + FileInfo newInfo = fi.CopyTo(Path.Join(directory.FullName, GetTestFileName())); + Assert.True(newInfo.Exists); + FileInfo newerInfo = fi.CopyTo(Path.Join(directory.FullName, GetTestFileName()), overwrite: true); + Assert.True(newerInfo.Exists); + } + + Assert.Equal(6, directory.GetFiles().Length); + } + + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] + public void TrimmedPathsReplace_Windows() + { + // Trailing spaces and periods are eaten when normalizing in Windows, making them impossible + // to access without using the \\?\ device syntax. We should, however, be able to replace them + // from the info class when enumerating. + + DirectoryInfo directory = Directory.CreateDirectory(GetTestFilePath()); + string fileOne = Path.Join(directory.FullName, "Trailing space "); + string fileTwo = Path.Join(directory.FullName, "Trailing period."); + File.WriteAllText(@"\\?\" + fileOne, "space"); + File.WriteAllText(@"\\?\" + fileTwo, "period"); + + FileInfo[] files = directory.GetFiles(); + Assert.Equal(2, files.Length); + + FileInfo destination = new FileInfo(Path.Join(directory.FullName, GetTestFileName())); + destination.Create().Dispose(); + + foreach (FileInfo fi in files) + { + fi.Replace(destination.FullName, null); + using (StreamReader reader = destination.OpenText()) + { + string content = reader.ReadToEnd(); + if (fi.FullName.EndsWith(fileOne)) + { + Assert.Equal("space", content); + } + else if (fi.FullName.EndsWith(fileTwo)) + { + Assert.Equal("period", content); + } + else + { + Assert.False(true, $"Unexpected name '{fi.FullName}'"); + } + } + } } } }